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}