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::ShortFlag(f) => format!("(shortflag {})", f),
94 Arg::LongFlag(f) => format!("(longflag {})", f),
95 Arg::DoubleDash => "(doubledash)".to_string(),
96 }
97}
98
99fn format_redirect(redir: &Redirect) -> String {
101 let kind = match redir.kind {
102 RedirectKind::StdoutOverwrite => ">",
103 RedirectKind::StdoutAppend => ">>",
104 RedirectKind::Stdin => "<",
105 RedirectKind::HereDoc => "<<",
106 RedirectKind::Stderr => "2>",
107 RedirectKind::Both => "&>",
108 RedirectKind::MergeStderr => "2>&1",
109 RedirectKind::MergeStdout => "1>&2",
110 };
111 format!("(redir {} {})", kind, format_expr(&redir.target))
112}
113
114pub fn format_pipeline(p: &Pipeline) -> String {
116 let cmds: Vec<String> = p.commands.iter().map(format_command).collect();
117
118 if p.background {
119 if cmds.len() == 1 {
120 format!("(background {})", cmds[0])
121 } else {
122 format!("(background (pipeline {}))", cmds.join(" "))
123 }
124 } else {
125 format!("(pipeline {})", cmds.join(" "))
126 }
127}
128
129fn format_if(if_stmt: &IfStmt) -> String {
131 let cond = format_expr(&if_stmt.condition);
132 let then_stmts: Vec<String> = if_stmt
133 .then_branch
134 .iter()
135 .filter(|s| !matches!(s, Stmt::Empty))
136 .map(format_stmt)
137 .collect();
138 let then_part = format!("(then {})", then_stmts.join(" "));
139
140 match &if_stmt.else_branch {
141 Some(else_stmts) => {
142 let else_inner: Vec<String> = else_stmts
143 .iter()
144 .filter(|s| !matches!(s, Stmt::Empty))
145 .map(format_stmt)
146 .collect();
147 if else_inner.is_empty() {
148 format!("(if {} {} (else))", cond, then_part)
149 } else {
150 format!("(if {} {} (else {}))", cond, then_part, else_inner.join(" "))
151 }
152 }
153 None => format!("(if {} {} (else))", cond, then_part),
154 }
155}
156
157fn format_for(for_loop: &ForLoop) -> String {
159 let items: Vec<String> = for_loop.items.iter().map(format_expr).collect();
160 let body_stmts: Vec<String> = for_loop
161 .body
162 .iter()
163 .filter(|s| !matches!(s, Stmt::Empty))
164 .map(format_stmt)
165 .collect();
166 format!(
167 "(for {} (in {}) (do {}))",
168 for_loop.variable,
169 items.join(" "),
170 body_stmts.join(" ")
171 )
172}
173
174fn format_while(while_loop: &WhileLoop) -> String {
176 let cond = format_expr(&while_loop.condition);
177 let body_stmts: Vec<String> = while_loop
178 .body
179 .iter()
180 .filter(|s| !matches!(s, Stmt::Empty))
181 .map(format_stmt)
182 .collect();
183 format!("(while {} (do {}))", cond, body_stmts.join(" "))
184}
185
186fn format_case(case_stmt: &CaseStmt) -> String {
188 let expr = format_expr(&case_stmt.expr);
189 let branches: Vec<String> = case_stmt
190 .branches
191 .iter()
192 .map(format_case_branch)
193 .collect();
194 format!("(case {} ({}))", expr, branches.join(" "))
195}
196
197fn format_case_branch(branch: &CaseBranch) -> String {
199 let patterns = branch.patterns.join("|");
200 let body_stmts: Vec<String> = branch
201 .body
202 .iter()
203 .filter(|s| !matches!(s, Stmt::Empty))
204 .map(format_stmt)
205 .collect();
206 format!("(branch \"{}\" ({}))", patterns, body_stmts.join(" "))
207}
208
209fn format_tooldef(tool: &ToolDef) -> String {
211 let params: Vec<String> = tool.params.iter().map(format_param).collect();
212 let body_stmts: Vec<String> = tool
213 .body
214 .iter()
215 .filter(|s| !matches!(s, Stmt::Empty))
216 .map(format_stmt)
217 .collect();
218 format!(
219 "(tooldef {} ({}) ({}))",
220 tool.name,
221 params.join(" "),
222 body_stmts.join(" ")
223 )
224}
225
226fn format_param(param: &ParamDef) -> String {
228 let type_str = param
229 .param_type
230 .as_ref()
231 .map(|t| match t {
232 ParamType::String => "string",
233 ParamType::Int => "int",
234 ParamType::Float => "float",
235 ParamType::Bool => "bool",
236 })
237 .unwrap_or("any");
238
239 match ¶m.default {
240 Some(default) => format!("(param {} {} {})", param.name, type_str, format_expr(default)),
241 None => format!("(param {} {})", param.name, type_str),
242 }
243}
244
245pub fn format_expr(expr: &Expr) -> String {
247 match expr {
248 Expr::Literal(value) => format_value(value),
249 Expr::VarRef(path) => format!("(varref {})", format_varpath(path)),
250 Expr::Interpolated(parts) => {
251 let parts_str: Vec<String> = parts
252 .iter()
253 .map(format_string_part)
254 .collect();
255 format!("(interpolated {})", parts_str.join(" "))
256 }
257 Expr::BinaryOp { left, op, right } => {
258 let op_str = match op {
259 BinaryOp::And => "and",
260 BinaryOp::Or => "or",
261 BinaryOp::Eq => "eq",
262 BinaryOp::NotEq => "neq",
263 BinaryOp::Match => "match",
264 BinaryOp::NotMatch => "not-match",
265 BinaryOp::Lt => "<",
266 BinaryOp::Gt => ">",
267 BinaryOp::LtEq => "<=",
268 BinaryOp::GtEq => ">=",
269 };
270 format!("({} {} {})", op_str, format_expr(left), format_expr(right))
271 }
272 Expr::CommandSubst(pipeline) => {
273 format!("(cmdsubst {})", format_pipeline(pipeline))
274 }
275 Expr::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
276 Expr::Positional(n) => format!("(positional {})", n),
277 Expr::AllArgs => "(all-args)".to_string(),
278 Expr::ArgCount => "(arg-count)".to_string(),
279 Expr::VarLength(name) => format!("(var-length {})", name),
280 Expr::VarWithDefault { name, default } => {
281 let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
282 format!("(var-default {} ({}))", name, default_parts.join(" "))
283 }
284 Expr::Arithmetic(expr_str) => format!("(arithmetic \"{}\")", expr_str),
285 Expr::Command(cmd) => format_command(cmd),
286 Expr::LastExitCode => "(last-exit-code)".to_string(),
287 Expr::CurrentPid => "(current-pid)".to_string(),
288 }
289}
290
291pub fn format_test_expr(test: &TestExpr) -> String {
293 match test {
294 TestExpr::FileTest { op, path } => {
295 let op_str = match op {
296 FileTestOp::Exists => "-e",
297 FileTestOp::IsFile => "-f",
298 FileTestOp::IsDir => "-d",
299 FileTestOp::Readable => "-r",
300 FileTestOp::Writable => "-w",
301 FileTestOp::Executable => "-x",
302 };
303 format!("(file {} {})", op_str, format_expr(path))
304 }
305 TestExpr::StringTest { op, value } => {
306 let op_str = match op {
307 StringTestOp::IsEmpty => "-z",
308 StringTestOp::IsNonEmpty => "-n",
309 };
310 format!("(string {} {})", op_str, format_expr(value))
311 }
312 TestExpr::Comparison { left, op, right } => {
313 let op_str = match op {
314 TestCmpOp::Eq => "==",
315 TestCmpOp::NotEq => "!=",
316 TestCmpOp::Match => "=~",
317 TestCmpOp::NotMatch => "!~",
318 TestCmpOp::Gt => "-gt",
319 TestCmpOp::Lt => "-lt",
320 TestCmpOp::GtEq => "-ge",
321 TestCmpOp::LtEq => "-le",
322 };
323 format!(
324 "(cmp {} {} {})",
325 op_str,
326 format_expr(left),
327 format_expr(right)
328 )
329 }
330 TestExpr::And { left, right } => {
331 format!("(and {} {})", format_test_expr(left), format_test_expr(right))
332 }
333 TestExpr::Or { left, right } => {
334 format!("(or {} {})", format_test_expr(left), format_test_expr(right))
335 }
336 TestExpr::Not { expr } => {
337 format!("(not {})", format_test_expr(expr))
338 }
339 }
340}
341
342fn format_string_part(part: &StringPart) -> String {
344 match part {
345 StringPart::Literal(s) => format!("\"{}\"", escape_for_display(s)),
346 StringPart::Var(path) => format!("(varref {})", format_varpath(path)),
347 StringPart::VarWithDefault { name, default } => {
348 let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
349 format!("(vardefault {} ({}))", name, default_parts.join(" "))
350 }
351 StringPart::VarLength(name) => format!("(varlength {})", name),
352 StringPart::Positional(n) => format!("(positional {})", n),
353 StringPart::AllArgs => "(allargs)".to_string(),
354 StringPart::ArgCount => "(argcount)".to_string(),
355 StringPart::Arithmetic(expr) => format!("(arith \"{}\")", expr),
356 StringPart::CommandSubst(pipeline) => format!("(cmdsubst {})", format_pipeline(pipeline)),
357 StringPart::LastExitCode => "(last-exit-code)".to_string(),
358 StringPart::CurrentPid => "(current-pid)".to_string(),
359 }
360}
361
362fn escape_for_display(s: &str) -> String {
364 s.replace('\n', "\\n")
365 .replace('\t', "\\t")
366 .replace('\r', "\\r")
367}
368
369pub fn format_value(value: &Value) -> String {
371 match value {
372 Value::Null => "(null)".to_string(),
373 Value::Bool(b) => format!("(bool {})", b),
374 Value::Int(n) => format!("(int {})", n),
375 Value::Float(f) => format!("(float {})", f),
376 Value::String(s) => format!("(string \"{}\")", escape_for_display(s)),
377 Value::Json(json) => format!("(json {})", json),
378 Value::Blob(blob) => format!("(blob id={} size={} type={})", blob.id, blob.size, blob.content_type),
379 }
380}
381
382pub fn format_varpath(path: &VarPath) -> String {
384 path.segments
385 .iter()
386 .map(|seg| match seg {
387 VarSegment::Field(name) => name.clone(),
388 })
389 .collect::<Vec<_>>()
390 .join(".")
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn format_simple_int() {
399 assert_eq!(format_value(&Value::Int(42)), "(int 42)");
400 }
401
402 #[test]
403 fn format_simple_string() {
404 assert_eq!(format_value(&Value::String("hello".to_string())), "(string \"hello\")");
405 }
406
407 #[test]
408 fn format_varpath_simple() {
409 let path = VarPath::simple("X");
410 assert_eq!(format_varpath(&path), "X");
411 }
412
413 #[test]
414 fn format_varpath_nested() {
415 let path = VarPath {
416 segments: vec![
417 VarSegment::Field("VAR".to_string()),
418 VarSegment::Field("field".to_string()),
419 ],
420 };
421 assert_eq!(format_varpath(&path), "VAR.field");
422 }
423}