pub use crate::extensions::heredoc_ast::HereDocInfo;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshProgram {
pub lists: Vec<ZshList>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshList {
pub sublist: ZshSublist,
pub flags: ListFlags,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct ListFlags {
pub async_: bool,
pub disown: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshSublist {
pub pipe: ZshPipe,
pub next: Option<(SublistOp, Box<ZshSublist>)>,
pub flags: SublistFlags,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SublistOp {
And, Or, }
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct SublistFlags {
pub coproc: bool,
pub not: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshPipe {
pub cmd: ZshCommand,
pub next: Option<Box<ZshPipe>>,
pub lineno: u64,
#[serde(default)]
pub merge_stderr: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ZshCommand {
Simple(ZshSimple),
Subsh(Box<ZshProgram>), Cursh(Box<ZshProgram>), For(ZshFor),
Case(ZshCase),
If(ZshIf),
While(ZshWhile),
Until(ZshWhile),
Repeat(ZshRepeat),
FuncDef(ZshFuncDef),
Time(Option<Box<ZshSublist>>),
Cond(ZshCond), Arith(String), Try(ZshTry), Redirected(Box<ZshCommand>, Vec<ZshRedir>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshSimple {
pub assigns: Vec<ZshAssign>,
pub words: Vec<String>,
pub redirs: Vec<ZshRedir>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshAssign {
pub name: String,
pub value: ZshAssignValue,
pub append: bool, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ZshAssignValue {
Scalar(String),
Array(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshRedir {
pub rtype: i32,
pub fd: i32,
pub name: String,
pub heredoc: Option<HereDocInfo>,
pub varid: Option<String>, #[serde(skip)]
pub heredoc_idx: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshFor {
pub var: String,
pub list: ForList,
pub body: Box<ZshProgram>,
#[serde(default)]
pub is_select: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ForList {
Words(Vec<String>),
CStyle {
init: String,
cond: String,
step: String,
},
Positional,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshCase {
pub word: String,
pub arms: Vec<CaseArm>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaseArm {
pub patterns: Vec<String>,
pub body: ZshProgram,
pub terminator: CaseTerm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CaseTerm {
Break, Continue, TestNext, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshIf {
pub cond: Box<ZshProgram>,
pub then: Box<ZshProgram>,
pub elif: Vec<(ZshProgram, ZshProgram)>,
pub else_: Option<Box<ZshProgram>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshWhile {
pub cond: Box<ZshProgram>,
pub body: Box<ZshProgram>,
pub until: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshRepeat {
pub count: String,
pub body: Box<ZshProgram>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshFuncDef {
pub names: Vec<String>,
pub body: Box<ZshProgram>,
pub tracing: bool,
#[serde(default)]
pub auto_call_args: Option<Vec<String>>,
#[serde(default)]
pub body_source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ZshCond {
Not(Box<ZshCond>),
And(Box<ZshCond>, Box<ZshCond>),
Or(Box<ZshCond>, Box<ZshCond>),
Unary(String, String), Binary(String, String, String), Regex(String, String), }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZshTry {
pub try_block: Box<ZshProgram>,
pub always: Box<ZshProgram>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ZshParamFlag {
Lower, Upper, Capitalize, Join(String), JoinNewline, Split(String), SplitLines, SplitWords, Type, Words, Quote, QuoteIfNeeded, DoubleQuote, DollarQuote, QuoteBackslash, Unique, Reverse, Sort, NumericSort, IndexSort, Keys, Values, Length, CountChars, Expand, PromptExpand, PromptExpandFull, Visible, Directory, Head(usize), Tail(usize), PadLeft(usize, char), PadRight(usize, char), Width(usize), Match, Remove, Subscript, Parameter, Glob, At,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ListOp {
And, Or, Semi, Amp, Newline, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ShellWord {
Literal(String),
Concat(Vec<ShellWord>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VarModifier {
Default(ShellWord),
DefaultAssign(ShellWord),
Error(ShellWord),
Alternate(ShellWord),
Length,
Substring(i64, Option<i64>),
RemovePrefix(ShellWord),
RemovePrefixLong(ShellWord),
RemoveSuffix(ShellWord),
RemoveSuffixLong(ShellWord),
Replace(ShellWord, ShellWord),
ReplaceAll(ShellWord, ShellWord),
ReplacePrefix(ShellWord, ShellWord),
ReplaceSuffix(ShellWord, ShellWord),
Upper,
Lower,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ShellCommand {
Simple(SimpleCommand),
Pipeline(Vec<ShellCommand>, bool),
List(Vec<(ShellCommand, ListOp)>),
Compound(CompoundCommand),
FunctionDef(String, Box<ShellCommand>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimpleCommand {
pub assignments: Vec<(String, ShellWord, bool)>,
pub words: Vec<ShellWord>,
pub redirects: Vec<Redirect>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Redirect {
pub fd: Option<i32>,
pub op: RedirectOp,
pub target: ShellWord,
pub heredoc_content: Option<String>,
pub fd_var: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RedirectOp {
Write,
Append,
Read,
ReadWrite,
Clobber,
DupRead,
DupWrite,
HereDoc,
HereString,
WriteBoth,
AppendBoth,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompoundCommand {
BraceGroup(Vec<ShellCommand>),
Subshell(Vec<ShellCommand>),
If {
conditions: Vec<(Vec<ShellCommand>, Vec<ShellCommand>)>,
else_part: Option<Vec<ShellCommand>>,
},
For {
var: String,
words: Option<Vec<ShellWord>>,
body: Vec<ShellCommand>,
},
ForArith {
init: String,
cond: String,
step: String,
body: Vec<ShellCommand>,
},
While {
condition: Vec<ShellCommand>,
body: Vec<ShellCommand>,
},
Until {
condition: Vec<ShellCommand>,
body: Vec<ShellCommand>,
},
Case {
word: ShellWord,
cases: Vec<(Vec<ShellWord>, Vec<ShellCommand>, CaseTerminator)>,
},
Select {
var: String,
words: Option<Vec<ShellWord>>,
body: Vec<ShellCommand>,
},
Coproc {
name: Option<String>,
body: Box<ShellCommand>,
},
Repeat {
count: String,
body: Vec<ShellCommand>,
},
Try {
try_body: Vec<ShellCommand>,
always_body: Vec<ShellCommand>,
},
Arith(String),
WithRedirects(Box<ShellCommand>, Vec<Redirect>),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum CaseTerminator {
Break,
Fallthrough,
Continue,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_flags_default_all_false() {
let f = ListFlags::default();
assert!(!f.async_, "default ListFlags.async_ must be false");
assert!(!f.disown, "default ListFlags.disown must be false");
}
#[test]
fn sublist_flags_default_all_false() {
let f = SublistFlags::default();
assert!(!f.coproc);
assert!(!f.not);
}
#[test]
fn zsh_program_empty_round_trips() {
let p = ZshProgram { lists: vec![] };
let json = serde_json::to_string(&p).expect("serialize");
let back: ZshProgram = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.lists.len(), 0);
}
#[test]
fn zsh_simple_round_trips_with_assigns_and_redirs() {
let simple = ZshSimple {
assigns: vec![
ZshAssign {
name: "FOO".to_string(),
value: ZshAssignValue::Scalar("bar".to_string()),
append: false,
},
ZshAssign {
name: "BAZ".to_string(),
value: ZshAssignValue::Scalar("qux".to_string()),
append: true,
},
],
words: vec!["echo".to_string(), "hi".to_string()],
redirs: vec![ZshRedir {
rtype: 0,
fd: 1,
name: "out".to_string(),
heredoc: None,
varid: None,
heredoc_idx: None,
}],
};
let json = serde_json::to_string(&simple).expect("serialize");
let back: ZshSimple = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.assigns.len(), 2);
assert_eq!(back.assigns[0].name, "FOO");
assert!(back.assigns[1].append, "+= flag must round-trip");
assert_eq!(back.words, vec!["echo", "hi"]);
assert_eq!(back.redirs.len(), 1);
assert_eq!(back.redirs[0].name, "out");
}
#[test]
fn assign_value_array_variant_round_trips() {
let v = ZshAssignValue::Array(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
let json = serde_json::to_string(&v).expect("serialize");
let back: ZshAssignValue = serde_json::from_str(&json).expect("deserialize");
match back {
ZshAssignValue::Array(items) => assert_eq!(items, vec!["a", "b", "c"]),
ZshAssignValue::Scalar(s) => panic!("expected Array, got Scalar({s:?})"),
}
}
#[test]
fn for_list_c_style_round_trips() {
let fl = ForList::CStyle {
init: "i=0".to_string(),
cond: "i<10".to_string(),
step: "i++".to_string(),
};
let json = serde_json::to_string(&fl).expect("serialize");
let back: ForList = serde_json::from_str(&json).expect("deserialize");
match back {
ForList::CStyle { init, cond, step } => {
assert_eq!(init, "i=0");
assert_eq!(cond, "i<10");
assert_eq!(step, "i++");
}
_ => panic!("expected CStyle variant"),
}
}
#[test]
fn for_list_positional_round_trips() {
let fl = ForList::Positional;
let json = serde_json::to_string(&fl).expect("serialize");
let back: ForList = serde_json::from_str(&json).expect("deserialize");
assert!(matches!(back, ForList::Positional));
}
#[test]
fn case_terminator_all_variants_round_trip() {
for t in [CaseTerm::Break, CaseTerm::Continue, CaseTerm::TestNext] {
let json = serde_json::to_string(&t).expect("serialize");
let back: CaseTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, t);
}
}
#[test]
fn sublist_op_round_trips_both_variants() {
for op in [SublistOp::And, SublistOp::Or] {
let json = serde_json::to_string(&op).expect("serialize");
let back: SublistOp = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, op);
}
}
#[test]
fn list_op_round_trips_all_variants() {
for op in [
ListOp::And,
ListOp::Or,
ListOp::Semi,
ListOp::Amp,
ListOp::Newline,
] {
let json = serde_json::to_string(&op).expect("serialize");
let back: ListOp = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, op);
}
}
#[test]
fn shell_word_concat_round_trips_nested() {
let w = ShellWord::Concat(vec![
ShellWord::Literal("foo".to_string()),
ShellWord::Literal("bar".to_string()),
ShellWord::Concat(vec![ShellWord::Literal("baz".to_string())]),
]);
let json = serde_json::to_string(&w).expect("serialize");
let back: ShellWord = serde_json::from_str(&json).expect("deserialize");
match back {
ShellWord::Concat(parts) => {
assert_eq!(parts.len(), 3, "outer concat must preserve element count");
match &parts[2] {
ShellWord::Concat(inner) => assert_eq!(inner.len(), 1),
_ => panic!("nested concat lost"),
}
}
_ => panic!("expected Concat top-level"),
}
}
#[test]
fn zsh_cond_nested_serialization() {
let c = ZshCond::And(
Box::new(ZshCond::Unary("-f".to_string(), "file".to_string())),
Box::new(ZshCond::Not(Box::new(ZshCond::Unary(
"-d".to_string(),
"dir".to_string(),
)))),
);
let json = serde_json::to_string(&c).expect("serialize");
let back: ZshCond = serde_json::from_str(&json).expect("deserialize");
match back {
ZshCond::And(lhs, rhs) => {
assert!(matches!(*lhs, ZshCond::Unary(_, _)));
assert!(matches!(*rhs, ZshCond::Not(_)));
}
_ => panic!("expected And at root"),
}
}
#[test]
fn redirect_op_all_variants_round_trip() {
for op in [
RedirectOp::Write,
RedirectOp::Append,
RedirectOp::Read,
RedirectOp::ReadWrite,
RedirectOp::Clobber,
RedirectOp::DupRead,
RedirectOp::DupWrite,
RedirectOp::HereDoc,
RedirectOp::HereString,
RedirectOp::WriteBoth,
RedirectOp::AppendBoth,
] {
let json = serde_json::to_string(&op).expect("serialize");
let back: RedirectOp = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, op);
}
}
#[test]
fn zsh_funcdef_serde_default_fields() {
let json = r#"{
"names": ["myfn"],
"body": { "lists": [] },
"tracing": false
}"#;
let fd: ZshFuncDef = serde_json::from_str(json).expect("default fields must apply");
assert_eq!(fd.names, vec!["myfn"]);
assert!(fd.auto_call_args.is_none());
assert!(fd.body_source.is_none());
assert!(!fd.tracing);
}
#[test]
fn zsh_pipe_merge_stderr_default_when_missing() {
let json = r#"{
"cmd": { "Simple": { "assigns": [], "words": ["x"], "redirs": [] } },
"next": null,
"lineno": 1
}"#;
let pipe: ZshPipe = serde_json::from_str(json).expect("default must apply");
assert!(!pipe.merge_stderr);
assert_eq!(pipe.lineno, 1);
}
}