Skip to main content

safe_chains/cst/
display.rs

1use std::fmt;
2use super::*;
3
4fn write_sep(f: &mut fmt::Formatter<'_>, trailing_op: Option<ListOp>) -> fmt::Result {
5    if !matches!(trailing_op, Some(ListOp::Semi)) {
6        f.write_str(";")?;
7    }
8    Ok(())
9}
10
11fn write_body(f: &mut fmt::Formatter<'_>, script: &Script) -> fmt::Result {
12    for (i, stmt) in script.0.iter().enumerate() {
13        if i > 0 {
14            f.write_str(" ")?;
15        }
16        write!(f, "{}", stmt.pipeline)?;
17        match &stmt.op {
18            Some(ListOp::Semi) | None => f.write_str(";")?,
19            Some(op) => write!(f, " {op}")?,
20        }
21    }
22    Ok(())
23}
24
25impl fmt::Display for Script {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        for (i, stmt) in self.0.iter().enumerate() {
28            if i > 0 {
29                f.write_str(" ")?;
30            }
31            write!(f, "{}", stmt.pipeline)?;
32            match &stmt.op {
33                Some(ListOp::Semi) => f.write_str(";")?,
34                Some(op) => write!(f, " {op}")?,
35                None => {}
36            }
37        }
38        Ok(())
39    }
40}
41
42impl fmt::Display for ListOp {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            ListOp::And => f.write_str("&&"),
46            ListOp::Or => f.write_str("||"),
47            ListOp::Semi => f.write_str(";"),
48            ListOp::Amp => f.write_str("&"),
49        }
50    }
51}
52
53impl fmt::Display for Pipeline {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        if self.bang {
56            f.write_str("! ")?;
57        }
58        for (i, cmd) in self.commands.iter().enumerate() {
59            if i > 0 {
60                f.write_str(" | ")?;
61            }
62            write!(f, "{cmd}")?;
63        }
64        Ok(())
65    }
66}
67
68impl fmt::Display for Cmd {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Cmd::Simple(s) => write!(f, "{s}"),
72            Cmd::Subshell { body, redirs } => {
73                write!(f, "({body})")?;
74                for r in redirs {
75                    write!(f, " {r}")?;
76                }
77                Ok(())
78            }
79            Cmd::BraceGroup { body, redirs } => {
80                write!(f, "{{ {body}; }}")?;
81                for r in redirs {
82                    write!(f, " {r}")?;
83                }
84                Ok(())
85            }
86            Cmd::For { var, items, body } => {
87                write!(f, "for {var}")?;
88                if !items.is_empty() {
89                    f.write_str(" in")?;
90                    for item in items {
91                        write!(f, " {item}")?;
92                    }
93                }
94                write_sep(f, None)?;
95                write!(f, " do ")?;
96                write_body(f, body)?;
97                f.write_str(" done")
98            }
99            Cmd::While { cond, body } => {
100                write!(f, "while {cond}")?;
101                write_sep(f, cond.0.last().and_then(|s| s.op))?;
102                write!(f, " do ")?;
103                write_body(f, body)?;
104                f.write_str(" done")
105            }
106            Cmd::Until { cond, body } => {
107                write!(f, "until {cond}")?;
108                write_sep(f, cond.0.last().and_then(|s| s.op))?;
109                write!(f, " do ")?;
110                write_body(f, body)?;
111                f.write_str(" done")
112            }
113            Cmd::If { branches, else_body } => {
114                for (i, branch) in branches.iter().enumerate() {
115                    if i == 0 {
116                        write!(f, "if {}", branch.cond)?;
117                    } else {
118                        write!(f, " elif {}", branch.cond)?;
119                    }
120                    write_sep(f, branch.cond.0.last().and_then(|s| s.op))?;
121                    write!(f, " then ")?;
122                    write_body(f, &branch.body)?;
123                    f.write_str("")?;
124                }
125                if let Some(eb) = else_body {
126                    write!(f, " else ")?;
127                    write_body(f, eb)?;
128                }
129                f.write_str(" fi")
130            }
131            Cmd::DoubleBracket { words, redirs } => {
132                f.write_str("[[")?;
133                for w in words {
134                    write!(f, " {w}")?;
135                }
136                f.write_str(" ]]")?;
137                for r in redirs {
138                    write!(f, " {r}")?;
139                }
140                Ok(())
141            }
142        }
143    }
144}
145
146impl fmt::Display for SimpleCmd {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        let mut first = true;
149        for (name, val) in &self.env {
150            if !first { f.write_str(" ")?; }
151            first = false;
152            write!(f, "{name}={val}")?;
153        }
154        for w in &self.words {
155            if !first { f.write_str(" ")?; }
156            first = false;
157            write!(f, "{w}")?;
158        }
159        for r in &self.redirs {
160            if !first { f.write_str(" ")?; }
161            first = false;
162            write!(f, "{r}")?;
163        }
164        Ok(())
165    }
166}
167
168impl fmt::Display for Word {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        for part in &self.0 {
171            write!(f, "{part}")?;
172        }
173        Ok(())
174    }
175}
176
177impl fmt::Display for WordPart {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            WordPart::Lit(s) => f.write_str(s),
181            WordPart::Escape(c) => write!(f, "\\{c}"),
182            WordPart::SQuote(s) => write!(f, "'{s}'"),
183            WordPart::DQuote(w) => write!(f, "\"{w}\""),
184            WordPart::CmdSub(s) => {
185                let rendered = s.to_string();
186                if rendered.starts_with('(') {
187                    write!(f, "$( {rendered})")
188                } else {
189                    write!(f, "$({rendered})")
190                }
191            }
192            WordPart::ProcSub(s) => write!(f, "<({s})"),
193            WordPart::Backtick(s) => write!(f, "`{s}`"),
194            WordPart::Arith(s) => write!(f, "$(({s}))"),
195        }
196    }
197}
198
199impl fmt::Display for Redir {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            Redir::Write { fd, target, append } => {
203                if *fd != 1 { write!(f, "{fd}")?; }
204                if *append { write!(f, ">> {target}") } else { write!(f, "> {target}") }
205            }
206            Redir::Read { fd, target } => {
207                if *fd != 0 { write!(f, "{fd}")?; }
208                write!(f, "< {target}")
209            }
210            Redir::HereStr(w) => write!(f, "<<< {w}"),
211            Redir::HereDoc { delimiter, strip_tabs } => {
212                if *strip_tabs { write!(f, "<<-{delimiter}") } else { write!(f, "<<{delimiter}") }
213            }
214            Redir::DupFd { src, dst } => {
215                if *src != 1 { write!(f, "{src}")?; }
216                write!(f, ">&{dst}")
217            }
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use crate::cst::parse;
225
226    #[test]
227    fn display_simple() {
228        let s = parse("echo hello").unwrap();
229        assert_eq!(s.to_string(), "echo hello");
230    }
231
232    #[test]
233    fn display_pipeline() {
234        let s = parse("grep foo | head -5").unwrap();
235        assert_eq!(s.to_string(), "grep foo | head -5");
236    }
237
238    #[test]
239    fn display_sequence() {
240        let s = parse("ls && echo done").unwrap();
241        assert_eq!(s.to_string(), "ls && echo done");
242    }
243
244    #[test]
245    fn display_single_quoted() {
246        let s = parse("echo 'hello world'").unwrap();
247        assert_eq!(s.to_string(), "echo 'hello world'");
248    }
249
250    #[test]
251    fn display_double_quoted() {
252        let s = parse("echo \"hello world\"").unwrap();
253        assert_eq!(s.to_string(), "echo \"hello world\"");
254    }
255
256    #[test]
257    fn display_redirect() {
258        let s = parse("echo hello > /dev/null").unwrap();
259        assert_eq!(s.to_string(), "echo hello > /dev/null");
260    }
261
262    #[test]
263    fn display_fd_redirect() {
264        let s = parse("echo hello 2>&1").unwrap();
265        assert_eq!(s.to_string(), "echo hello 2>&1");
266    }
267
268    #[test]
269    fn display_cmd_sub() {
270        let s = parse("echo $(ls)").unwrap();
271        assert_eq!(s.to_string(), "echo $(ls)");
272    }
273
274    #[test]
275    fn display_for() {
276        let s = parse("for x in 1 2 3; do echo $x; done").unwrap();
277        assert_eq!(s.to_string(), "for x in 1 2 3; do echo $x; done");
278    }
279
280    #[test]
281    fn display_if() {
282        let s = parse("if true; then echo yes; else echo no; fi").unwrap();
283        assert_eq!(s.to_string(), "if true; then echo yes; else echo no; fi");
284    }
285
286    #[test]
287    fn display_env_prefix() {
288        let s = parse("FOO=bar ls").unwrap();
289        assert_eq!(s.to_string(), "FOO=bar ls");
290    }
291
292    #[test]
293    fn display_subshell() {
294        let s = parse("(echo hello)").unwrap();
295        assert_eq!(s.to_string(), "(echo hello)");
296    }
297
298    #[test]
299    fn display_negation() {
300        let s = parse("! echo hello").unwrap();
301        assert_eq!(s.to_string(), "! echo hello");
302    }
303}