1use super::*;
8
9pub fn format_program(program: &Program) -> String {
13 let stmts: Vec<_> = program
14 .statements
15 .iter()
16 .filter(|s| !matches!(s, Stmt::Empty))
17 .collect();
18
19 match stmts.len() {
20 0 => "(program)".to_string(),
21 1 => format_stmt(stmts[0]),
22 _ => {
23 let parts: Vec<String> = stmts.iter().map(|s| format_stmt(s)).collect();
24 format!("(program {})", parts.join(" "))
25 }
26 }
27}
28
29pub fn format_stmt(stmt: &Stmt) -> String {
31 match stmt {
32 Stmt::Assignment(a) => format_assignment(a),
33 Stmt::Command(cmd) => format_command(cmd),
34 Stmt::Pipeline(p) => format_pipeline(p),
35 Stmt::If(if_stmt) => format_if(if_stmt),
36 Stmt::For(for_loop) => format_for(for_loop),
37 Stmt::While(while_loop) => format_while(while_loop),
38 Stmt::Case(case_stmt) => format_case(case_stmt),
39 Stmt::Break(n) => match n {
40 Some(level) => format!("(break {})", level),
41 None => "(break)".to_string(),
42 },
43 Stmt::Continue(n) => match n {
44 Some(level) => format!("(continue {})", level),
45 None => "(continue)".to_string(),
46 },
47 Stmt::Return(expr) => match expr {
48 Some(e) => format!("(return {})", format_expr(e)),
49 None => "(return)".to_string(),
50 },
51 Stmt::Exit(expr) => match expr {
52 Some(e) => format!("(exit {})", format_expr(e)),
53 None => "(exit)".to_string(),
54 },
55 Stmt::ToolDef(tool) => format_tooldef(tool),
56 Stmt::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
57 Stmt::AndChain { left, right } => {
58 format!("(and-chain {} {})", format_stmt(left), format_stmt(right))
59 }
60 Stmt::OrChain { left, right } => {
61 format!("(or-chain {} {})", format_stmt(left), format_stmt(right))
62 }
63 Stmt::Empty => "(empty)".to_string(),
64 }
65}
66
67fn format_assignment(a: &Assignment) -> String {
69 let value = format_expr(&a.value);
70 format!("(assign {} {} local={})", a.name, value, a.local)
71}
72
73pub fn format_command(cmd: &Command) -> String {
75 let mut parts = vec![format!("(cmd {}", cmd.name)];
76
77 for arg in &cmd.args {
78 parts.push(format_arg(arg));
79 }
80
81 for redir in &cmd.redirects {
82 parts.push(format_redirect(redir));
83 }
84
85 format!("{})", parts.join(" "))
86}
87
88fn format_arg(arg: &Arg) -> String {
90 match arg {
91 Arg::Positional(expr) => format!("(pos {})", format_expr(expr)),
92 Arg::Named { key, value } => format!("(named {} {})", key, format_expr(value)),
93 Arg::WordAssign { key, value } => format!("(wordassign {} {})", key, format_expr(value)),
94 Arg::ShortFlag(f) => format!("(shortflag {})", f),
95 Arg::LongFlag(f) => format!("(longflag {})", f),
96 Arg::DoubleDash => "(doubledash)".to_string(),
97 }
98}
99
100fn format_redirect(redir: &Redirect) -> String {
102 let kind = match redir.kind {
103 RedirectKind::StdoutOverwrite => ">",
104 RedirectKind::StdoutAppend => ">>",
105 RedirectKind::Stdin => "<",
106 RedirectKind::HereDoc => "<<",
107 RedirectKind::HereString => "<<<",
108 RedirectKind::Stderr => "2>",
109 RedirectKind::Both => "&>",
110 RedirectKind::MergeStderr => "2>&1",
111 RedirectKind::MergeStdout => "1>&2",
112 };
113 format!("(redir {} {})", kind, format_expr(&redir.target))
114}
115
116pub fn format_pipeline(p: &Pipeline) -> String {
118 let cmds: Vec<String> = p.commands.iter().map(format_command).collect();
119
120 if p.background {
121 if cmds.len() == 1 {
122 format!("(background {})", cmds[0])
123 } else {
124 format!("(background (pipeline {}))", cmds.join(" "))
125 }
126 } else {
127 format!("(pipeline {})", cmds.join(" "))
128 }
129}
130
131fn format_if(if_stmt: &IfStmt) -> String {
133 let cond = format_expr(&if_stmt.condition);
134 let then_stmts: Vec<String> = if_stmt
135 .then_branch
136 .iter()
137 .filter(|s| !matches!(s, Stmt::Empty))
138 .map(format_stmt)
139 .collect();
140 let then_part = format!("(then {})", then_stmts.join(" "));
141
142 match &if_stmt.else_branch {
143 Some(else_stmts) => {
144 let else_inner: Vec<String> = else_stmts
145 .iter()
146 .filter(|s| !matches!(s, Stmt::Empty))
147 .map(format_stmt)
148 .collect();
149 if else_inner.is_empty() {
150 format!("(if {} {} (else))", cond, then_part)
151 } else {
152 format!("(if {} {} (else {}))", cond, then_part, else_inner.join(" "))
153 }
154 }
155 None => format!("(if {} {} (else))", cond, then_part),
156 }
157}
158
159fn format_for(for_loop: &ForLoop) -> String {
161 let items: Vec<String> = for_loop.items.iter().map(format_expr).collect();
162 let body_stmts: Vec<String> = for_loop
163 .body
164 .iter()
165 .filter(|s| !matches!(s, Stmt::Empty))
166 .map(format_stmt)
167 .collect();
168 format!(
169 "(for {} (in {}) (do {}))",
170 for_loop.variable,
171 items.join(" "),
172 body_stmts.join(" ")
173 )
174}
175
176fn format_while(while_loop: &WhileLoop) -> String {
178 let cond = format_expr(&while_loop.condition);
179 let body_stmts: Vec<String> = while_loop
180 .body
181 .iter()
182 .filter(|s| !matches!(s, Stmt::Empty))
183 .map(format_stmt)
184 .collect();
185 format!("(while {} (do {}))", cond, body_stmts.join(" "))
186}
187
188fn format_case(case_stmt: &CaseStmt) -> String {
190 let expr = format_expr(&case_stmt.expr);
191 let branches: Vec<String> = case_stmt
192 .branches
193 .iter()
194 .map(format_case_branch)
195 .collect();
196 format!("(case {} ({}))", expr, branches.join(" "))
197}
198
199fn format_case_branch(branch: &CaseBranch) -> String {
201 let patterns = branch.patterns.join("|");
202 let body_stmts: Vec<String> = branch
203 .body
204 .iter()
205 .filter(|s| !matches!(s, Stmt::Empty))
206 .map(format_stmt)
207 .collect();
208 format!("(branch \"{}\" ({}))", patterns, body_stmts.join(" "))
209}
210
211fn format_tooldef(tool: &ToolDef) -> String {
213 let params: Vec<String> = tool.params.iter().map(format_param).collect();
214 let body_stmts: Vec<String> = tool
215 .body
216 .iter()
217 .filter(|s| !matches!(s, Stmt::Empty))
218 .map(format_stmt)
219 .collect();
220 format!(
221 "(tooldef {} ({}) ({}))",
222 tool.name,
223 params.join(" "),
224 body_stmts.join(" ")
225 )
226}
227
228fn format_param(param: &ParamDef) -> String {
230 let type_str = param
231 .param_type
232 .as_ref()
233 .map(|t| match t {
234 ParamType::String => "string",
235 ParamType::Int => "int",
236 ParamType::Float => "float",
237 ParamType::Bool => "bool",
238 })
239 .unwrap_or("any");
240
241 match ¶m.default {
242 Some(default) => format!("(param {} {} {})", param.name, type_str, format_expr(default)),
243 None => format!("(param {} {})", param.name, type_str),
244 }
245}
246
247pub fn format_expr(expr: &Expr) -> String {
249 match expr {
250 Expr::Literal(value) => format_value(value),
251 Expr::VarRef(path) => format!("(varref {})", format_varpath(path)),
252 Expr::Interpolated(parts) => {
253 let parts_str: Vec<String> = parts
254 .iter()
255 .map(format_string_part)
256 .collect();
257 format!("(interpolated {})", parts_str.join(" "))
258 }
259 Expr::HereDocBody { parts, strip_tabs } => {
260 let parts_str: Vec<String> = parts
261 .iter()
262 .map(|sp| format_string_part(&sp.part))
263 .collect();
264 format!(
265 "(heredoc-body strip-tabs={} {})",
266 strip_tabs,
267 parts_str.join(" ")
268 )
269 }
270 Expr::BinaryOp { left, op, right } => {
271 let op_str = match op {
272 BinaryOp::And => "and",
273 BinaryOp::Or => "or",
274 };
275 format!("({} {} {})", op_str, format_expr(left), format_expr(right))
276 }
277 Expr::CommandSubst(pipeline) => {
278 format!("(cmdsubst {})", format_pipeline(pipeline))
279 }
280 Expr::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
281 Expr::Positional(n) => format!("(positional {})", n),
282 Expr::AllArgs => "(all-args)".to_string(),
283 Expr::ArgCount => "(arg-count)".to_string(),
284 Expr::VarLength(name) => format!("(var-length {})", name),
285 Expr::VarWithDefault { name, default } => {
286 let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
287 format!("(var-default {} ({}))", name, default_parts.join(" "))
288 }
289 Expr::Arithmetic(expr_str) => format!("(arithmetic \"{}\")", expr_str),
290 Expr::Command(cmd) => format_command(cmd),
291 Expr::LastExitCode => "(last-exit-code)".to_string(),
292 Expr::CurrentPid => "(current-pid)".to_string(),
293 Expr::GlobPattern(s) => format!("(glob \"{}\")", s),
294 }
295}
296
297pub fn format_test_expr(test: &TestExpr) -> String {
299 match test {
300 TestExpr::FileTest { op, path } => {
301 let op_str = match op {
302 FileTestOp::Exists => "-e",
303 FileTestOp::IsFile => "-f",
304 FileTestOp::IsDir => "-d",
305 FileTestOp::Readable => "-r",
306 FileTestOp::Writable => "-w",
307 FileTestOp::Executable => "-x",
308 };
309 format!("(file {} {})", op_str, format_expr(path))
310 }
311 TestExpr::StringTest { op, value } => {
312 let op_str = match op {
313 StringTestOp::IsEmpty => "-z",
314 StringTestOp::IsNonEmpty => "-n",
315 };
316 format!("(string {} {})", op_str, format_expr(value))
317 }
318 TestExpr::Comparison { left, op, right } => {
319 let op_str = match op {
320 TestCmpOp::Eq => "==",
321 TestCmpOp::NotEq => "!=",
322 TestCmpOp::Match => "=~",
323 TestCmpOp::NotMatch => "!~",
324 TestCmpOp::Gt => ">",
325 TestCmpOp::Lt => "<",
326 TestCmpOp::GtEq => ">=",
327 TestCmpOp::LtEq => "<=",
328 TestCmpOp::NumEq => "-eq",
329 TestCmpOp::NumNotEq => "-ne",
330 TestCmpOp::NumGt => "-gt",
331 TestCmpOp::NumLt => "-lt",
332 TestCmpOp::NumGtEq => "-ge",
333 TestCmpOp::NumLtEq => "-le",
334 };
335 format!(
336 "(cmp {} {} {})",
337 op_str,
338 format_expr(left),
339 format_expr(right)
340 )
341 }
342 TestExpr::And { left, right } => {
343 format!("(and {} {})", format_test_expr(left), format_test_expr(right))
344 }
345 TestExpr::Or { left, right } => {
346 format!("(or {} {})", format_test_expr(left), format_test_expr(right))
347 }
348 TestExpr::Not { expr } => {
349 format!("(not {})", format_test_expr(expr))
350 }
351 }
352}
353
354fn format_string_part(part: &StringPart) -> String {
356 match part {
357 StringPart::Literal(s) => format!("\"{}\"", escape_for_display(s)),
358 StringPart::Var(path) => format!("(varref {})", format_varpath(path)),
359 StringPart::VarWithDefault { name, default } => {
360 let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
361 format!("(vardefault {} ({}))", name, default_parts.join(" "))
362 }
363 StringPart::VarLength(name) => format!("(varlength {})", name),
364 StringPart::Positional(n) => format!("(positional {})", n),
365 StringPart::AllArgs => "(allargs)".to_string(),
366 StringPart::ArgCount => "(argcount)".to_string(),
367 StringPart::Arithmetic(expr) => format!("(arith \"{}\")", expr),
368 StringPart::CommandSubst(pipeline) => format!("(cmdsubst {})", format_pipeline(pipeline)),
369 StringPart::LastExitCode => "(last-exit-code)".to_string(),
370 StringPart::CurrentPid => "(current-pid)".to_string(),
371 }
372}
373
374fn escape_for_display(s: &str) -> String {
376 s.replace('\n', "\\n")
377 .replace('\t', "\\t")
378 .replace('\r', "\\r")
379}
380
381pub fn format_value(value: &Value) -> String {
383 match value {
384 Value::Null => "(null)".to_string(),
385 Value::Bool(b) => format!("(bool {})", b),
386 Value::Int(n) => format!("(int {})", n),
387 Value::Float(f) => format!("(float {})", f),
388 Value::String(s) => format!("(string \"{}\")", escape_for_display(s)),
389 Value::Json(json) => format!("(json {})", json),
390 Value::Blob(blob) => format!("(blob id={} size={} type={})", blob.id, blob.size, blob.content_type),
391 }
392}
393
394pub fn format_varpath(path: &VarPath) -> String {
396 path.segments
397 .iter()
398 .map(|seg| match seg {
399 VarSegment::Field(name) => name.clone(),
400 })
401 .collect::<Vec<_>>()
402 .join(".")
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn format_simple_int() {
411 assert_eq!(format_value(&Value::Int(42)), "(int 42)");
412 }
413
414 #[test]
415 fn format_simple_string() {
416 assert_eq!(format_value(&Value::String("hello".to_string())), "(string \"hello\")");
417 }
418
419 #[test]
420 fn format_varpath_simple() {
421 let path = VarPath::simple("X");
422 assert_eq!(format_varpath(&path), "X");
423 }
424
425 #[test]
426 fn format_varpath_nested() {
427 let path = VarPath {
428 segments: vec![
429 VarSegment::Field("VAR".to_string()),
430 VarSegment::Field("field".to_string()),
431 ],
432 };
433 assert_eq!(format_varpath(&path), "VAR.field");
434 }
435}