use super::*;
pub fn format_program(program: &Program) -> String {
let stmts: Vec<_> = program
.statements
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.collect();
match stmts.len() {
0 => "(program)".to_string(),
1 => format_stmt(stmts[0]),
_ => {
let parts: Vec<String> = stmts.iter().map(|s| format_stmt(s)).collect();
format!("(program {})", parts.join(" "))
}
}
}
pub fn format_stmt(stmt: &Stmt) -> String {
match stmt {
Stmt::Assignment(a) => format_assignment(a),
Stmt::Command(cmd) => format_command(cmd),
Stmt::Pipeline(p) => format_pipeline(p),
Stmt::If(if_stmt) => format_if(if_stmt),
Stmt::For(for_loop) => format_for(for_loop),
Stmt::While(while_loop) => format_while(while_loop),
Stmt::Case(case_stmt) => format_case(case_stmt),
Stmt::Break(n) => match n {
Some(level) => format!("(break {})", level),
None => "(break)".to_string(),
},
Stmt::Continue(n) => match n {
Some(level) => format!("(continue {})", level),
None => "(continue)".to_string(),
},
Stmt::Return(expr) => match expr {
Some(e) => format!("(return {})", format_expr(e)),
None => "(return)".to_string(),
},
Stmt::Exit(expr) => match expr {
Some(e) => format!("(exit {})", format_expr(e)),
None => "(exit)".to_string(),
},
Stmt::ToolDef(tool) => format_tooldef(tool),
Stmt::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
Stmt::AndChain { left, right } => {
format!("(and-chain {} {})", format_stmt(left), format_stmt(right))
}
Stmt::OrChain { left, right } => {
format!("(or-chain {} {})", format_stmt(left), format_stmt(right))
}
Stmt::EnvScoped { assignments, body } => {
let assigns: Vec<String> = assignments.iter().map(format_assignment).collect();
format!("(env-scoped ({}) {})", assigns.join(" "), format_stmt(body))
}
Stmt::Empty => "(empty)".to_string(),
}
}
fn format_assignment(a: &Assignment) -> String {
let value = format_expr(&a.value);
format!("(assign {} {} local={})", a.name, value, a.local)
}
pub fn format_command(cmd: &Command) -> String {
let mut parts = vec![format!("(cmd {}", cmd.name)];
for arg in &cmd.args {
parts.push(format_arg(arg));
}
for redir in &cmd.redirects {
parts.push(format_redirect(redir));
}
format!("{})", parts.join(" "))
}
fn format_arg(arg: &Arg) -> String {
match arg {
Arg::Positional(expr) => format!("(pos {})", format_expr(expr)),
Arg::Named { key, value } => format!("(named {} {})", key, format_expr(value)),
Arg::WordAssign { key, value } => format!("(wordassign {} {})", key, format_expr(value)),
Arg::ShortFlag(f) => format!("(shortflag {})", f),
Arg::LongFlag(f) => format!("(longflag {})", f),
Arg::DoubleDash => "(doubledash)".to_string(),
}
}
fn format_redirect(redir: &Redirect) -> String {
let kind = match redir.kind {
RedirectKind::StdoutOverwrite => ">",
RedirectKind::StdoutAppend => ">>",
RedirectKind::Stdin => "<",
RedirectKind::HereDoc => "<<",
RedirectKind::HereString => "<<<",
RedirectKind::Stderr => "2>",
RedirectKind::Both => "&>",
RedirectKind::MergeStderr => "2>&1",
RedirectKind::MergeStdout => "1>&2",
};
format!("(redir {} {})", kind, format_expr(&redir.target))
}
pub fn format_stmt_block(stmts: &[Stmt]) -> String {
if stmts.len() == 1 {
format_stmt(&stmts[0])
} else {
let inner: Vec<String> = stmts.iter().map(format_stmt).collect();
format!("(block {})", inner.join(" "))
}
}
pub fn format_pipeline(p: &Pipeline) -> String {
let cmds: Vec<String> = p.commands.iter().map(format_command).collect();
if p.background {
if cmds.len() == 1 {
format!("(background {})", cmds[0])
} else {
format!("(background (pipeline {}))", cmds.join(" "))
}
} else {
format!("(pipeline {})", cmds.join(" "))
}
}
fn format_if(if_stmt: &IfStmt) -> String {
let cond = format_expr(&if_stmt.condition);
let then_stmts: Vec<String> = if_stmt
.then_branch
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.map(format_stmt)
.collect();
let then_part = format!("(then {})", then_stmts.join(" "));
match &if_stmt.else_branch {
Some(else_stmts) => {
let else_inner: Vec<String> = else_stmts
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.map(format_stmt)
.collect();
if else_inner.is_empty() {
format!("(if {} {} (else))", cond, then_part)
} else {
format!("(if {} {} (else {}))", cond, then_part, else_inner.join(" "))
}
}
None => format!("(if {} {} (else))", cond, then_part),
}
}
fn format_for(for_loop: &ForLoop) -> String {
let items: Vec<String> = for_loop.items.iter().map(format_expr).collect();
let body_stmts: Vec<String> = for_loop
.body
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.map(format_stmt)
.collect();
format!(
"(for {} (in {}) (do {}))",
for_loop.variable,
items.join(" "),
body_stmts.join(" ")
)
}
fn format_while(while_loop: &WhileLoop) -> String {
let cond = format_expr(&while_loop.condition);
let body_stmts: Vec<String> = while_loop
.body
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.map(format_stmt)
.collect();
format!("(while {} (do {}))", cond, body_stmts.join(" "))
}
fn format_case(case_stmt: &CaseStmt) -> String {
let expr = format_expr(&case_stmt.expr);
let branches: Vec<String> = case_stmt
.branches
.iter()
.map(format_case_branch)
.collect();
format!("(case {} ({}))", expr, branches.join(" "))
}
fn format_case_branch(branch: &CaseBranch) -> String {
let patterns = branch.patterns.join("|");
let body_stmts: Vec<String> = branch
.body
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.map(format_stmt)
.collect();
format!("(branch \"{}\" ({}))", patterns, body_stmts.join(" "))
}
fn format_tooldef(tool: &ToolDef) -> String {
let params: Vec<String> = tool.params.iter().map(format_param).collect();
let body_stmts: Vec<String> = tool
.body
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.map(format_stmt)
.collect();
format!(
"(tooldef {} ({}) ({}))",
tool.name,
params.join(" "),
body_stmts.join(" ")
)
}
fn format_param(param: &ParamDef) -> String {
let type_str = param
.param_type
.as_ref()
.map(|t| match t {
ParamType::String => "string",
ParamType::Int => "int",
ParamType::Float => "float",
ParamType::Bool => "bool",
})
.unwrap_or("any");
match ¶m.default {
Some(default) => format!("(param {} {} {})", param.name, type_str, format_expr(default)),
None => format!("(param {} {})", param.name, type_str),
}
}
pub fn format_expr(expr: &Expr) -> String {
match expr {
Expr::Literal(value) => format_value(value),
Expr::VarRef(path) => format!("(varref {})", format_varpath(path)),
Expr::Interpolated(parts) => {
let parts_str: Vec<String> = parts
.iter()
.map(format_string_part)
.collect();
format!("(interpolated {})", parts_str.join(" "))
}
Expr::HereDocBody { parts, strip_tabs } => {
let parts_str: Vec<String> = parts
.iter()
.map(|sp| format_string_part(&sp.part))
.collect();
format!(
"(heredoc-body strip-tabs={} {})",
strip_tabs,
parts_str.join(" ")
)
}
Expr::BinaryOp { left, op, right } => {
let op_str = match op {
BinaryOp::And => "and",
BinaryOp::Or => "or",
};
format!("({} {} {})", op_str, format_expr(left), format_expr(right))
}
Expr::CommandSubst(stmts) => {
format!("(cmdsubst {})", format_stmt_block(stmts))
}
Expr::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
Expr::Positional(n) => format!("(positional {})", n),
Expr::AllArgs => "(all-args)".to_string(),
Expr::ArgCount => "(arg-count)".to_string(),
Expr::VarLength(name) => format!("(var-length {})", name),
Expr::VarWithDefault { name, default } => {
let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
format!("(var-default {} ({}))", name, default_parts.join(" "))
}
Expr::Arithmetic(expr_str) => format!("(arithmetic \"{}\")", expr_str),
Expr::Command(cmd) => format_command(cmd),
Expr::LastExitCode => "(last-exit-code)".to_string(),
Expr::CurrentPid => "(current-pid)".to_string(),
Expr::GlobPattern(s) => format!("(glob \"{}\")", s),
}
}
pub fn format_test_expr(test: &TestExpr) -> String {
match test {
TestExpr::FileTest { op, path } => {
let op_str = match op {
FileTestOp::Exists => "-e",
FileTestOp::IsFile => "-f",
FileTestOp::IsDir => "-d",
FileTestOp::Readable => "-r",
FileTestOp::Writable => "-w",
FileTestOp::Executable => "-x",
};
format!("(file {} {})", op_str, format_expr(path))
}
TestExpr::StringTest { op, value } => {
let op_str = match op {
StringTestOp::IsEmpty => "-z",
StringTestOp::IsNonEmpty => "-n",
};
format!("(string {} {})", op_str, format_expr(value))
}
TestExpr::Comparison { left, op, right } => {
let op_str = match op {
TestCmpOp::Eq => "==",
TestCmpOp::NotEq => "!=",
TestCmpOp::Match => "=~",
TestCmpOp::NotMatch => "!~",
TestCmpOp::Gt => ">",
TestCmpOp::Lt => "<",
TestCmpOp::GtEq => ">=",
TestCmpOp::LtEq => "<=",
TestCmpOp::NumEq => "-eq",
TestCmpOp::NumNotEq => "-ne",
TestCmpOp::NumGt => "-gt",
TestCmpOp::NumLt => "-lt",
TestCmpOp::NumGtEq => "-ge",
TestCmpOp::NumLtEq => "-le",
};
format!(
"(cmp {} {} {})",
op_str,
format_expr(left),
format_expr(right)
)
}
TestExpr::And { left, right } => {
format!("(and {} {})", format_test_expr(left), format_test_expr(right))
}
TestExpr::Or { left, right } => {
format!("(or {} {})", format_test_expr(left), format_test_expr(right))
}
TestExpr::Not { expr } => {
format!("(not {})", format_test_expr(expr))
}
}
}
fn format_string_part(part: &StringPart) -> String {
match part {
StringPart::Literal(s) => format!("\"{}\"", escape_for_display(s)),
StringPart::Var(path) => format!("(varref {})", format_varpath(path)),
StringPart::VarWithDefault { name, default } => {
let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
format!("(vardefault {} ({}))", name, default_parts.join(" "))
}
StringPart::VarLength(name) => format!("(varlength {})", name),
StringPart::Positional(n) => format!("(positional {})", n),
StringPart::AllArgs => "(allargs)".to_string(),
StringPart::ArgCount => "(argcount)".to_string(),
StringPart::Arithmetic(expr) => format!("(arith \"{}\")", expr),
StringPart::CommandSubst(stmts) => format!("(cmdsubst {})", format_stmt_block(stmts)),
StringPart::LastExitCode => "(last-exit-code)".to_string(),
StringPart::CurrentPid => "(current-pid)".to_string(),
}
}
fn escape_for_display(s: &str) -> String {
s.replace('\n', "\\n")
.replace('\t', "\\t")
.replace('\r', "\\r")
}
pub fn format_value(value: &Value) -> String {
match value {
Value::Null => "(null)".to_string(),
Value::Bool(b) => format!("(bool {})", b),
Value::Int(n) => format!("(int {})", n),
Value::Float(f) => format!("(float {})", f),
Value::String(s) => format!("(string \"{}\")", escape_for_display(s)),
Value::Json(json) => format!("(json {})", json),
Value::Blob(blob) => format!("(blob id={} size={} type={})", blob.id, blob.size, blob.content_type),
}
}
pub fn format_varpath(path: &VarPath) -> String {
path.segments
.iter()
.map(|seg| match seg {
VarSegment::Field(name) => name.clone(),
})
.collect::<Vec<_>>()
.join(".")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_simple_int() {
assert_eq!(format_value(&Value::Int(42)), "(int 42)");
}
#[test]
fn format_simple_string() {
assert_eq!(format_value(&Value::String("hello".to_string())), "(string \"hello\")");
}
#[test]
fn format_varpath_simple() {
let path = VarPath::simple("X");
assert_eq!(format_varpath(&path), "X");
}
#[test]
fn format_varpath_nested() {
let path = VarPath {
segments: vec![
VarSegment::Field("VAR".to_string()),
VarSegment::Field("field".to_string()),
],
};
assert_eq!(format_varpath(&path), "VAR.field");
}
}