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_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 { f.write_str(" ")?; }
196            first = false;
197            write!(f, "{name}={val}")?;
198        }
199        for w in &self.words {
200            if !first { f.write_str(" ")?; }
201            first = false;
202            write!(f, "{w}")?;
203        }
204        for r in &self.redirs {
205            if !first { f.write_str(" ")?; }
206            first = false;
207            write!(f, "{r}")?;
208        }
209        Ok(())
210    }
211}
212
213impl fmt::Display for Word {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        for part in &self.0 {
216            write!(f, "{part}")?;
217        }
218        Ok(())
219    }
220}
221
222impl fmt::Display for WordPart {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        match self {
225            WordPart::Lit(s) => f.write_str(s),
226            WordPart::Escape(c) => write!(f, "\\{c}"),
227            WordPart::SQuote(s) => write!(f, "'{s}'"),
228            WordPart::DQuote(w) => write!(f, "\"{w}\""),
229            WordPart::CmdSub(s) => {
230                let rendered = s.to_string();
231                if rendered.starts_with('(') {
232                    write!(f, "$( {rendered})")
233                } else {
234                    write!(f, "$({rendered})")
235                }
236            }
237            WordPart::ProcSub(s) => write!(f, "<({s})"),
238            WordPart::Backtick(s) => write!(f, "`{s}`"),
239            WordPart::Arith(w) => write!(f, "$(({w}))"),
240        }
241    }
242}
243
244impl fmt::Display for Redir {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        match self {
247            Redir::Write { fd, target, mode } => {
248                // `&>`/`&>>` name both streams by construction and take no descriptor prefix —
249                // `2&> f` is not a redirect, and rendering one would not re-parse.
250                let both = matches!(mode, WriteMode::TruncateBoth | WriteMode::AppendBoth);
251                if *fd != 1 && !both { write!(f, "{fd}")?; }
252                let op = match mode {
253                    WriteMode::Truncate => ">",
254                    WriteMode::Append => ">>",
255                    WriteMode::Clobber => ">|",
256                    WriteMode::TruncateBoth => "&>",
257                    WriteMode::AppendBoth => "&>>",
258                };
259                write!(f, "{op} {target}")
260            }
261            Redir::Read { fd, target } => {
262                if *fd != 0 { write!(f, "{fd}")?; }
263                write!(f, "< {target}")
264            }
265            Redir::ReadWrite { fd, target } => {
266                if *fd != 0 { write!(f, "{fd}")?; }
267                write!(f, "<> {target}")
268            }
269            Redir::HereStr(w) => write!(f, "<<< {w}"),
270            Redir::HereDoc { delimiter, strip_tabs, .. } => {
271                if *strip_tabs { write!(f, "<<-{delimiter}") } else { write!(f, "<<{delimiter}") }
272            }
273            Redir::DupFd { src, dst } => {
274                if *src != 1 { write!(f, "{src}")?; }
275                write!(f, ">&{dst}")
276            }
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use crate::cst::parse;
284
285    #[test]
286    fn display_simple() {
287        let s = parse("echo hello").unwrap();
288        assert_eq!(s.to_string(), "echo hello");
289    }
290
291    #[test]
292    fn display_pipeline() {
293        let s = parse("grep foo | head -5").unwrap();
294        assert_eq!(s.to_string(), "grep foo | head -5");
295    }
296
297    #[test]
298    fn display_sequence() {
299        let s = parse("ls && echo done").unwrap();
300        assert_eq!(s.to_string(), "ls && echo done");
301    }
302
303    #[test]
304    fn display_single_quoted() {
305        let s = parse("echo 'hello world'").unwrap();
306        assert_eq!(s.to_string(), "echo 'hello world'");
307    }
308
309    #[test]
310    fn display_double_quoted() {
311        let s = parse("echo \"hello world\"").unwrap();
312        assert_eq!(s.to_string(), "echo \"hello world\"");
313    }
314
315    #[test]
316    fn display_redirect() {
317        let s = parse("echo hello > /dev/null").unwrap();
318        assert_eq!(s.to_string(), "echo hello > /dev/null");
319    }
320
321    #[test]
322    fn display_fd_redirect() {
323        let s = parse("echo hello 2>&1").unwrap();
324        assert_eq!(s.to_string(), "echo hello 2>&1");
325    }
326
327    #[test]
328    fn display_cmd_sub() {
329        let s = parse("echo $(ls)").unwrap();
330        assert_eq!(s.to_string(), "echo $(ls)");
331    }
332
333    #[test]
334    fn display_for() {
335        let s = parse("for x in 1 2 3; do echo $x; done").unwrap();
336        assert_eq!(s.to_string(), "for x in 1 2 3; do echo $x; done");
337    }
338
339    #[test]
340    fn display_if() {
341        let s = parse("if true; then echo yes; else echo no; fi").unwrap();
342        assert_eq!(s.to_string(), "if true; then echo yes; else echo no; fi");
343    }
344
345    #[test]
346    fn display_for_with_redirect() {
347        let s = parse("for x in 1 2; do echo $x; done 2>/dev/null").unwrap();
348        assert_eq!(s.to_string(), "for x in 1 2; do echo $x; done 2> /dev/null");
349    }
350
351    #[test]
352    fn display_if_with_redirect() {
353        let s = parse("if true; then echo yes; fi 2>&1").unwrap();
354        assert_eq!(s.to_string(), "if true; then echo yes; fi 2>&1");
355    }
356
357    #[test]
358    fn display_env_prefix() {
359        let s = parse("FOO=bar ls").unwrap();
360        assert_eq!(s.to_string(), "FOO=bar ls");
361    }
362
363    #[test]
364    fn display_subshell() {
365        let s = parse("(echo hello)").unwrap();
366        assert_eq!(s.to_string(), "(echo hello)");
367    }
368
369    #[test]
370    fn display_negation() {
371        let s = parse("! echo hello").unwrap();
372        assert_eq!(s.to_string(), "! echo hello");
373    }
374}