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