Skip to main content

safe_chains/cst/
display.rs

1use super::*;
2use std::fmt;
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_case_arm(f: &mut fmt::Formatter<'_>, arm: &CaseArm) -> fmt::Result {
12    f.write_str(" ")?;
13    for (i, p) in arm.patterns.iter().enumerate() {
14        if i > 0 {
15            f.write_str("|")?;
16        }
17        write!(f, "{p}")?;
18    }
19    f.write_str(") ")?;
20    // `Script`'s Display terminates a trailing `Semi` itself, and `;;` is a token rather than a
21    // separator, so it is written detached: `write_body` here would render `a;` + `;;` = `a;;;`,
22    // which is not the same tree.
23    write!(f, "{}", arm.body)?;
24    f.write_str(" ;;")
25}
26
27fn write_redirs(f: &mut fmt::Formatter<'_>, redirs: &[Redir]) -> fmt::Result {
28    for r in redirs {
29        write!(f, " {r}")?;
30    }
31    Ok(())
32}
33
34fn write_body(f: &mut fmt::Formatter<'_>, script: &Script) -> fmt::Result {
35    for (i, stmt) in script.0.iter().enumerate() {
36        if i > 0 {
37            f.write_str(" ")?;
38        }
39        write!(f, "{}", stmt.pipeline)?;
40        match &stmt.op {
41            Some(ListOp::Semi) | None => f.write_str(";")?,
42            Some(op) => write!(f, " {op}")?,
43        }
44    }
45    Ok(())
46}
47
48impl fmt::Display for Script {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        for (i, stmt) in self.0.iter().enumerate() {
51            if i > 0 {
52                f.write_str(" ")?;
53            }
54            write!(f, "{}", stmt.pipeline)?;
55            match &stmt.op {
56                Some(ListOp::Semi) => f.write_str(";")?,
57                Some(op) => write!(f, " {op}")?,
58                None => {}
59            }
60        }
61        Ok(())
62    }
63}
64
65impl fmt::Display for ListOp {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            ListOp::And => f.write_str("&&"),
69            ListOp::Or => f.write_str("||"),
70            ListOp::Semi => f.write_str(";"),
71            ListOp::Amp => f.write_str("&"),
72        }
73    }
74}
75
76impl fmt::Display for Pipeline {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        if self.bang {
79            f.write_str("! ")?;
80        }
81        for (i, cmd) in self.commands.iter().enumerate() {
82            if i > 0 {
83                f.write_str(" | ")?;
84            }
85            write!(f, "{cmd}")?;
86        }
87        Ok(())
88    }
89}
90
91impl fmt::Display for Cmd {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            Cmd::Simple(s) => write!(f, "{s}"),
95            Cmd::Subshell { body, redirs } => {
96                write!(f, "({body})")?;
97                for r in redirs {
98                    write!(f, " {r}")?;
99                }
100                Ok(())
101            }
102            // `Script`'s Display already terminates a trailing `Semi` statement, so `write_sep`
103            // supplies the `;` only when it is missing. Appending one unconditionally emitted
104            // `{ echo hi;; }`, which re-parsed as the same tree only while `;;` meant nothing.
105            Cmd::BraceGroup { body, redirs } => {
106                write!(f, "{{ {body}")?;
107                write_sep(f, body.0.last().and_then(|s| s.op))?;
108                f.write_str(" }")?;
109                for r in redirs {
110                    write!(f, " {r}")?;
111                }
112                Ok(())
113            }
114            Cmd::FunctionDef { name, body } => {
115                write!(f, "{name}() {{ {body}")?;
116                write_sep(f, body.0.last().and_then(|s| s.op))?;
117                f.write_str(" }")
118            }
119            Cmd::For { var, items, body, redirs } => {
120                write!(f, "for {var}")?;
121                if !items.is_empty() {
122                    f.write_str(" in")?;
123                    for item in items {
124                        write!(f, " {item}")?;
125                    }
126                }
127                write_sep(f, None)?;
128                write!(f, " do ")?;
129                write_body(f, body)?;
130                f.write_str(" done")?;
131                write_redirs(f, redirs)
132            }
133            Cmd::While { cond, body, redirs } => {
134                write!(f, "while {cond}")?;
135                write_sep(f, cond.0.last().and_then(|s| s.op))?;
136                write!(f, " do ")?;
137                write_body(f, body)?;
138                f.write_str(" done")?;
139                write_redirs(f, redirs)
140            }
141            Cmd::Until { cond, body, redirs } => {
142                write!(f, "until {cond}")?;
143                write_sep(f, cond.0.last().and_then(|s| s.op))?;
144                write!(f, " do ")?;
145                write_body(f, body)?;
146                f.write_str(" done")?;
147                write_redirs(f, redirs)
148            }
149            Cmd::If { branches, else_body, redirs } => {
150                for (i, branch) in branches.iter().enumerate() {
151                    if i == 0 {
152                        write!(f, "if {}", branch.cond)?;
153                    } else {
154                        write!(f, " elif {}", branch.cond)?;
155                    }
156                    write_sep(f, branch.cond.0.last().and_then(|s| s.op))?;
157                    write!(f, " then ")?;
158                    write_body(f, &branch.body)?;
159                    f.write_str("")?;
160                }
161                if let Some(eb) = else_body {
162                    write!(f, " else ")?;
163                    write_body(f, eb)?;
164                }
165                f.write_str(" fi")?;
166                write_redirs(f, redirs)
167            }
168            Cmd::Case { subject, arms, redirs } => {
169                write!(f, "case {subject} in")?;
170                for arm in arms {
171                    write_case_arm(f, arm)?;
172                }
173                f.write_str(" esac")?;
174                write_redirs(f, redirs)
175            }
176            Cmd::DoubleBracket { words, redirs } => {
177                f.write_str("[[")?;
178                for w in words {
179                    write!(f, " {w}")?;
180                }
181                f.write_str(" ]]")?;
182                for r in redirs {
183                    write!(f, " {r}")?;
184                }
185                Ok(())
186            }
187        }
188    }
189}
190
191impl fmt::Display for SimpleCmd {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        let mut first = true;
194        for (name, val) in &self.env {
195            if !first {
196                f.write_str(" ")?;
197            }
198            first = false;
199            write!(f, "{name}={val}")?;
200        }
201        for w in &self.words {
202            if !first {
203                f.write_str(" ")?;
204            }
205            first = false;
206            write!(f, "{w}")?;
207        }
208        for r in &self.redirs {
209            if !first {
210                f.write_str(" ")?;
211            }
212            first = false;
213            write!(f, "{r}")?;
214        }
215        Ok(())
216    }
217}
218
219impl fmt::Display for Word {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        for part in &self.0 {
222            write!(f, "{part}")?;
223        }
224        Ok(())
225    }
226}
227
228impl fmt::Display for WordPart {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            WordPart::Lit(s) => f.write_str(s),
232            WordPart::Escape(c) => write!(f, "\\{c}"),
233            WordPart::SQuote(s) => write!(f, "'{s}'"),
234            WordPart::DQuote(w) => write!(f, "\"{w}\""),
235            WordPart::CmdSub(s) => {
236                let rendered = s.to_string();
237                if rendered.starts_with('(') { write!(f, "$( {rendered})") } else { write!(f, "$({rendered})") }
238            }
239            WordPart::ProcSub(s) => write!(f, "<({s})"),
240            WordPart::Backtick(s) => write!(f, "`{s}`"),
241            WordPart::Arith(w) => write!(f, "$(({w}))"),
242        }
243    }
244}
245
246impl fmt::Display for Redir {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self {
249            Redir::Write { fd, target, mode } => {
250                // `&>`/`&>>` name both streams by construction and take no descriptor prefix —
251                // `2&> f` is not a redirect, and rendering one would not re-parse.
252                let both = matches!(mode, WriteMode::TruncateBoth | WriteMode::AppendBoth);
253                if *fd != 1 && !both {
254                    write!(f, "{fd}")?;
255                }
256                let op = match mode {
257                    WriteMode::Truncate => ">",
258                    WriteMode::Append => ">>",
259                    WriteMode::Clobber => ">|",
260                    WriteMode::TruncateBoth => "&>",
261                    WriteMode::AppendBoth => "&>>",
262                };
263                write!(f, "{op} {target}")
264            }
265            Redir::Read { fd, target } => {
266                if *fd != 0 {
267                    write!(f, "{fd}")?;
268                }
269                write!(f, "< {target}")
270            }
271            Redir::ReadWrite { fd, target } => {
272                if *fd != 0 {
273                    write!(f, "{fd}")?;
274                }
275                write!(f, "<> {target}")
276            }
277            Redir::HereStr(w) => write!(f, "<<< {w}"),
278            Redir::HereDoc { delimiter, strip_tabs, .. } => {
279                if *strip_tabs {
280                    write!(f, "<<-{delimiter}")
281                } else {
282                    write!(f, "<<{delimiter}")
283                }
284            }
285            Redir::DupFd { src, dst } => {
286                if *src != 1 {
287                    write!(f, "{src}")?;
288                }
289                write!(f, ">&{dst}")
290            }
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use crate::cst::parse;
298
299    #[test]
300    fn display_simple() {
301        let s = parse("echo hello").unwrap();
302        assert_eq!(s.to_string(), "echo hello");
303    }
304
305    #[test]
306    fn display_pipeline() {
307        let s = parse("grep foo | head -5").unwrap();
308        assert_eq!(s.to_string(), "grep foo | head -5");
309    }
310
311    #[test]
312    fn display_sequence() {
313        let s = parse("ls && echo done").unwrap();
314        assert_eq!(s.to_string(), "ls && echo done");
315    }
316
317    #[test]
318    fn display_single_quoted() {
319        let s = parse("echo 'hello world'").unwrap();
320        assert_eq!(s.to_string(), "echo 'hello world'");
321    }
322
323    #[test]
324    fn display_double_quoted() {
325        let s = parse("echo \"hello world\"").unwrap();
326        assert_eq!(s.to_string(), "echo \"hello world\"");
327    }
328
329    #[test]
330    fn display_redirect() {
331        let s = parse("echo hello > /dev/null").unwrap();
332        assert_eq!(s.to_string(), "echo hello > /dev/null");
333    }
334
335    #[test]
336    fn display_fd_redirect() {
337        let s = parse("echo hello 2>&1").unwrap();
338        assert_eq!(s.to_string(), "echo hello 2>&1");
339    }
340
341    #[test]
342    fn display_cmd_sub() {
343        let s = parse("echo $(ls)").unwrap();
344        assert_eq!(s.to_string(), "echo $(ls)");
345    }
346
347    #[test]
348    fn display_for() {
349        let s = parse("for x in 1 2 3; do echo $x; done").unwrap();
350        assert_eq!(s.to_string(), "for x in 1 2 3; do echo $x; done");
351    }
352
353    #[test]
354    fn display_if() {
355        let s = parse("if true; then echo yes; else echo no; fi").unwrap();
356        assert_eq!(s.to_string(), "if true; then echo yes; else echo no; fi");
357    }
358
359    #[test]
360    fn display_for_with_redirect() {
361        let s = parse("for x in 1 2; do echo $x; done 2>/dev/null").unwrap();
362        assert_eq!(s.to_string(), "for x in 1 2; do echo $x; done 2> /dev/null");
363    }
364
365    #[test]
366    fn display_if_with_redirect() {
367        let s = parse("if true; then echo yes; fi 2>&1").unwrap();
368        assert_eq!(s.to_string(), "if true; then echo yes; fi 2>&1");
369    }
370
371    #[test]
372    fn display_env_prefix() {
373        let s = parse("FOO=bar ls").unwrap();
374        assert_eq!(s.to_string(), "FOO=bar ls");
375    }
376
377    #[test]
378    fn display_subshell() {
379        let s = parse("(echo hello)").unwrap();
380        assert_eq!(s.to_string(), "(echo hello)");
381    }
382
383    #[test]
384    fn display_negation() {
385        let s = parse("! echo hello").unwrap();
386        assert_eq!(s.to_string(), "! echo hello");
387    }
388}