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::EnvScoped { assignments, body } => {
64 let assigns: Vec<String> = assignments.iter().map(format_assignment).collect();
65 format!("(env-scoped ({}) {})", assigns.join(" "), format_stmt(body))
66 }
67 Stmt::Empty => "(empty)".to_string(),
68 }
69}
70
71fn format_assignment(a: &Assignment) -> String {
73 let value = format_expr(&a.value);
74 format!("(assign {} {} local={})", format_varpath(&a.path), value, a.local)
75}
76
77pub fn format_command(cmd: &Command) -> String {
79 let mut parts = vec![format!("(cmd {}", cmd.name)];
80
81 for arg in &cmd.args {
82 parts.push(format_arg(arg));
83 }
84
85 for redir in &cmd.redirects {
86 parts.push(format_redirect(redir));
87 }
88
89 format!("{})", parts.join(" "))
90}
91
92fn format_arg(arg: &Arg) -> String {
94 match arg {
95 Arg::Positional(expr) => format!("(pos {})", format_expr(expr)),
96 Arg::Named { key, value } => format!("(named {} {})", key, format_expr(value)),
97 Arg::WordAssign { key, value } => format!("(wordassign {} {})", key, format_expr(value)),
98 Arg::ShortFlag(f) => format!("(shortflag {})", f),
99 Arg::LongFlag(f) => format!("(longflag {})", f),
100 Arg::DoubleDash => "(doubledash)".to_string(),
101 }
102}
103
104fn format_redirect(redir: &Redirect) -> String {
106 let kind = match redir.kind {
107 RedirectKind::StdoutOverwrite => ">",
108 RedirectKind::StdoutAppend => ">>",
109 RedirectKind::Stdin => "<",
110 RedirectKind::HereDoc(_) => "<<",
111 RedirectKind::HereString => "<<<",
112 RedirectKind::Stderr => "2>",
113 RedirectKind::Both => "&>",
114 RedirectKind::MergeStderr => "2>&1",
115 RedirectKind::MergeStdout => "1>&2",
116 };
117 format!("(redir {} {})", kind, format_expr(&redir.target))
118}
119
120pub fn format_stmt_block(stmts: &[Stmt]) -> String {
125 if stmts.len() == 1 {
126 format_stmt(&stmts[0])
127 } else {
128 let inner: Vec<String> = stmts.iter().map(format_stmt).collect();
129 format!("(block {})", inner.join(" "))
130 }
131}
132
133pub fn format_pipeline(p: &Pipeline) -> String {
134 let cmds: Vec<String> = p
135 .stages
136 .iter()
137 .map(|stage| match stage {
138 PipelineStage::Command(cmd) => format_command(cmd),
139 PipelineStage::Compound(stmt) => format_stmt(stmt),
140 })
141 .collect();
142
143 if p.background {
144 if cmds.len() == 1 {
145 format!("(background {})", cmds[0])
146 } else {
147 format!("(background (pipeline {}))", cmds.join(" "))
148 }
149 } else {
150 format!("(pipeline {})", cmds.join(" "))
151 }
152}
153
154fn format_if(if_stmt: &IfStmt) -> String {
156 let cond = format_expr(&if_stmt.condition);
157 let then_stmts: Vec<String> = if_stmt
158 .then_branch
159 .iter()
160 .filter(|s| !matches!(s, Stmt::Empty))
161 .map(format_stmt)
162 .collect();
163 let then_part = format!("(then {})", then_stmts.join(" "));
164
165 match &if_stmt.else_branch {
166 Some(else_stmts) => {
167 let else_inner: Vec<String> = else_stmts
168 .iter()
169 .filter(|s| !matches!(s, Stmt::Empty))
170 .map(format_stmt)
171 .collect();
172 if else_inner.is_empty() {
173 format!("(if {} {} (else))", cond, then_part)
174 } else {
175 format!("(if {} {} (else {}))", cond, then_part, else_inner.join(" "))
176 }
177 }
178 None => format!("(if {} {} (else))", cond, then_part),
179 }
180}
181
182fn format_for(for_loop: &ForLoop) -> String {
184 let items: Vec<String> = for_loop.items.iter().map(format_expr).collect();
185 let body_stmts: Vec<String> = for_loop
186 .body
187 .iter()
188 .filter(|s| !matches!(s, Stmt::Empty))
189 .map(format_stmt)
190 .collect();
191 format!(
192 "(for {} (in {}) (do {}))",
193 for_loop.variable,
194 items.join(" "),
195 body_stmts.join(" ")
196 )
197}
198
199fn format_while(while_loop: &WhileLoop) -> String {
201 let cond = format_expr(&while_loop.condition);
202 let body_stmts: Vec<String> = while_loop
203 .body
204 .iter()
205 .filter(|s| !matches!(s, Stmt::Empty))
206 .map(format_stmt)
207 .collect();
208 format!("(while {} (do {}))", cond, body_stmts.join(" "))
209}
210
211fn format_case(case_stmt: &CaseStmt) -> String {
213 let expr = format_expr(&case_stmt.expr);
214 let branches: Vec<String> = case_stmt
215 .branches
216 .iter()
217 .map(format_case_branch)
218 .collect();
219 format!("(case {} ({}))", expr, branches.join(" "))
220}
221
222fn format_case_branch(branch: &CaseBranch) -> String {
224 let patterns = branch.patterns.join("|");
225 let body_stmts: Vec<String> = branch
226 .body
227 .iter()
228 .filter(|s| !matches!(s, Stmt::Empty))
229 .map(format_stmt)
230 .collect();
231 format!("(branch \"{}\" ({}))", patterns, body_stmts.join(" "))
232}
233
234fn format_tooldef(tool: &ToolDef) -> String {
236 let params: Vec<String> = tool.params.iter().map(format_param).collect();
237 let body_stmts: Vec<String> = tool
238 .body
239 .iter()
240 .filter(|s| !matches!(s, Stmt::Empty))
241 .map(format_stmt)
242 .collect();
243 format!(
244 "(tooldef {} ({}) ({}))",
245 tool.name,
246 params.join(" "),
247 body_stmts.join(" ")
248 )
249}
250
251fn format_param(param: &ParamDef) -> String {
253 let type_str = param
254 .param_type
255 .as_ref()
256 .map(|t| match t {
257 ParamType::String => "string",
258 ParamType::Int => "int",
259 ParamType::Float => "float",
260 ParamType::Bool => "bool",
261 })
262 .unwrap_or("any");
263
264 match ¶m.default {
265 Some(default) => format!("(param {} {} {})", param.name, type_str, format_expr(default)),
266 None => format!("(param {} {})", param.name, type_str),
267 }
268}
269
270pub fn format_expr(expr: &Expr) -> String {
272 match expr {
273 Expr::Not(inner) => format!("(not {})", format_expr(inner)),
274 Expr::Literal(value) => format_value(value),
275 Expr::VarRef(path) => format!("(varref {})", format_varpath(path)),
276 Expr::Interpolated(parts) => {
277 let parts_str: Vec<String> = parts
278 .iter()
279 .map(format_string_part)
280 .collect();
281 format!("(interpolated {})", parts_str.join(" "))
282 }
283 Expr::HereDocBody { parts, strip_tabs } => {
284 let parts_str: Vec<String> = parts
285 .iter()
286 .map(|sp| format_string_part(&sp.part))
287 .collect();
288 format!(
289 "(heredoc-body strip-tabs={} {})",
290 strip_tabs,
291 parts_str.join(" ")
292 )
293 }
294 Expr::BinaryOp { left, op, right } => {
295 let op_str = match op {
296 BinaryOp::And => "and",
297 BinaryOp::Or => "or",
298 };
299 format!("({} {} {})", op_str, format_expr(left), format_expr(right))
300 }
301 Expr::CommandSubst(stmts) => {
302 format!("(cmdsubst {})", format_stmt_block(stmts))
303 }
304 Expr::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
305 Expr::Positional(n) => format!("(positional {})", n),
306 Expr::AllArgs => "(all-args)".to_string(),
307 Expr::ArgCount => "(arg-count)".to_string(),
308 Expr::VarLength(path) => format!("(var-length {})", format_varpath(path)),
309 Expr::VarWithDefault { path, default } => {
310 let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
311 format!("(var-default {} ({}))", format_varpath(path), default_parts.join(" "))
312 }
313 Expr::Arithmetic(expr_str) => format!("(arithmetic \"{}\")", expr_str),
314 Expr::Command(cmd) => format_command(cmd),
315 Expr::LastExitCode => "(last-exit-code)".to_string(),
316 Expr::CurrentPid => "(current-pid)".to_string(),
317 Expr::GlobPattern(s) => format!("(glob \"{}\")", s),
318 Expr::ListLiteral(elems) => {
319 let parts: Vec<String> = elems
320 .iter()
321 .map(|elem| match elem {
322 ListElem::Item(e) => format_expr(e),
323 ListElem::Spread(e) => format!("(spread {})", format_expr(e)),
324 })
325 .collect();
326 format!("(list {})", parts.join(" "))
327 }
328 Expr::RecordLiteral(entries) => {
329 let parts: Vec<String> = entries
330 .iter()
331 .map(|entry| {
332 let key = match &entry.key {
333 RecordKey::Bare(s) => s.clone(),
334 RecordKey::Quoted(s) => format!("\"{}\"", s),
335 RecordKey::Interpolated(parts) => {
336 let parts_str: Vec<String> =
337 parts.iter().map(format_string_part).collect();
338 format!("(interpolated {})", parts_str.join(" "))
339 }
340 };
341 format!("({} {})", key, format_expr(&entry.value))
342 })
343 .collect();
344 format!("(record {})", parts.join(" "))
345 }
346 }
347}
348
349pub fn format_test_expr(test: &TestExpr) -> String {
351 match test {
352 TestExpr::FileTest { op, path } => {
353 let op_str = match op {
354 FileTestOp::Exists => "-e",
355 FileTestOp::IsFile => "-f",
356 FileTestOp::IsDir => "-d",
357 FileTestOp::Readable => "-r",
358 FileTestOp::Writable => "-w",
359 FileTestOp::Executable => "-x",
360 };
361 format!("(file {} {})", op_str, format_expr(path))
362 }
363 TestExpr::StringTest { op, value } => {
364 let op_str = match op {
365 StringTestOp::IsEmpty => "-z",
366 StringTestOp::IsNonEmpty => "-n",
367 StringTestOp::IsList => "-list",
368 StringTestOp::IsRecord => "-record",
369 };
370 format!("(string {} {})", op_str, format_expr(value))
371 }
372 TestExpr::Comparison { left, op, right } => {
373 let op_str = match op {
374 TestCmpOp::Eq => "==",
375 TestCmpOp::NotEq => "!=",
376 TestCmpOp::Match => "=~",
377 TestCmpOp::NotMatch => "!~",
378 TestCmpOp::Gt => ">",
379 TestCmpOp::Lt => "<",
380 TestCmpOp::GtEq => ">=",
381 TestCmpOp::LtEq => "<=",
382 TestCmpOp::NumEq => "-eq",
383 TestCmpOp::NumNotEq => "-ne",
384 TestCmpOp::NumGt => "-gt",
385 TestCmpOp::NumLt => "-lt",
386 TestCmpOp::NumGtEq => "-ge",
387 TestCmpOp::NumLtEq => "-le",
388 };
389 format!(
390 "(cmp {} {} {})",
391 op_str,
392 format_expr(left),
393 format_expr(right)
394 )
395 }
396 TestExpr::And { left, right } => {
397 format!("(and {} {})", format_test_expr(left), format_test_expr(right))
398 }
399 TestExpr::Or { left, right } => {
400 format!("(or {} {})", format_test_expr(left), format_test_expr(right))
401 }
402 TestExpr::Not { expr } => {
403 format!("(not {})", format_test_expr(expr))
404 }
405 TestExpr::In { left, right } => {
406 format!("(in {} {})", format_expr(left), format_expr(right))
407 }
408 TestExpr::NotIn { left, right } => {
409 format!("(not-in {} {})", format_expr(left), format_expr(right))
410 }
411 }
412}
413
414fn format_string_part(part: &StringPart) -> String {
416 match part {
417 StringPart::Literal(s) => format!("\"{}\"", escape_for_display(s)),
418 StringPart::Var(path) => format!("(varref {})", format_varpath(path)),
419 StringPart::VarWithDefault { path, default } => {
420 let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
421 format!("(vardefault {} ({}))", format_varpath(path), default_parts.join(" "))
422 }
423 StringPart::VarLength(path) => format!("(varlength {})", format_varpath(path)),
424 StringPart::Positional(n) => format!("(positional {})", n),
425 StringPart::AllArgs => "(allargs)".to_string(),
426 StringPart::ArgCount => "(argcount)".to_string(),
427 StringPart::Arithmetic(expr) => format!("(arith \"{}\")", expr),
428 StringPart::CommandSubst(stmts) => format!("(cmdsubst {})", format_stmt_block(stmts)),
429 StringPart::LastExitCode => "(last-exit-code)".to_string(),
430 StringPart::CurrentPid => "(current-pid)".to_string(),
431 }
432}
433
434fn escape_for_display(s: &str) -> String {
436 s.replace('\n', "\\n")
437 .replace('\t', "\\t")
438 .replace('\r', "\\r")
439}
440
441pub fn format_value(value: &Value) -> String {
443 match value {
444 Value::Null => "(null)".to_string(),
445 Value::Bool(b) => format!("(bool {})", b),
446 Value::Int(n) => format!("(int {})", n),
447 Value::Float(f) => format!("(float {})", f),
448 Value::String(s) => format!("(string \"{}\")", escape_for_display(s)),
449 Value::Json(json) => format!("(json {})", json),
450 Value::Bytes(b) => format!("(bytes len={})", b.len()),
451 }
452}
453
454pub fn format_varpath(path: &VarPath) -> String {
456 path.segments
457 .iter()
458 .map(|seg| match seg {
459 VarSegment::Field(name) => name.clone(),
460 VarSegment::Index(i) => format!("[{i}]"),
461 VarSegment::Key(k) => format!("[{k}]"),
462 VarSegment::Dynamic(v) => format!("[${v}]"),
463 VarSegment::Slice(a, b) => format!(
464 "[{}:{}]",
465 a.map(|n| n.to_string()).unwrap_or_default(),
466 b.map(|n| n.to_string()).unwrap_or_default()
467 ),
468 })
469 .collect::<Vec<_>>()
470 .join(".")
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn format_simple_int() {
479 assert_eq!(format_value(&Value::Int(42)), "(int 42)");
480 }
481
482 #[test]
483 fn format_simple_string() {
484 assert_eq!(format_value(&Value::String("hello".to_string())), "(string \"hello\")");
485 }
486
487 #[test]
488 fn format_varpath_simple() {
489 let path = VarPath::simple("X");
490 assert_eq!(format_varpath(&path), "X");
491 }
492
493 #[test]
494 fn format_varpath_nested() {
495 let path = VarPath {
496 segments: vec![
497 VarSegment::Field("VAR".to_string()),
498 VarSegment::Field("field".to_string()),
499 ],
500 };
501 assert_eq!(format_varpath(&path), "VAR.field");
502 }
503}