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