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 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 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 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}