Skip to main content

zsh/
parser.rs

1//! Zsh parser - Direct port from zsh/Src/parse.c
2//!
3//! This parser takes tokens from the ZshLexer and builds an AST.
4//! It follows the zsh grammar closely, producing structures that
5//! can be executed by the shell executor.
6
7use crate::lexer::ZshLexer;
8use crate::tokens::LexTok;
9use serde::{Deserialize, Serialize};
10use std::iter::Peekable;
11use std::str::Chars;
12
13/// AST node for a complete program (list of commands)
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ZshProgram {
16    pub lists: Vec<ZshList>,
17}
18
19/// A list is a sequence of sublists separated by ; or & or newline
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ZshList {
22    pub sublist: ZshSublist,
23    pub flags: ListFlags,
24}
25
26#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
27pub struct ListFlags {
28    /// Run asynchronously (&)
29    pub async_: bool,
30    /// Disown after running (&| or &!)
31    pub disown: bool,
32}
33
34/// A sublist is pipelines connected by && or ||
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ZshSublist {
37    pub pipe: ZshPipe,
38    pub next: Option<(SublistOp, Box<ZshSublist>)>,
39    pub flags: SublistFlags,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum SublistOp {
44    And, // &&
45    Or,  // ||
46}
47
48#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
49pub struct SublistFlags {
50    /// Coproc
51    pub coproc: bool,
52    /// Negated with !
53    pub not: bool,
54}
55
56/// A pipeline is commands connected by |
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ZshPipe {
59    pub cmd: ZshCommand,
60    pub next: Option<Box<ZshPipe>>,
61    pub lineno: u64,
62}
63
64/// A command
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub enum ZshCommand {
67    Simple(ZshSimple),
68    Subsh(Box<ZshProgram>), // (list)
69    Cursh(Box<ZshProgram>), // {list}
70    For(ZshFor),
71    Case(ZshCase),
72    If(ZshIf),
73    While(ZshWhile),
74    Until(ZshWhile),
75    Repeat(ZshRepeat),
76    FuncDef(ZshFuncDef),
77    Time(Option<Box<ZshSublist>>),
78    Cond(ZshCond), // [[ ... ]]
79    Arith(String), // (( ... ))
80    Try(ZshTry),   // { ... } always { ... }
81}
82
83/// A simple command (assignments, words, redirections)
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ZshSimple {
86    pub assigns: Vec<ZshAssign>,
87    pub words: Vec<String>,
88    pub redirs: Vec<ZshRedir>,
89}
90
91/// An assignment
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ZshAssign {
94    pub name: String,
95    pub value: ZshAssignValue,
96    pub append: bool, // +=
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub enum ZshAssignValue {
101    Scalar(String),
102    Array(Vec<String>),
103}
104
105/// A redirection
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ZshRedir {
108    pub rtype: RedirType,
109    pub fd: i32,
110    pub name: String,
111    pub heredoc: Option<HereDocInfo>,
112    pub varid: Option<String>, // {var}>file
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct HereDocInfo {
117    pub content: String,
118    pub terminator: String,
119}
120
121/// Redirection type
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123pub enum RedirType {
124    Write,        // >
125    Writenow,     // >|
126    Append,       // >>
127    Appendnow,    // >>|
128    Read,         // <
129    ReadWrite,    // <>
130    Heredoc,      // <<
131    HeredocDash,  // <<-
132    Herestr,      // <<<
133    MergeIn,      // <&
134    MergeOut,     // >&
135    ErrWrite,     // &>
136    ErrWritenow,  // &>|
137    ErrAppend,    // >>&
138    ErrAppendnow, // >>&|
139    InPipe,       // < <(...)
140    OutPipe,      // > >(...)
141}
142
143/// For loop
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ZshFor {
146    pub var: String,
147    pub list: ForList,
148    pub body: Box<ZshProgram>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum ForList {
153    Words(Vec<String>),
154    CStyle {
155        init: String,
156        cond: String,
157        step: String,
158    },
159    Positional,
160}
161
162/// Case statement
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ZshCase {
165    pub word: String,
166    pub arms: Vec<CaseArm>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct CaseArm {
171    pub patterns: Vec<String>,
172    pub body: ZshProgram,
173    pub terminator: CaseTerm,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
177pub enum CaseTerm {
178    Break,    // ;;
179    Continue, // ;&
180    TestNext, // ;|
181}
182
183/// If statement
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ZshIf {
186    pub cond: Box<ZshProgram>,
187    pub then: Box<ZshProgram>,
188    pub elif: Vec<(ZshProgram, ZshProgram)>,
189    pub else_: Option<Box<ZshProgram>>,
190}
191
192/// While/Until loop
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ZshWhile {
195    pub cond: Box<ZshProgram>,
196    pub body: Box<ZshProgram>,
197    pub until: bool,
198}
199
200/// Repeat loop
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ZshRepeat {
203    pub count: String,
204    pub body: Box<ZshProgram>,
205}
206
207/// Function definition
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ZshFuncDef {
210    pub names: Vec<String>,
211    pub body: Box<ZshProgram>,
212    pub tracing: bool,
213}
214
215/// Conditional expression [[ ... ]]
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub enum ZshCond {
218    Not(Box<ZshCond>),
219    And(Box<ZshCond>, Box<ZshCond>),
220    Or(Box<ZshCond>, Box<ZshCond>),
221    Unary(String, String),          // -f file, -n str, etc.
222    Binary(String, String, String), // str = pat, a -eq b, etc.
223    Regex(String, String),          // str =~ regex
224}
225
226/// Try/always block
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ZshTry {
229    pub try_block: Box<ZshProgram>,
230    pub always: Box<ZshProgram>,
231}
232
233/// Zsh parameter expansion flags
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub enum ZshParamFlag {
236    Lower,                 // L - lowercase
237    Upper,                 // U - uppercase
238    Capitalize,            // C - capitalize words
239    Join(String),          // j:sep: - join array with separator
240    JoinNewline,           // F - join with newlines
241    Split(String),         // s:sep: - split string into array
242    SplitLines,            // f - split on newlines
243    SplitWords,            // z - split into words (shell parsing)
244    Type,                  // t - type of variable
245    Words,                 // w - word splitting
246    Quote,                 // q - quote result
247    DoubleQuote,           // qq - double quote
248    QuoteBackslash,        // b - quote with backslashes for patterns
249    Unique,                // u - unique elements only
250    Reverse,               // O - reverse sort
251    Sort,                  // o - sort
252    NumericSort,           // n - numeric sort
253    IndexSort,             // a - sort in array index order
254    Keys,                  // k - associative array keys
255    Values,                // v - associative array values
256    Length,                // # - length (character codes)
257    CountChars,            // c - count total characters
258    Expand,                // e - perform shell expansions
259    PromptExpand,          // % - expand prompt escapes
260    PromptExpandFull,      // %% - full prompt expansion
261    Visible,               // V - make non-printable chars visible
262    Directory,             // D - substitute directory names
263    Head(usize),           // [1,n] - first n elements
264    Tail(usize),           // [-n,-1] - last n elements
265    PadLeft(usize, char),  // l:len:fill: - pad left
266    PadRight(usize, char), // r:len:fill: - pad right
267    Width(usize),          // m - use width for padding
268    Match,                 // M - include matched portion
269    Remove,                // R - include non-matched portion (complement of M)
270    Subscript,             // S - subscript scanning
271    Parameter,             // P - use value as parameter name (indirection)
272    Glob,                  // ~ - glob patterns in pattern
273}
274
275/// List operator (for shell command lists)
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
277pub enum ListOp {
278    And,     // &&
279    Or,      // ||
280    Semi,    // ;
281    Amp,     // &
282    Newline, // \n
283}
284
285/// Shell word - can be simple literal or complex expansion
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub enum ShellWord {
288    Literal(String),
289    SingleQuoted(String),
290    DoubleQuoted(Vec<ShellWord>),
291    Variable(String),
292    VariableBraced(String, Option<Box<VarModifier>>),
293    ArrayVar(String, Box<ShellWord>),
294    CommandSub(Box<ShellCommand>),
295    ProcessSubIn(Box<ShellCommand>),
296    ProcessSubOut(Box<ShellCommand>),
297    ArithSub(String),
298    ArrayLiteral(Vec<ShellWord>),
299    Glob(String),
300    Tilde(Option<String>),
301    Concat(Vec<ShellWord>),
302}
303
304/// Variable modifier for parameter expansion
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub enum VarModifier {
307    Default(ShellWord),
308    DefaultAssign(ShellWord),
309    Error(ShellWord),
310    Alternate(ShellWord),
311    Length,
312    ArrayLength,
313    ArrayIndex(String),
314    ArrayAll,
315    Substring(i64, Option<i64>),
316    RemovePrefix(ShellWord),
317    RemovePrefixLong(ShellWord),
318    RemoveSuffix(ShellWord),
319    RemoveSuffixLong(ShellWord),
320    Replace(ShellWord, ShellWord),
321    ReplaceAll(ShellWord, ShellWord),
322    Upper,
323    Lower,
324    ZshFlags(Vec<ZshParamFlag>),
325}
326
327/// Shell command - the old shell_ast compatible type
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub enum ShellCommand {
330    Simple(SimpleCommand),
331    Pipeline(Vec<ShellCommand>, bool),
332    List(Vec<(ShellCommand, ListOp)>),
333    Compound(CompoundCommand),
334    FunctionDef(String, Box<ShellCommand>),
335}
336
337/// Simple command with assignments, words, and redirects
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct SimpleCommand {
340    pub assignments: Vec<(String, ShellWord, bool)>,
341    pub words: Vec<ShellWord>,
342    pub redirects: Vec<Redirect>,
343}
344
345/// Redirect
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct Redirect {
348    pub fd: Option<i32>,
349    pub op: RedirectOp,
350    pub target: ShellWord,
351    pub heredoc_content: Option<String>,
352    pub fd_var: Option<String>,
353}
354
355/// Redirect operator
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
357pub enum RedirectOp {
358    Write,
359    Append,
360    Read,
361    ReadWrite,
362    Clobber,
363    DupRead,
364    DupWrite,
365    HereDoc,
366    HereString,
367    WriteBoth,
368    AppendBoth,
369}
370
371/// Compound command
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub enum CompoundCommand {
374    BraceGroup(Vec<ShellCommand>),
375    Subshell(Vec<ShellCommand>),
376    If {
377        conditions: Vec<(Vec<ShellCommand>, Vec<ShellCommand>)>,
378        else_part: Option<Vec<ShellCommand>>,
379    },
380    For {
381        var: String,
382        words: Option<Vec<ShellWord>>,
383        body: Vec<ShellCommand>,
384    },
385    ForArith {
386        init: String,
387        cond: String,
388        step: String,
389        body: Vec<ShellCommand>,
390    },
391    While {
392        condition: Vec<ShellCommand>,
393        body: Vec<ShellCommand>,
394    },
395    Until {
396        condition: Vec<ShellCommand>,
397        body: Vec<ShellCommand>,
398    },
399    Case {
400        word: ShellWord,
401        cases: Vec<(Vec<ShellWord>, Vec<ShellCommand>, CaseTerminator)>,
402    },
403    Select {
404        var: String,
405        words: Option<Vec<ShellWord>>,
406        body: Vec<ShellCommand>,
407    },
408    Coproc {
409        name: Option<String>,
410        body: Box<ShellCommand>,
411    },
412    /// repeat N do ... done
413    Repeat {
414        count: String,
415        body: Vec<ShellCommand>,
416    },
417    /// { try-block } always { always-block }
418    Try {
419        try_body: Vec<ShellCommand>,
420        always_body: Vec<ShellCommand>,
421    },
422    Cond(CondExpr),
423    Arith(String),
424    WithRedirects(Box<ShellCommand>, Vec<Redirect>),
425}
426
427/// Case terminator
428#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
429pub enum CaseTerminator {
430    Break,
431    Fallthrough,
432    Continue,
433}
434
435/// Conditional expression for [[ ]]
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub enum CondExpr {
438    FileExists(ShellWord),
439    FileRegular(ShellWord),
440    FileDirectory(ShellWord),
441    FileSymlink(ShellWord),
442    FileReadable(ShellWord),
443    FileWritable(ShellWord),
444    FileExecutable(ShellWord),
445    FileNonEmpty(ShellWord),
446    StringEmpty(ShellWord),
447    StringNonEmpty(ShellWord),
448    StringEqual(ShellWord, ShellWord),
449    StringNotEqual(ShellWord, ShellWord),
450    StringMatch(ShellWord, ShellWord),
451    StringLess(ShellWord, ShellWord),
452    StringGreater(ShellWord, ShellWord),
453    NumEqual(ShellWord, ShellWord),
454    NumNotEqual(ShellWord, ShellWord),
455    NumLess(ShellWord, ShellWord),
456    NumLessEqual(ShellWord, ShellWord),
457    NumGreater(ShellWord, ShellWord),
458    NumGreaterEqual(ShellWord, ShellWord),
459    Not(Box<CondExpr>),
460    And(Box<CondExpr>, Box<CondExpr>),
461    Or(Box<CondExpr>, Box<CondExpr>),
462}
463
464/// Parse errors
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct ParseError {
467    pub message: String,
468    pub line: u64,
469}
470
471impl std::fmt::Display for ParseError {
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473        write!(f, "parse error at line {}: {}", self.line, self.message)
474    }
475}
476
477impl std::error::Error for ParseError {}
478
479// ============================================================================
480// ShellToken, ShellLexer, and ShellParser - compatibility layer for exec.rs
481// ============================================================================
482
483#[derive(Debug, Clone, PartialEq)]
484pub enum ShellToken {
485    Word(String),
486    SingleQuotedWord(String),
487    DoubleQuotedWord(String),
488    Number(i64),
489    Semi,
490    Newline,
491    Amp,
492    AmpAmp,
493    Pipe,
494    PipePipe,
495    LParen,
496    RParen,
497    LBrace,
498    RBrace,
499    LBracket,
500    RBracket,
501    DoubleLBracket,
502    DoubleRBracket,
503    Less,
504    Greater,
505    GreaterGreater,
506    LessGreater,
507    GreaterAmp,
508    LessAmp,
509    GreaterPipe,
510    LessLess,
511    LessLessLess,
512    HereDoc(String, String),
513    AmpGreater,
514    AmpGreaterGreater,
515    DoubleLParen,
516    DoubleRParen,
517    If,
518    Then,
519    Else,
520    Elif,
521    Fi,
522    Case,
523    Esac,
524    For,
525    While,
526    Until,
527    Do,
528    Done,
529    In,
530    Function,
531    Select,
532    Time,
533    Coproc,
534    Typeset(String),
535    Repeat,
536    Always,
537    Bang,
538    DoubleSemi,
539    SemiAmp,
540    SemiSemiAmp,
541    Eof,
542}
543
544pub struct ShellLexer<'a> {
545    input: Peekable<Chars<'a>>,
546    line: usize,
547    col: usize,
548    at_line_start: bool,
549}
550
551impl<'a> ShellLexer<'a> {
552    pub fn new(input: &'a str) -> Self {
553        Self {
554            input: input.chars().peekable(),
555            line: 1,
556            col: 1,
557            at_line_start: true,
558        }
559    }
560
561    fn peek(&mut self) -> Option<char> {
562        self.input.peek().copied()
563    }
564
565    fn next_char(&mut self) -> Option<char> {
566        let c = self.input.next();
567        if let Some(ch) = c {
568            if ch == '\n' {
569                self.line += 1;
570                self.col = 1;
571                self.at_line_start = true;
572            } else {
573                self.col += 1;
574                self.at_line_start = false;
575            }
576        }
577        c
578    }
579
580    fn skip_whitespace(&mut self) -> bool {
581        let mut had_whitespace = false;
582        while let Some(c) = self.peek() {
583            if c == ' ' || c == '\t' {
584                self.next_char();
585                had_whitespace = true;
586            } else if c == '\\' {
587                self.next_char();
588                if self.peek() == Some('\n') {
589                    self.next_char();
590                }
591                had_whitespace = true;
592            } else {
593                break;
594            }
595        }
596        had_whitespace
597    }
598
599    fn skip_comment(&mut self, after_whitespace: bool) {
600        if after_whitespace && self.peek() == Some('#') {
601            while let Some(c) = self.peek() {
602                if c == '\n' {
603                    break;
604                }
605                self.next_char();
606            }
607        }
608    }
609
610    pub fn next_token(&mut self) -> ShellToken {
611        let was_at_line_start = self.at_line_start;
612        let had_whitespace = self.skip_whitespace();
613        self.skip_comment(had_whitespace || was_at_line_start);
614
615        let c = match self.peek() {
616            Some(c) => c,
617            None => return ShellToken::Eof,
618        };
619
620        if c == '\n' {
621            self.next_char();
622            self.at_line_start = true;
623            return ShellToken::Newline;
624        }
625
626        self.at_line_start = false;
627
628        if c == ';' {
629            self.next_char();
630            if self.peek() == Some(';') {
631                self.next_char();
632                if self.peek() == Some('&') {
633                    self.next_char();
634                    return ShellToken::SemiSemiAmp;
635                }
636                return ShellToken::DoubleSemi;
637            }
638            if self.peek() == Some('&') {
639                self.next_char();
640                return ShellToken::SemiAmp;
641            }
642            return ShellToken::Semi;
643        }
644
645        if c == '&' {
646            self.next_char();
647            match self.peek() {
648                Some('&') => {
649                    self.next_char();
650                    return ShellToken::AmpAmp;
651                }
652                Some('>') => {
653                    self.next_char();
654                    if self.peek() == Some('>') {
655                        self.next_char();
656                        return ShellToken::AmpGreaterGreater;
657                    }
658                    return ShellToken::AmpGreater;
659                }
660                _ => return ShellToken::Amp,
661            }
662        }
663
664        if c == '|' {
665            self.next_char();
666            if self.peek() == Some('|') {
667                self.next_char();
668                return ShellToken::PipePipe;
669            }
670            return ShellToken::Pipe;
671        }
672
673        if c == '<' {
674            self.next_char();
675            match self.peek() {
676                Some('(') => {
677                    self.next_char();
678                    let cmd = self.read_process_sub();
679                    return ShellToken::Word(format!("<({}", cmd));
680                }
681                Some('<') => {
682                    self.next_char();
683                    if self.peek() == Some('<') {
684                        self.next_char();
685                        return ShellToken::LessLessLess;
686                    }
687                    return self.read_heredoc();
688                }
689                Some('>') => {
690                    self.next_char();
691                    return ShellToken::LessGreater;
692                }
693                Some('&') => {
694                    self.next_char();
695                    return ShellToken::LessAmp;
696                }
697                _ => return ShellToken::Less,
698            }
699        }
700
701        if c == '>' {
702            self.next_char();
703            match self.peek() {
704                Some('(') => {
705                    self.next_char();
706                    let cmd = self.read_process_sub();
707                    return ShellToken::Word(format!(">({}", cmd));
708                }
709                Some('>') => {
710                    self.next_char();
711                    return ShellToken::GreaterGreater;
712                }
713                Some('&') => {
714                    self.next_char();
715                    return ShellToken::GreaterAmp;
716                }
717                Some('|') => {
718                    self.next_char();
719                    return ShellToken::GreaterPipe;
720                }
721                _ => return ShellToken::Greater,
722            }
723        }
724
725        if c == '(' {
726            self.next_char();
727            if self.peek() == Some('(') {
728                self.next_char();
729                return ShellToken::DoubleLParen;
730            }
731            return ShellToken::LParen;
732        }
733
734        if c == ')' {
735            self.next_char();
736            if self.peek() == Some(')') {
737                self.next_char();
738                return ShellToken::DoubleRParen;
739            }
740            return ShellToken::RParen;
741        }
742
743        if c == '[' {
744            self.next_char();
745            if self.peek() == Some('[') {
746                self.next_char();
747                return ShellToken::DoubleLBracket;
748            }
749            if let Some(next_ch) = self.peek() {
750                if !next_ch.is_whitespace() && next_ch != ']' {
751                    let mut pattern = String::from("[");
752                    while let Some(ch) = self.peek() {
753                        pattern.push(self.next_char().unwrap());
754                        if ch == ']' {
755                            while let Some(c2) = self.peek() {
756                                if c2.is_whitespace()
757                                    || c2 == ';'
758                                    || c2 == '&'
759                                    || c2 == '|'
760                                    || c2 == '<'
761                                    || c2 == '>'
762                                    || c2 == ')'
763                                    || c2 == '\n'
764                                {
765                                    break;
766                                }
767                                pattern.push(self.next_char().unwrap());
768                            }
769                            return ShellToken::Word(pattern);
770                        }
771                        if ch.is_whitespace() {
772                            return ShellToken::Word(pattern);
773                        }
774                    }
775                    return ShellToken::Word(pattern);
776                }
777            }
778            return ShellToken::LBracket;
779        }
780
781        if c == ']' {
782            self.next_char();
783            if self.peek() == Some(']') {
784                self.next_char();
785                return ShellToken::DoubleRBracket;
786            }
787            return ShellToken::RBracket;
788        }
789
790        if c == '{' {
791            self.next_char();
792            match self.peek() {
793                Some(' ') | Some('\t') | Some('\n') | None => {
794                    return ShellToken::LBrace;
795                }
796                _ => {
797                    let mut word = String::from("{");
798                    let mut depth = 1;
799                    while let Some(ch) = self.peek() {
800                        if ch == '{' {
801                            depth += 1;
802                            word.push(self.next_char().unwrap());
803                        } else if ch == '}' {
804                            depth -= 1;
805                            word.push(self.next_char().unwrap());
806                            if depth == 0 {
807                                break;
808                            }
809                        } else if (ch == ' ' || ch == '\t' || ch == '\n') && depth == 1 {
810                            break;
811                        } else {
812                            word.push(self.next_char().unwrap());
813                        }
814                    }
815                    while let Some(ch) = self.peek() {
816                        if ch.is_whitespace()
817                            || ch == ';'
818                            || ch == '&'
819                            || ch == '|'
820                            || ch == '<'
821                            || ch == '>'
822                            || ch == '('
823                            || ch == ')'
824                        {
825                            break;
826                        }
827                        word.push(self.next_char().unwrap());
828                    }
829                    return ShellToken::Word(word);
830                }
831            }
832        }
833
834        if c == '}' {
835            self.next_char();
836            return ShellToken::RBrace;
837        }
838
839        if c == '!' {
840            self.next_char();
841            if self.peek() == Some('=') {
842                self.next_char();
843                return ShellToken::Word("!=".to_string());
844            }
845            if self.peek() == Some('(') {
846                let mut word = String::from("!(");
847                self.next_char();
848                let mut depth = 1;
849                while let Some(ch) = self.peek() {
850                    word.push(self.next_char().unwrap());
851                    if ch == '(' {
852                        depth += 1;
853                    } else if ch == ')' {
854                        depth -= 1;
855                        if depth == 0 {
856                            break;
857                        }
858                    }
859                }
860                while let Some(ch) = self.peek() {
861                    if ch.is_whitespace()
862                        || ch == ';'
863                        || ch == '&'
864                        || ch == '|'
865                        || ch == '<'
866                        || ch == '>'
867                    {
868                        break;
869                    }
870                    word.push(self.next_char().unwrap());
871                }
872                return ShellToken::Word(word);
873            }
874            return ShellToken::Bang;
875        }
876
877        if c.is_alphanumeric()
878            || c == '_'
879            || c == '/'
880            || c == '.'
881            || c == '-'
882            || c == '$'
883            || c == '\''
884            || c == '"'
885            || c == '~'
886            || c == '*'
887            || c == '?'
888            || c == '%'
889            || c == '+'
890            || c == '@'
891            || c == ':'
892            || c == '='
893            || c == '`'
894        {
895            return self.read_word();
896        }
897
898        self.next_char();
899        ShellToken::Word(c.to_string())
900    }
901
902    fn read_process_sub(&mut self) -> String {
903        let mut content = String::new();
904        let mut depth = 1;
905        while let Some(c) = self.next_char() {
906            if c == '(' {
907                depth += 1;
908                content.push(c);
909            } else if c == ')' {
910                depth -= 1;
911                if depth == 0 {
912                    content.push(')');
913                    break;
914                }
915                content.push(c);
916            } else {
917                content.push(c);
918            }
919        }
920        content
921    }
922
923    fn read_heredoc(&mut self) -> ShellToken {
924        while self.peek() == Some(' ') || self.peek() == Some('\t') {
925            self.next_char();
926        }
927        let quoted = self.peek() == Some('\'') || self.peek() == Some('"');
928        if quoted {
929            self.next_char();
930        }
931        let mut delimiter = String::new();
932        while let Some(c) = self.peek() {
933            if c == '\n' || c == ' ' || c == '\t' {
934                break;
935            }
936            if quoted && (c == '\'' || c == '"') {
937                self.next_char();
938                break;
939            }
940            delimiter.push(self.next_char().unwrap());
941        }
942        while let Some(c) = self.peek() {
943            if c == '\n' {
944                self.next_char();
945                break;
946            }
947            self.next_char();
948        }
949        let mut content = String::new();
950        let mut current_line = String::new();
951        while let Some(c) = self.next_char() {
952            if c == '\n' {
953                if current_line.trim() == delimiter {
954                    break;
955                }
956                content.push_str(&current_line);
957                content.push('\n');
958                current_line.clear();
959            } else {
960                current_line.push(c);
961            }
962        }
963        ShellToken::HereDoc(delimiter, content)
964    }
965
966    fn read_word(&mut self) -> ShellToken {
967        let mut word = String::new();
968        while let Some(c) = self.peek() {
969            match c {
970                ' ' | '\t' | '\n' | ';' | '&' | '<' | '>' => break,
971                '[' => {
972                    word.push(self.next_char().unwrap());
973                    let mut bracket_depth = 1;
974                    while let Some(ch) = self.peek() {
975                        word.push(self.next_char().unwrap());
976                        if ch == '[' {
977                            bracket_depth += 1;
978                        } else if ch == ']' {
979                            bracket_depth -= 1;
980                            if bracket_depth == 0 {
981                                break;
982                            }
983                        }
984                        if ch == ' ' || ch == '\t' || ch == '\n' {
985                            break;
986                        }
987                    }
988                }
989                ']' => {
990                    if word.is_empty() {
991                        break;
992                    }
993                    word.push(self.next_char().unwrap());
994                }
995                '|' | '(' | ')' => {
996                    if c == '(' && !word.is_empty() {
997                        let last_char = word.chars().last().unwrap();
998                        if matches!(last_char, '?' | '*' | '+' | '@' | '!') {
999                            word.push(self.next_char().unwrap());
1000                            let mut depth = 1;
1001                            while let Some(ch) = self.peek() {
1002                                word.push(self.next_char().unwrap());
1003                                if ch == '(' {
1004                                    depth += 1;
1005                                } else if ch == ')' {
1006                                    depth -= 1;
1007                                    if depth == 0 {
1008                                        break;
1009                                    }
1010                                }
1011                            }
1012                            continue;
1013                        }
1014                        if last_char == '=' {
1015                            word.push(self.next_char().unwrap());
1016                            let mut depth = 1;
1017                            let mut in_sq = false;
1018                            let mut in_dq = false;
1019                            while let Some(ch) = self.peek() {
1020                                if in_sq {
1021                                    if ch == '\'' {
1022                                        in_sq = false;
1023                                    }
1024                                    word.push(self.next_char().unwrap());
1025                                } else if in_dq {
1026                                    if ch == '"' {
1027                                        in_dq = false;
1028                                    } else if ch == '\\' {
1029                                        word.push(self.next_char().unwrap());
1030                                        if self.peek().is_some() {
1031                                            word.push(self.next_char().unwrap());
1032                                        }
1033                                        continue;
1034                                    }
1035                                    word.push(self.next_char().unwrap());
1036                                } else {
1037                                    match ch {
1038                                        '\'' => {
1039                                            in_sq = true;
1040                                            word.push(self.next_char().unwrap());
1041                                        }
1042                                        '"' => {
1043                                            in_dq = true;
1044                                            word.push(self.next_char().unwrap());
1045                                        }
1046                                        '(' => {
1047                                            depth += 1;
1048                                            word.push(self.next_char().unwrap());
1049                                        }
1050                                        ')' => {
1051                                            depth -= 1;
1052                                            word.push(self.next_char().unwrap());
1053                                            if depth == 0 {
1054                                                break;
1055                                            }
1056                                        }
1057                                        '\\' => {
1058                                            word.push(self.next_char().unwrap());
1059                                            if self.peek().is_some() {
1060                                                word.push(self.next_char().unwrap());
1061                                            }
1062                                        }
1063                                        _ => word.push(self.next_char().unwrap()),
1064                                    }
1065                                }
1066                            }
1067                            continue;
1068                        }
1069                    }
1070                    break;
1071                }
1072                '{' => {
1073                    word.push(self.next_char().unwrap());
1074                    let mut depth = 1;
1075                    while let Some(ch) = self.peek() {
1076                        if ch == '{' {
1077                            depth += 1;
1078                            word.push(self.next_char().unwrap());
1079                        } else if ch == '}' {
1080                            depth -= 1;
1081                            word.push(self.next_char().unwrap());
1082                            if depth == 0 {
1083                                break;
1084                            }
1085                        } else if ch == ' ' || ch == '\t' || ch == '\n' {
1086                            break;
1087                        } else {
1088                            word.push(self.next_char().unwrap());
1089                        }
1090                    }
1091                }
1092                '}' => break,
1093                '$' => {
1094                    word.push(self.next_char().unwrap());
1095                    if self.peek() == Some('\'') {
1096                        word.push(self.next_char().unwrap());
1097                        while let Some(ch) = self.peek() {
1098                            if ch == '\'' {
1099                                word.push(self.next_char().unwrap());
1100                                break;
1101                            } else if ch == '\\' {
1102                                word.push(self.next_char().unwrap());
1103                                if self.peek().is_some() {
1104                                    word.push(self.next_char().unwrap());
1105                                }
1106                            } else {
1107                                word.push(self.next_char().unwrap());
1108                            }
1109                        }
1110                    } else if self.peek() == Some('{') {
1111                        word.push(self.next_char().unwrap());
1112                        let mut depth = 1;
1113                        while let Some(ch) = self.peek() {
1114                            if ch == '{' {
1115                                depth += 1;
1116                            } else if ch == '}' {
1117                                depth -= 1;
1118                                if depth == 0 {
1119                                    word.push(self.next_char().unwrap());
1120                                    break;
1121                                }
1122                            }
1123                            word.push(self.next_char().unwrap());
1124                        }
1125                    } else if self.peek() == Some('(') {
1126                        word.push(self.next_char().unwrap());
1127                        let mut depth = 1;
1128                        while let Some(ch) = self.peek() {
1129                            if ch == '(' {
1130                                depth += 1;
1131                            } else if ch == ')' {
1132                                depth -= 1;
1133                                if depth == 0 {
1134                                    word.push(self.next_char().unwrap());
1135                                    break;
1136                                }
1137                            }
1138                            word.push(self.next_char().unwrap());
1139                        }
1140                    }
1141                }
1142                '=' => {
1143                    word.push(self.next_char().unwrap());
1144                    if self.peek() == Some('(') {
1145                        word.push(self.next_char().unwrap());
1146                        let mut depth = 1;
1147                        while let Some(ch) = self.peek() {
1148                            if ch == '(' {
1149                                depth += 1;
1150                            } else if ch == ')' {
1151                                depth -= 1;
1152                                if depth == 0 {
1153                                    word.push(self.next_char().unwrap());
1154                                    break;
1155                                }
1156                            }
1157                            word.push(self.next_char().unwrap());
1158                        }
1159                    }
1160                }
1161                '`' => {
1162                    // Backtick command substitution — keep backticks in the word
1163                    // so expand_string can see and execute them.
1164                    word.push(self.next_char().unwrap()); // opening `
1165                    while let Some(ch) = self.peek() {
1166                        if ch == '`' {
1167                            word.push(self.next_char().unwrap()); // closing `
1168                            break;
1169                        }
1170                        word.push(self.next_char().unwrap());
1171                    }
1172                }
1173                '\'' => {
1174                    self.next_char();
1175                    while let Some(ch) = self.peek() {
1176                        if ch == '\'' {
1177                            self.next_char();
1178                            break;
1179                        }
1180                        let c = self.next_char().unwrap();
1181                        if matches!(c, '`' | '$' | '(' | ')') {
1182                            word.push('\x00');
1183                        }
1184                        word.push(c);
1185                    }
1186                }
1187                '"' => {
1188                    self.next_char();
1189                    while let Some(ch) = self.peek() {
1190                        if ch == '"' {
1191                            self.next_char();
1192                            break;
1193                        }
1194                        if ch == '\\' {
1195                            self.next_char();
1196                            if let Some(escaped) = self.peek() {
1197                                match escaped {
1198                                    '$' | '`' | '"' | '\\' | '\n' => {
1199                                        word.push(self.next_char().unwrap());
1200                                    }
1201                                    _ => {
1202                                        word.push('\\');
1203                                        word.push(self.next_char().unwrap());
1204                                    }
1205                                }
1206                            } else {
1207                                word.push('\\');
1208                            }
1209                        } else {
1210                            word.push(self.next_char().unwrap());
1211                        }
1212                    }
1213                }
1214                '\\' => {
1215                    self.next_char();
1216                    if let Some(escaped) = self.next_char() {
1217                        word.push(escaped);
1218                    }
1219                }
1220                _ => {
1221                    word.push(self.next_char().unwrap());
1222                }
1223            }
1224        }
1225
1226        match word.as_str() {
1227            "if" => ShellToken::If,
1228            "then" => ShellToken::Then,
1229            "else" => ShellToken::Else,
1230            "elif" => ShellToken::Elif,
1231            "fi" => ShellToken::Fi,
1232            "case" => ShellToken::Case,
1233            "esac" => ShellToken::Esac,
1234            "for" => ShellToken::For,
1235            "while" => ShellToken::While,
1236            "until" => ShellToken::Until,
1237            "do" => ShellToken::Do,
1238            "done" => ShellToken::Done,
1239            "in" => ShellToken::In,
1240            "function" => ShellToken::Function,
1241            "select" => ShellToken::Select,
1242            "time" => ShellToken::Time,
1243            "coproc" => ShellToken::Coproc,
1244            "repeat" => ShellToken::Repeat,
1245            // "always" is NOT a keyword — it's context-dependent.
1246            // Checked as a string in parse_brace_group_or_try per C zsh par_subsh().
1247            "typeset" | "local" | "declare" | "export" | "readonly" | "integer" | "float" => {
1248                ShellToken::Typeset(word)
1249            }
1250            _ => ShellToken::Word(word),
1251        }
1252    }
1253}
1254
1255pub struct ShellParser<'a> {
1256    lexer: ShellLexer<'a>,
1257    current: ShellToken,
1258}
1259
1260impl<'a> ShellParser<'a> {
1261    pub fn new(input: &'a str) -> Self {
1262        let mut lexer = ShellLexer::new(input);
1263        let current = lexer.next_token();
1264        Self { lexer, current }
1265    }
1266
1267    fn parse_array_elements(content: &str) -> Vec<ShellWord> {
1268        let mut elements = Vec::new();
1269        let mut current = String::new();
1270        let mut chars = content.chars().peekable();
1271        let mut in_single_quote = false;
1272        let mut in_double_quote = false;
1273
1274        while let Some(c) = chars.next() {
1275            if in_single_quote {
1276                if c == '\'' {
1277                    in_single_quote = false;
1278                    let marked: String = current
1279                        .chars()
1280                        .flat_map(|ch| {
1281                            if matches!(ch, '`' | '$' | '(' | ')') {
1282                                vec!['\x00', ch]
1283                            } else {
1284                                vec![ch]
1285                            }
1286                        })
1287                        .collect();
1288                    elements.push(ShellWord::Literal(marked));
1289                    current.clear();
1290                } else {
1291                    current.push(c);
1292                }
1293            } else if in_double_quote {
1294                if c == '"' {
1295                    in_double_quote = false;
1296                    elements.push(ShellWord::Literal(current.clone()));
1297                    current.clear();
1298                } else if c == '\\' {
1299                    if let Some(&next) = chars.peek() {
1300                        if matches!(next, '$' | '`' | '"' | '\\') {
1301                            chars.next();
1302                            current.push(next);
1303                        } else {
1304                            current.push(c);
1305                        }
1306                    } else {
1307                        current.push(c);
1308                    }
1309                } else {
1310                    current.push(c);
1311                }
1312            } else {
1313                match c {
1314                    '\'' => in_single_quote = true,
1315                    '"' => in_double_quote = true,
1316                    '$' if chars.peek() == Some(&'(') => {
1317                        // Command substitution $(...)  — flush current, collect balanced parens
1318                        if !current.is_empty() {
1319                            elements.push(ShellWord::Literal(current.clone()));
1320                            current.clear();
1321                        }
1322                        chars.next(); // consume '('
1323                        let mut depth = 1;
1324                        let mut cmd = String::new();
1325                        while let Some(ch) = chars.next() {
1326                            if ch == '(' {
1327                                depth += 1;
1328                            }
1329                            if ch == ')' {
1330                                depth -= 1;
1331                                if depth == 0 {
1332                                    break;
1333                                }
1334                            }
1335                            cmd.push(ch);
1336                        }
1337                        // Parse the command substitution content into an AST
1338                        let mut sub_parser = ShellParser::new(&cmd);
1339                        if let Ok(cmds) = sub_parser.parse_script() {
1340                            if cmds.len() == 1 {
1341                                elements.push(ShellWord::CommandSub(Box::new(
1342                                    cmds.into_iter().next().unwrap(),
1343                                )));
1344                            } else if !cmds.is_empty() {
1345                                // Multiple commands — wrap in a brace group
1346                                elements.push(ShellWord::CommandSub(Box::new(
1347                                    ShellCommand::Compound(CompoundCommand::BraceGroup(cmds)),
1348                                )));
1349                            }
1350                        }
1351                    }
1352                    ' ' | '\t' | '\n' => {
1353                        if !current.is_empty() {
1354                            elements.push(ShellWord::Literal(current.clone()));
1355                            current.clear();
1356                        }
1357                    }
1358                    '\\' => {
1359                        if let Some(next) = chars.next() {
1360                            current.push(next);
1361                        }
1362                    }
1363                    _ => current.push(c),
1364                }
1365            }
1366        }
1367
1368        if !current.is_empty() {
1369            elements.push(ShellWord::Literal(current));
1370        }
1371
1372        elements
1373    }
1374
1375    fn advance(&mut self) -> ShellToken {
1376        std::mem::replace(&mut self.current, self.lexer.next_token())
1377    }
1378
1379    fn expect(&mut self, expected: ShellToken) -> Result<(), String> {
1380        if self.current == expected {
1381            self.advance();
1382            Ok(())
1383        } else {
1384            Err(format!("Expected {:?}, got {:?}", expected, self.current))
1385        }
1386    }
1387
1388    fn skip_newlines(&mut self) {
1389        while self.current == ShellToken::Newline {
1390            self.advance();
1391        }
1392    }
1393
1394    fn skip_separators(&mut self) {
1395        while self.current == ShellToken::Newline || self.current == ShellToken::Semi {
1396            self.advance();
1397        }
1398    }
1399
1400    #[tracing::instrument(level = "trace", skip_all)]
1401    pub fn parse_script(&mut self) -> Result<Vec<ShellCommand>, String> {
1402        let mut commands = Vec::new();
1403        self.skip_newlines();
1404        while self.current != ShellToken::Eof {
1405            if let Some(cmd) = self.parse_complete_command()? {
1406                commands.push(cmd);
1407            }
1408            self.skip_newlines();
1409        }
1410        Ok(commands)
1411    }
1412
1413    fn parse_complete_command(&mut self) -> Result<Option<ShellCommand>, String> {
1414        self.skip_newlines();
1415        if self.current == ShellToken::Eof {
1416            return Ok(None);
1417        }
1418        let cmd = self.parse_list()?;
1419        match &self.current {
1420            ShellToken::Newline | ShellToken::Semi | ShellToken::Amp => {
1421                self.advance();
1422            }
1423            _ => {}
1424        }
1425        Ok(Some(cmd))
1426    }
1427
1428    fn parse_list(&mut self) -> Result<ShellCommand, String> {
1429        let first = self.parse_pipeline()?;
1430        let mut items = vec![(first, ListOp::Semi)];
1431
1432        loop {
1433            let op = match &self.current {
1434                ShellToken::AmpAmp => ListOp::And,
1435                ShellToken::PipePipe => ListOp::Or,
1436                ShellToken::Semi => ListOp::Semi,
1437                ShellToken::Amp => ListOp::Amp,
1438                ShellToken::Newline => break,
1439                _ => break,
1440            };
1441
1442            self.advance();
1443            self.skip_newlines();
1444
1445            if let Some(last) = items.last_mut() {
1446                last.1 = op;
1447            }
1448
1449            if self.current == ShellToken::Eof
1450                || self.current == ShellToken::Then
1451                || self.current == ShellToken::Else
1452                || self.current == ShellToken::Elif
1453                || self.current == ShellToken::Fi
1454                || self.current == ShellToken::Do
1455                || self.current == ShellToken::Done
1456                || self.current == ShellToken::Esac
1457                || self.current == ShellToken::RBrace
1458                || self.current == ShellToken::RParen
1459            {
1460                break;
1461            }
1462
1463            let next = self.parse_pipeline()?;
1464            items.push((next, ListOp::Semi));
1465        }
1466
1467        if items.len() == 1 {
1468            let (cmd, op) = items.pop().unwrap();
1469            if matches!(op, ListOp::Amp) {
1470                Ok(ShellCommand::List(vec![(cmd, op)]))
1471            } else {
1472                Ok(cmd)
1473            }
1474        } else {
1475            Ok(ShellCommand::List(items))
1476        }
1477    }
1478
1479    fn parse_pipeline(&mut self) -> Result<ShellCommand, String> {
1480        let negated = if self.current == ShellToken::Bang {
1481            self.advance();
1482            true
1483        } else {
1484            false
1485        };
1486
1487        let first = self.parse_command()?;
1488        let mut cmds = vec![first];
1489
1490        while self.current == ShellToken::Pipe {
1491            self.advance();
1492            self.skip_newlines();
1493            cmds.push(self.parse_command()?);
1494        }
1495
1496        if cmds.len() == 1 && !negated {
1497            Ok(cmds.pop().unwrap())
1498        } else {
1499            Ok(ShellCommand::Pipeline(cmds, negated))
1500        }
1501    }
1502
1503    fn parse_command(&mut self) -> Result<ShellCommand, String> {
1504        let cmd = match &self.current {
1505            ShellToken::If => self.parse_if(),
1506            ShellToken::For => self.parse_for(),
1507            ShellToken::While => self.parse_while(),
1508            ShellToken::Until => self.parse_until(),
1509            ShellToken::Case => self.parse_case(),
1510            ShellToken::Repeat => self.parse_repeat(),
1511            ShellToken::LBrace => self.parse_brace_group_or_try(),
1512            ShellToken::LParen => {
1513                self.advance();
1514                if self.current == ShellToken::RParen {
1515                    self.advance();
1516                    self.skip_newlines();
1517                    if self.current == ShellToken::LBrace {
1518                        let body = self.parse_brace_group()?;
1519                        Ok(ShellCommand::FunctionDef(String::new(), Box::new(body)))
1520                    } else {
1521                        Ok(ShellCommand::Compound(CompoundCommand::Subshell(vec![])))
1522                    }
1523                } else {
1524                    self.skip_newlines();
1525                    let body = self.parse_compound_list()?;
1526                    self.expect(ShellToken::RParen)?;
1527                    Ok(ShellCommand::Compound(CompoundCommand::Subshell(body)))
1528                }
1529            }
1530            ShellToken::DoubleLBracket => self.parse_cond_command(),
1531            ShellToken::DoubleLParen => self.parse_arith_command(),
1532            ShellToken::Function => self.parse_function(),
1533            ShellToken::Coproc => self.parse_coproc(),
1534            _ => self.parse_simple_command(),
1535        }?;
1536
1537        let mut redirects = Vec::new();
1538        loop {
1539            if let ShellToken::Word(w) = &self.current {
1540                if w.chars().all(|c| c.is_ascii_digit()) {
1541                    let fd_str = w.clone();
1542                    self.advance();
1543                    match &self.current {
1544                        ShellToken::Less
1545                        | ShellToken::Greater
1546                        | ShellToken::GreaterGreater
1547                        | ShellToken::LessAmp
1548                        | ShellToken::GreaterAmp
1549                        | ShellToken::LessLess
1550                        | ShellToken::LessLessLess
1551                        | ShellToken::LessGreater
1552                        | ShellToken::GreaterPipe => {
1553                            let fd = fd_str.parse::<i32>().ok();
1554                            redirects.push(self.parse_redirect_with_fd(fd)?);
1555                            continue;
1556                        }
1557                        _ => break,
1558                    }
1559                }
1560            }
1561
1562            match &self.current {
1563                ShellToken::Less
1564                | ShellToken::Greater
1565                | ShellToken::GreaterGreater
1566                | ShellToken::LessAmp
1567                | ShellToken::GreaterAmp
1568                | ShellToken::LessLess
1569                | ShellToken::LessLessLess
1570                | ShellToken::LessGreater
1571                | ShellToken::GreaterPipe
1572                | ShellToken::AmpGreater
1573                | ShellToken::AmpGreaterGreater => {
1574                    redirects.push(self.parse_redirect_with_fd(None)?);
1575                }
1576                _ => break,
1577            }
1578        }
1579
1580        if !redirects.is_empty() {
1581            Ok(ShellCommand::Compound(CompoundCommand::WithRedirects(
1582                Box::new(cmd),
1583                redirects,
1584            )))
1585        } else {
1586            Ok(cmd)
1587        }
1588    }
1589
1590    fn parse_simple_command(&mut self) -> Result<ShellCommand, String> {
1591        let mut cmd = SimpleCommand {
1592            assignments: Vec::new(),
1593            words: Vec::new(),
1594            redirects: Vec::new(),
1595        };
1596
1597        loop {
1598            match &self.current {
1599                ShellToken::Word(w) => {
1600                    if w.starts_with('{') && w.ends_with('}') && w.len() > 2 {
1601                        let varname = w[1..w.len() - 1].to_string();
1602                        if varname.chars().all(|c| c.is_alphanumeric() || c == '_') {
1603                            let saved_word = w.clone();
1604                            self.advance();
1605                            match &self.current {
1606                                ShellToken::Less
1607                                | ShellToken::Greater
1608                                | ShellToken::GreaterGreater
1609                                | ShellToken::LessAmp
1610                                | ShellToken::GreaterAmp
1611                                | ShellToken::LessLess
1612                                | ShellToken::LessLessLess
1613                                | ShellToken::LessGreater
1614                                | ShellToken::GreaterPipe => {
1615                                    let mut redir = self.parse_redirect_with_fd(None)?;
1616                                    redir.fd_var = Some(varname);
1617                                    cmd.redirects.push(redir);
1618                                    continue;
1619                                }
1620                                _ => {
1621                                    cmd.words.push(ShellWord::Literal(saved_word));
1622                                    continue;
1623                                }
1624                            }
1625                        }
1626                    }
1627
1628                    if w.chars().all(|c| c.is_ascii_digit()) {
1629                        let fd_str = w.clone();
1630                        self.advance();
1631                        match &self.current {
1632                            ShellToken::Less
1633                            | ShellToken::Greater
1634                            | ShellToken::GreaterGreater
1635                            | ShellToken::LessAmp
1636                            | ShellToken::GreaterAmp
1637                            | ShellToken::LessLess
1638                            | ShellToken::LessLessLess
1639                            | ShellToken::LessGreater
1640                            | ShellToken::GreaterPipe => {
1641                                let fd = fd_str.parse::<i32>().ok();
1642                                cmd.redirects.push(self.parse_redirect_with_fd(fd)?);
1643                                continue;
1644                            }
1645                            _ => {
1646                                cmd.words.push(ShellWord::Literal(fd_str));
1647                                continue;
1648                            }
1649                        }
1650                    }
1651
1652                    if cmd.words.is_empty() && w.contains('=') && !w.starts_with('=') {
1653                        let (eq_pos, is_append) = if let Some(pos) = w.find("+=") {
1654                            (pos, true)
1655                        } else if let Some(pos) = w.find('=') {
1656                            (pos, false)
1657                        } else {
1658                            (0, false)
1659                        };
1660
1661                        if eq_pos > 0 {
1662                            let var = w[..eq_pos].to_string();
1663                            let val_start = if is_append { eq_pos + 2 } else { eq_pos + 1 };
1664                            let val = w[val_start..].to_string();
1665
1666                            let is_valid_var = if let Some(bracket_pos) = var.find('[') {
1667                                let name = &var[..bracket_pos];
1668                                let rest = &var[bracket_pos..];
1669                                name.chars().all(|c| c.is_alphanumeric() || c == '_')
1670                                    && rest.ends_with(']')
1671                            } else {
1672                                var.chars().all(|c| c.is_alphanumeric() || c == '_')
1673                            };
1674                            if is_valid_var {
1675                                if val.starts_with('(') && val.ends_with(')') {
1676                                    let array_content = &val[1..val.len() - 1];
1677                                    let elements = Self::parse_array_elements(array_content);
1678                                    cmd.assignments.push((
1679                                        var,
1680                                        ShellWord::ArrayLiteral(elements),
1681                                        is_append,
1682                                    ));
1683                                } else {
1684                                    cmd.assignments
1685                                        .push((var, ShellWord::Literal(val), is_append));
1686                                }
1687                                self.advance();
1688                                continue;
1689                            }
1690                        }
1691                    }
1692
1693                    cmd.words.push(self.parse_word()?);
1694                }
1695
1696                ShellToken::SingleQuotedWord(_) | ShellToken::DoubleQuotedWord(_) => {
1697                    cmd.words.push(self.parse_word()?);
1698                }
1699
1700                ShellToken::LBracket => {
1701                    cmd.words.push(ShellWord::Literal("[".to_string()));
1702                    self.advance();
1703                }
1704                ShellToken::RBracket => {
1705                    if !cmd.words.is_empty() {
1706                        cmd.words.push(ShellWord::Literal("]".to_string()));
1707                        self.advance();
1708                    } else {
1709                        break;
1710                    }
1711                }
1712                ShellToken::If
1713                | ShellToken::Then
1714                | ShellToken::Else
1715                | ShellToken::Elif
1716                | ShellToken::Fi
1717                | ShellToken::Case
1718                | ShellToken::Esac
1719                | ShellToken::For
1720                | ShellToken::While
1721                | ShellToken::Until
1722                | ShellToken::Do
1723                | ShellToken::Done
1724                | ShellToken::In
1725                | ShellToken::Function
1726                | ShellToken::Select
1727                | ShellToken::Time
1728                | ShellToken::Coproc
1729                | ShellToken::Repeat => {
1730                    if !cmd.words.is_empty() {
1731                        cmd.words.push(self.parse_word()?);
1732                    } else {
1733                        break;
1734                    }
1735                }
1736
1737                ShellToken::Typeset(ref name) => {
1738                    if cmd.words.is_empty() {
1739                        let name = name.clone();
1740                        cmd.words.push(ShellWord::Literal(name));
1741                        self.advance();
1742                    } else {
1743                        cmd.words.push(self.parse_word()?);
1744                    }
1745                }
1746
1747                ShellToken::Less
1748                | ShellToken::Greater
1749                | ShellToken::GreaterGreater
1750                | ShellToken::LessAmp
1751                | ShellToken::GreaterAmp
1752                | ShellToken::LessLess
1753                | ShellToken::LessLessLess
1754                | ShellToken::LessGreater
1755                | ShellToken::GreaterPipe
1756                | ShellToken::AmpGreater
1757                | ShellToken::AmpGreaterGreater
1758                | ShellToken::HereDoc(_, _) => {
1759                    cmd.redirects.push(self.parse_redirect_with_fd(None)?);
1760                }
1761
1762                _ => break,
1763            }
1764        }
1765
1766        if cmd.words.len() == 1 && self.current == ShellToken::LParen {
1767            if let ShellWord::Literal(name) = &cmd.words[0] {
1768                let name = name.clone();
1769                self.advance();
1770                self.expect(ShellToken::RParen)?;
1771                self.skip_newlines();
1772                let body = self.parse_command()?;
1773                return Ok(ShellCommand::FunctionDef(name, Box::new(body)));
1774            }
1775        }
1776
1777        Ok(ShellCommand::Simple(cmd))
1778    }
1779
1780    fn parse_word(&mut self) -> Result<ShellWord, String> {
1781        let token = self.advance();
1782        match token {
1783            ShellToken::Word(w) => Ok(ShellWord::Literal(w)),
1784            ShellToken::SingleQuotedWord(s) => Ok(ShellWord::SingleQuoted(s)),
1785            ShellToken::DoubleQuotedWord(s) => Ok(self.parse_double_quoted_contents(&s)),
1786            ShellToken::LBracket => Ok(ShellWord::Literal("[".to_string())),
1787            ShellToken::If => Ok(ShellWord::Literal("if".to_string())),
1788            ShellToken::Then => Ok(ShellWord::Literal("then".to_string())),
1789            ShellToken::Else => Ok(ShellWord::Literal("else".to_string())),
1790            ShellToken::Elif => Ok(ShellWord::Literal("elif".to_string())),
1791            ShellToken::Fi => Ok(ShellWord::Literal("fi".to_string())),
1792            ShellToken::Case => Ok(ShellWord::Literal("case".to_string())),
1793            ShellToken::Esac => Ok(ShellWord::Literal("esac".to_string())),
1794            ShellToken::For => Ok(ShellWord::Literal("for".to_string())),
1795            ShellToken::While => Ok(ShellWord::Literal("while".to_string())),
1796            ShellToken::Until => Ok(ShellWord::Literal("until".to_string())),
1797            ShellToken::Do => Ok(ShellWord::Literal("do".to_string())),
1798            ShellToken::Done => Ok(ShellWord::Literal("done".to_string())),
1799            ShellToken::In => Ok(ShellWord::Literal("in".to_string())),
1800            ShellToken::Function => Ok(ShellWord::Literal("function".to_string())),
1801            ShellToken::Select => Ok(ShellWord::Literal("select".to_string())),
1802            ShellToken::Time => Ok(ShellWord::Literal("time".to_string())),
1803            ShellToken::Coproc => Ok(ShellWord::Literal("coproc".to_string())),
1804            ShellToken::Typeset(name) => Ok(ShellWord::Literal(name)),
1805            ShellToken::Repeat => Ok(ShellWord::Literal("repeat".to_string())),
1806            _ => Err("Expected word".to_string()),
1807        }
1808    }
1809
1810    fn parse_double_quoted_contents(&self, s: &str) -> ShellWord {
1811        let mut parts = Vec::new();
1812        let mut literal = String::new();
1813        let chars: Vec<char> = s.chars().collect();
1814        let mut i = 0;
1815        while i < chars.len() {
1816            if chars[i] == '$' {
1817                if !literal.is_empty() {
1818                    parts.push(ShellWord::Literal(std::mem::take(&mut literal)));
1819                }
1820                i += 1;
1821                if i >= chars.len() {
1822                    literal.push('$');
1823                    continue;
1824                }
1825                if chars[i] == '{' {
1826                    i += 1;
1827                    let mut var_name = String::new();
1828                    while i < chars.len() && chars[i] != '}' {
1829                        var_name.push(chars[i]);
1830                        i += 1;
1831                    }
1832                    if i < chars.len() {
1833                        i += 1;
1834                    }
1835                    parts.push(ShellWord::VariableBraced(var_name, None));
1836                } else if chars[i].is_ascii_alphabetic() || chars[i] == '_' {
1837                    let mut var_name = String::new();
1838                    while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
1839                        var_name.push(chars[i]);
1840                        i += 1;
1841                    }
1842                    parts.push(ShellWord::Variable(var_name));
1843                } else {
1844                    literal.push('$');
1845                }
1846            } else if chars[i] == '\\' && i + 1 < chars.len() {
1847                i += 1;
1848                match chars[i] {
1849                    'n' => literal.push('\n'),
1850                    't' => literal.push('\t'),
1851                    'r' => literal.push('\r'),
1852                    '\\' => literal.push('\\'),
1853                    '"' => literal.push('"'),
1854                    '$' => literal.push('$'),
1855                    '`' => literal.push('`'),
1856                    c => {
1857                        literal.push('\\');
1858                        literal.push(c);
1859                    }
1860                }
1861                i += 1;
1862            } else {
1863                literal.push(chars[i]);
1864                i += 1;
1865            }
1866        }
1867        if !literal.is_empty() {
1868            parts.push(ShellWord::Literal(literal));
1869        }
1870        if parts.is_empty() {
1871            ShellWord::Literal(String::new())
1872        } else if parts.len() == 1 {
1873            parts.pop().unwrap()
1874        } else {
1875            ShellWord::DoubleQuoted(parts)
1876        }
1877    }
1878
1879    fn parse_redirect_with_fd(&mut self, fd: Option<i32>) -> Result<Redirect, String> {
1880        if let ShellToken::HereDoc(delimiter, content) = &self.current {
1881            let delimiter = delimiter.clone();
1882            let content = content.clone();
1883            self.advance();
1884            return Ok(Redirect {
1885                fd,
1886                op: RedirectOp::HereDoc,
1887                target: ShellWord::Literal(delimiter),
1888                heredoc_content: Some(content),
1889                fd_var: None,
1890            });
1891        }
1892
1893        let mut fd_var = None;
1894        if let ShellToken::Word(w) = &self.current {
1895            if w.starts_with('{') && w.ends_with('}') && w.len() > 2 {
1896                let varname = w[1..w.len() - 1].to_string();
1897                fd_var = Some(varname);
1898                self.advance();
1899            }
1900        }
1901
1902        let op = match self.advance() {
1903            ShellToken::Less => RedirectOp::Read,
1904            ShellToken::Greater => RedirectOp::Write,
1905            ShellToken::GreaterGreater => RedirectOp::Append,
1906            ShellToken::LessAmp => RedirectOp::DupRead,
1907            ShellToken::GreaterAmp => RedirectOp::DupWrite,
1908            ShellToken::LessLess => RedirectOp::HereDoc,
1909            ShellToken::LessLessLess => RedirectOp::HereString,
1910            ShellToken::LessGreater => RedirectOp::ReadWrite,
1911            ShellToken::GreaterPipe => RedirectOp::Clobber,
1912            ShellToken::AmpGreater => RedirectOp::WriteBoth,
1913            ShellToken::AmpGreaterGreater => RedirectOp::AppendBoth,
1914            _ => return Err("Expected redirect operator".to_string()),
1915        };
1916
1917        let target = self.parse_word()?;
1918
1919        Ok(Redirect {
1920            fd,
1921            op,
1922            target,
1923            heredoc_content: None,
1924            fd_var,
1925        })
1926    }
1927
1928    fn parse_if(&mut self) -> Result<ShellCommand, String> {
1929        let mut conditions = Vec::new();
1930        let mut else_part = None;
1931        let mut usebrace = false;
1932
1933        let mut xtok = self.current.clone();
1934        loop {
1935            if xtok == ShellToken::Fi {
1936                self.advance();
1937                break;
1938            }
1939
1940            self.advance();
1941
1942            if xtok == ShellToken::Else {
1943                break;
1944            }
1945
1946            self.skip_separators();
1947
1948            if xtok != ShellToken::If && xtok != ShellToken::Elif {
1949                return Err(format!("Expected If or Elif, got {:?}", xtok));
1950            }
1951
1952            let cond = self.parse_compound_list_until(&[ShellToken::Then, ShellToken::LBrace])?;
1953            self.skip_separators();
1954            xtok = ShellToken::Fi;
1955
1956            if self.current == ShellToken::Then {
1957                usebrace = false;
1958                self.advance();
1959                let body = self.parse_compound_list()?;
1960                conditions.push((cond, body));
1961            } else if self.current == ShellToken::LBrace {
1962                usebrace = true;
1963                self.advance();
1964                self.skip_separators();
1965                let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
1966                if self.current != ShellToken::RBrace {
1967                    return Err(format!("Expected RBrace, got {:?}", self.current));
1968                }
1969                conditions.push((cond, body));
1970                self.advance();
1971                if self.current == ShellToken::Newline || self.current == ShellToken::Semi {
1972                    break;
1973                }
1974            } else {
1975                return Err(format!(
1976                    "Expected Then or LBrace after condition, got {:?}",
1977                    self.current
1978                ));
1979            }
1980
1981            xtok = self.current.clone();
1982            if xtok != ShellToken::Elif && xtok != ShellToken::Else && xtok != ShellToken::Fi {
1983                break;
1984            }
1985        }
1986
1987        if xtok == ShellToken::Else || self.current == ShellToken::Else {
1988            if self.current == ShellToken::Else {
1989                self.advance();
1990            }
1991            self.skip_separators();
1992
1993            if self.current == ShellToken::LBrace && usebrace {
1994                self.advance();
1995                self.skip_separators();
1996                let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
1997                if self.current != ShellToken::RBrace {
1998                    return Err(format!("Expected RBrace in else, got {:?}", self.current));
1999                }
2000                self.advance();
2001                else_part = Some(body);
2002            } else {
2003                let body = self.parse_compound_list()?;
2004                if self.current != ShellToken::Fi {
2005                    return Err(format!("Expected Fi, got {:?}", self.current));
2006                }
2007                self.advance();
2008                else_part = Some(body);
2009            }
2010        }
2011
2012        Ok(ShellCommand::Compound(CompoundCommand::If {
2013            conditions,
2014            else_part,
2015        }))
2016    }
2017
2018    fn parse_for(&mut self) -> Result<ShellCommand, String> {
2019        self.expect(ShellToken::For)?;
2020        self.skip_newlines();
2021
2022        if self.current == ShellToken::DoubleLParen {
2023            return self.parse_for_arith();
2024        }
2025
2026        let var = if let ShellToken::Word(w) = self.advance() {
2027            w
2028        } else {
2029            return Err("Expected variable name after 'for'".to_string());
2030        };
2031
2032        while self.current == ShellToken::Newline {
2033            self.advance();
2034        }
2035
2036        let words = if self.current == ShellToken::In {
2037            self.advance();
2038            let mut words = Vec::new();
2039            while let ShellToken::Word(_) = &self.current {
2040                words.push(self.parse_word()?);
2041            }
2042            Some(words)
2043        } else if self.current == ShellToken::LParen {
2044            self.advance();
2045            let mut words = Vec::new();
2046            while self.current != ShellToken::RParen && self.current != ShellToken::Eof {
2047                if let ShellToken::Word(_) = &self.current {
2048                    words.push(self.parse_word()?);
2049                } else if self.current == ShellToken::Newline {
2050                    self.advance();
2051                } else {
2052                    break;
2053                }
2054            }
2055            self.expect(ShellToken::RParen)?;
2056            Some(words)
2057        } else {
2058            None
2059        };
2060
2061        self.skip_separators();
2062
2063        let body = if self.current == ShellToken::LBrace {
2064            self.advance();
2065            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
2066            self.expect(ShellToken::RBrace)?;
2067            body
2068        } else {
2069            self.expect(ShellToken::Do)?;
2070            let body = self.parse_compound_list()?;
2071            self.expect(ShellToken::Done)?;
2072            body
2073        };
2074
2075        Ok(ShellCommand::Compound(CompoundCommand::For {
2076            var,
2077            words,
2078            body,
2079        }))
2080    }
2081
2082    fn parse_for_arith(&mut self) -> Result<ShellCommand, String> {
2083        self.expect(ShellToken::DoubleLParen)?;
2084
2085        let mut parts = Vec::new();
2086        let mut current_part = String::new();
2087        let mut depth = 0;
2088
2089        loop {
2090            match &self.current {
2091                ShellToken::DoubleRParen if depth == 0 => break,
2092                ShellToken::DoubleLParen => {
2093                    depth += 1;
2094                    current_part.push_str("((");
2095                    self.advance();
2096                }
2097                ShellToken::DoubleRParen => {
2098                    depth -= 1;
2099                    current_part.push_str("))");
2100                    self.advance();
2101                }
2102                ShellToken::Semi => {
2103                    parts.push(current_part.trim().to_string());
2104                    current_part = String::new();
2105                    self.advance();
2106                }
2107                ShellToken::Word(w) => {
2108                    current_part.push_str(w);
2109                    current_part.push(' ');
2110                    self.advance();
2111                }
2112                ShellToken::Less => {
2113                    current_part.push('<');
2114                    self.advance();
2115                }
2116                ShellToken::Greater => {
2117                    current_part.push('>');
2118                    self.advance();
2119                }
2120                ShellToken::LessLess => {
2121                    current_part.push_str("<<");
2122                    self.advance();
2123                }
2124                ShellToken::GreaterGreater => {
2125                    current_part.push_str(">>");
2126                    self.advance();
2127                }
2128                _ => {
2129                    self.advance();
2130                }
2131            }
2132        }
2133        parts.push(current_part.trim().to_string());
2134
2135        self.expect(ShellToken::DoubleRParen)?;
2136        self.skip_newlines();
2137
2138        match &self.current {
2139            ShellToken::Semi | ShellToken::Newline => {
2140                self.advance();
2141            }
2142            _ => {}
2143        }
2144        self.skip_newlines();
2145
2146        self.expect(ShellToken::Do)?;
2147        self.skip_newlines();
2148        let body = self.parse_compound_list()?;
2149        self.expect(ShellToken::Done)?;
2150
2151        Ok(ShellCommand::Compound(CompoundCommand::ForArith {
2152            init: parts.first().cloned().unwrap_or_default(),
2153            cond: parts.get(1).cloned().unwrap_or_default(),
2154            step: parts.get(2).cloned().unwrap_or_default(),
2155            body,
2156        }))
2157    }
2158
2159    fn parse_while(&mut self) -> Result<ShellCommand, String> {
2160        self.expect(ShellToken::While)?;
2161        let condition = self.parse_compound_list_until(&[ShellToken::Do, ShellToken::LBrace])?;
2162        self.skip_separators();
2163
2164        let body = if self.current == ShellToken::LBrace {
2165            self.advance();
2166            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
2167            self.expect(ShellToken::RBrace)?;
2168            body
2169        } else {
2170            self.expect(ShellToken::Do)?;
2171            let body = self.parse_compound_list()?;
2172            self.expect(ShellToken::Done)?;
2173            body
2174        };
2175
2176        Ok(ShellCommand::Compound(CompoundCommand::While {
2177            condition,
2178            body,
2179        }))
2180    }
2181
2182    fn parse_until(&mut self) -> Result<ShellCommand, String> {
2183        self.expect(ShellToken::Until)?;
2184        let condition = self.parse_compound_list_until(&[ShellToken::Do, ShellToken::LBrace])?;
2185        self.skip_separators();
2186
2187        let body = if self.current == ShellToken::LBrace {
2188            self.advance();
2189            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
2190            self.expect(ShellToken::RBrace)?;
2191            body
2192        } else {
2193            self.expect(ShellToken::Do)?;
2194            let body = self.parse_compound_list()?;
2195            self.expect(ShellToken::Done)?;
2196            body
2197        };
2198
2199        Ok(ShellCommand::Compound(CompoundCommand::Until {
2200            condition,
2201            body,
2202        }))
2203    }
2204
2205    fn parse_case(&mut self) -> Result<ShellCommand, String> {
2206        self.expect(ShellToken::Case)?;
2207        self.skip_newlines();
2208        let word = self.parse_word()?;
2209        self.skip_newlines();
2210        self.expect(ShellToken::In)?;
2211        self.skip_newlines();
2212
2213        let mut cases = Vec::new();
2214
2215        while self.current != ShellToken::Esac {
2216            let mut patterns = Vec::new();
2217            if self.current == ShellToken::LParen {
2218                self.advance();
2219            }
2220
2221            loop {
2222                patterns.push(self.parse_word()?);
2223                if self.current == ShellToken::Pipe {
2224                    self.advance();
2225                } else {
2226                    break;
2227                }
2228            }
2229
2230            self.expect(ShellToken::RParen)?;
2231            self.skip_newlines();
2232
2233            let body = self.parse_compound_list()?;
2234
2235            let term = match &self.current {
2236                ShellToken::DoubleSemi => {
2237                    self.advance();
2238                    CaseTerminator::Break
2239                }
2240                ShellToken::SemiAmp => {
2241                    self.advance();
2242                    CaseTerminator::Fallthrough
2243                }
2244                ShellToken::SemiSemiAmp => {
2245                    self.advance();
2246                    CaseTerminator::Continue
2247                }
2248                _ => CaseTerminator::Break,
2249            };
2250
2251            cases.push((patterns, body, term));
2252            self.skip_newlines();
2253        }
2254
2255        self.expect(ShellToken::Esac)?;
2256
2257        Ok(ShellCommand::Compound(CompoundCommand::Case {
2258            word,
2259            cases,
2260        }))
2261    }
2262
2263    fn parse_repeat(&mut self) -> Result<ShellCommand, String> {
2264        self.expect(ShellToken::Repeat)?;
2265
2266        let count = match &self.current {
2267            ShellToken::Word(w) => {
2268                let c = w.clone();
2269                self.advance();
2270                c
2271            }
2272            _ => return Err("expected count after 'repeat'".to_string()),
2273        };
2274
2275        self.skip_separators();
2276
2277        let body = if self.current == ShellToken::LBrace {
2278            self.advance();
2279            self.skip_newlines();
2280            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
2281            self.expect(ShellToken::RBrace)?;
2282            body
2283        } else if self.current == ShellToken::Do {
2284            self.expect(ShellToken::Do)?;
2285            let body = self.parse_compound_list()?;
2286            self.expect(ShellToken::Done)?;
2287            body
2288        } else {
2289            // repeat N SIMPLE_COMMAND (no braces or do/done needed)
2290            let cmd = self.parse_command()?;
2291            vec![cmd]
2292        };
2293
2294        Ok(ShellCommand::Compound(CompoundCommand::Repeat {
2295            count,
2296            body,
2297        }))
2298    }
2299
2300    fn parse_brace_group_or_try(&mut self) -> Result<ShellCommand, String> {
2301        self.expect(ShellToken::LBrace)?;
2302        self.skip_newlines();
2303        let try_body = self.parse_compound_list()?;
2304        self.expect(ShellToken::RBrace)?;
2305
2306        // C zsh: tok == STRING && !strcmp(tokstr, "always")
2307        // "always" is context-dependent, only keyword after OUTBRACE.
2308        if matches!(&self.current, ShellToken::Word(w) if w == "always") {
2309            self.advance();
2310            self.expect(ShellToken::LBrace)?;
2311            self.skip_newlines();
2312            let always_body = self.parse_compound_list()?;
2313            self.expect(ShellToken::RBrace)?;
2314
2315            Ok(ShellCommand::Compound(CompoundCommand::Try {
2316                try_body,
2317                always_body,
2318            }))
2319        } else {
2320            Ok(ShellCommand::Compound(CompoundCommand::BraceGroup(
2321                try_body,
2322            )))
2323        }
2324    }
2325
2326    fn parse_brace_group(&mut self) -> Result<ShellCommand, String> {
2327        self.expect(ShellToken::LBrace)?;
2328        self.skip_newlines();
2329        let body = self.parse_compound_list()?;
2330        self.expect(ShellToken::RBrace)?;
2331
2332        Ok(ShellCommand::Compound(CompoundCommand::BraceGroup(body)))
2333    }
2334
2335    fn parse_cond_command(&mut self) -> Result<ShellCommand, String> {
2336        self.expect(ShellToken::DoubleLBracket)?;
2337
2338        let mut tokens: Vec<String> = Vec::new();
2339        while self.current != ShellToken::DoubleRBracket && self.current != ShellToken::Eof {
2340            match &self.current {
2341                ShellToken::Word(w) => tokens.push(w.clone()),
2342                ShellToken::Bang => tokens.push("!".to_string()),
2343                ShellToken::AmpAmp => tokens.push("&&".to_string()),
2344                ShellToken::PipePipe => tokens.push("||".to_string()),
2345                ShellToken::LParen => tokens.push("(".to_string()),
2346                ShellToken::RParen => tokens.push(")".to_string()),
2347                ShellToken::Less => tokens.push("<".to_string()),
2348                ShellToken::Greater => tokens.push(">".to_string()),
2349                _ => {}
2350            }
2351            self.advance();
2352        }
2353
2354        self.expect(ShellToken::DoubleRBracket)?;
2355
2356        let expr = self.parse_cond_tokens(&tokens)?;
2357        Ok(ShellCommand::Compound(CompoundCommand::Cond(expr)))
2358    }
2359
2360    fn parse_cond_tokens(&self, tokens: &[String]) -> Result<CondExpr, String> {
2361        if tokens.is_empty() {
2362            return Ok(CondExpr::StringNonEmpty(ShellWord::Literal(String::new())));
2363        }
2364
2365        if tokens[0] == "!" {
2366            let inner = self.parse_cond_tokens(&tokens[1..])?;
2367            return Ok(CondExpr::Not(Box::new(inner)));
2368        }
2369
2370        // Precedence: || (lowest) > && > comparisons (highest).
2371        // Scan for || first, then &&, then binary operators.
2372        // This matches the C implementation's precedence in cond.c.
2373
2374        // Level 1: || (lowest precedence — split on rightmost to get left-associativity)
2375        for i in (0..tokens.len()).rev() {
2376            if tokens[i] == "||" {
2377                let left = self.parse_cond_tokens(&tokens[..i])?;
2378                let right = self.parse_cond_tokens(&tokens[i + 1..])?;
2379                return Ok(CondExpr::Or(Box::new(left), Box::new(right)));
2380            }
2381        }
2382
2383        // Level 2: &&
2384        for i in (0..tokens.len()).rev() {
2385            if tokens[i] == "&&" {
2386                let left = self.parse_cond_tokens(&tokens[..i])?;
2387                let right = self.parse_cond_tokens(&tokens[i + 1..])?;
2388                return Ok(CondExpr::And(Box::new(left), Box::new(right)));
2389            }
2390        }
2391
2392        // Level 3: binary comparison operators (highest precedence)
2393        for (i, tok) in tokens.iter().enumerate() {
2394            match tok.as_str() {
2395                "=" | "==" => {
2396                    let left = tokens[..i].join(" ");
2397                    let right = tokens[i + 1..].join(" ");
2398                    return Ok(CondExpr::StringEqual(
2399                        ShellWord::Literal(left),
2400                        ShellWord::Literal(right),
2401                    ));
2402                }
2403                "!=" => {
2404                    let left = tokens[..i].join(" ");
2405                    let right = tokens[i + 1..].join(" ");
2406                    return Ok(CondExpr::StringNotEqual(
2407                        ShellWord::Literal(left),
2408                        ShellWord::Literal(right),
2409                    ));
2410                }
2411                "=~" => {
2412                    let left = tokens[..i].join(" ");
2413                    let right = tokens[i + 1..].join(" ");
2414                    return Ok(CondExpr::StringMatch(
2415                        ShellWord::Literal(left),
2416                        ShellWord::Literal(right),
2417                    ));
2418                }
2419                "-eq" => {
2420                    let left = tokens[..i].join(" ");
2421                    let right = tokens[i + 1..].join(" ");
2422                    return Ok(CondExpr::NumEqual(
2423                        ShellWord::Literal(left),
2424                        ShellWord::Literal(right),
2425                    ));
2426                }
2427                "-ne" => {
2428                    let left = tokens[..i].join(" ");
2429                    let right = tokens[i + 1..].join(" ");
2430                    return Ok(CondExpr::NumNotEqual(
2431                        ShellWord::Literal(left),
2432                        ShellWord::Literal(right),
2433                    ));
2434                }
2435                "-lt" => {
2436                    let left = tokens[..i].join(" ");
2437                    let right = tokens[i + 1..].join(" ");
2438                    return Ok(CondExpr::NumLess(
2439                        ShellWord::Literal(left),
2440                        ShellWord::Literal(right),
2441                    ));
2442                }
2443                "-le" => {
2444                    let left = tokens[..i].join(" ");
2445                    let right = tokens[i + 1..].join(" ");
2446                    return Ok(CondExpr::NumLessEqual(
2447                        ShellWord::Literal(left),
2448                        ShellWord::Literal(right),
2449                    ));
2450                }
2451                "-gt" => {
2452                    let left = tokens[..i].join(" ");
2453                    let right = tokens[i + 1..].join(" ");
2454                    return Ok(CondExpr::NumGreater(
2455                        ShellWord::Literal(left),
2456                        ShellWord::Literal(right),
2457                    ));
2458                }
2459                "-ge" => {
2460                    let left = tokens[..i].join(" ");
2461                    let right = tokens[i + 1..].join(" ");
2462                    return Ok(CondExpr::NumGreaterEqual(
2463                        ShellWord::Literal(left),
2464                        ShellWord::Literal(right),
2465                    ));
2466                }
2467                "<" => {
2468                    let left = tokens[..i].join(" ");
2469                    let right = tokens[i + 1..].join(" ");
2470                    return Ok(CondExpr::StringLess(
2471                        ShellWord::Literal(left),
2472                        ShellWord::Literal(right),
2473                    ));
2474                }
2475                ">" => {
2476                    let left = tokens[..i].join(" ");
2477                    let right = tokens[i + 1..].join(" ");
2478                    return Ok(CondExpr::StringGreater(
2479                        ShellWord::Literal(left),
2480                        ShellWord::Literal(right),
2481                    ));
2482                }
2483                _ => {}
2484            }
2485        }
2486
2487        if tokens.len() >= 2 {
2488            let op = &tokens[0];
2489            let arg = tokens[1..].join(" ");
2490            match op.as_str() {
2491                "-e" => return Ok(CondExpr::FileExists(ShellWord::Literal(arg))),
2492                "-f" => return Ok(CondExpr::FileRegular(ShellWord::Literal(arg))),
2493                "-d" => return Ok(CondExpr::FileDirectory(ShellWord::Literal(arg))),
2494                "-L" | "-h" => return Ok(CondExpr::FileSymlink(ShellWord::Literal(arg))),
2495                "-r" => return Ok(CondExpr::FileReadable(ShellWord::Literal(arg))),
2496                "-w" => return Ok(CondExpr::FileWritable(ShellWord::Literal(arg))),
2497                "-x" => return Ok(CondExpr::FileExecutable(ShellWord::Literal(arg))),
2498                "-s" => return Ok(CondExpr::FileNonEmpty(ShellWord::Literal(arg))),
2499                "-z" => return Ok(CondExpr::StringEmpty(ShellWord::Literal(arg))),
2500                "-n" => return Ok(CondExpr::StringNonEmpty(ShellWord::Literal(arg))),
2501                _ => {}
2502            }
2503        }
2504
2505        let expr_str = tokens.join(" ");
2506        Ok(CondExpr::StringNonEmpty(ShellWord::Literal(expr_str)))
2507    }
2508
2509    fn parse_arith_command(&mut self) -> Result<ShellCommand, String> {
2510        self.expect(ShellToken::DoubleLParen)?;
2511
2512        let mut expr = String::new();
2513        let mut depth = 1;
2514
2515        while depth > 0 {
2516            match &self.current {
2517                ShellToken::DoubleLParen => {
2518                    depth += 1;
2519                    expr.push_str("((");
2520                }
2521                ShellToken::DoubleRParen => {
2522                    depth -= 1;
2523                    if depth > 0 {
2524                        expr.push_str("))");
2525                    }
2526                }
2527                ShellToken::Word(w) => {
2528                    expr.push_str(w);
2529                    expr.push(' ');
2530                }
2531                ShellToken::LParen => expr.push('('),
2532                ShellToken::RParen => expr.push(')'),
2533                ShellToken::LBracket => expr.push('['),
2534                ShellToken::RBracket => expr.push(']'),
2535                ShellToken::Less => expr.push('<'),
2536                ShellToken::Greater => expr.push('>'),
2537                ShellToken::LessLess => expr.push_str("<<"),
2538                ShellToken::GreaterGreater => expr.push_str(">>"),
2539                ShellToken::Bang => expr.push('!'),
2540                ShellToken::Eof => break,
2541                _ => {}
2542            }
2543            self.advance();
2544        }
2545
2546        Ok(ShellCommand::Compound(CompoundCommand::Arith(
2547            expr.trim().to_string(),
2548        )))
2549    }
2550
2551    fn parse_function(&mut self) -> Result<ShellCommand, String> {
2552        self.expect(ShellToken::Function)?;
2553        self.skip_newlines();
2554        let name = if let ShellToken::Word(w) = self.advance() {
2555            w
2556        } else {
2557            return Err("Expected function name".to_string());
2558        };
2559
2560        self.skip_newlines();
2561        if self.current == ShellToken::LParen {
2562            self.advance();
2563            self.expect(ShellToken::RParen)?;
2564            self.skip_newlines();
2565        }
2566
2567        let body = self.parse_command()?;
2568        Ok(ShellCommand::FunctionDef(name, Box::new(body)))
2569    }
2570
2571    fn parse_coproc(&mut self) -> Result<ShellCommand, String> {
2572        self.expect(ShellToken::Coproc)?;
2573        self.skip_newlines();
2574
2575        let name = if let ShellToken::Word(w) = &self.current {
2576            let n = w.clone();
2577            self.advance();
2578            self.skip_newlines();
2579            Some(n)
2580        } else {
2581            None
2582        };
2583
2584        let body = self.parse_command()?;
2585        Ok(ShellCommand::Compound(CompoundCommand::Coproc {
2586            name,
2587            body: Box::new(body),
2588        }))
2589    }
2590
2591    fn parse_compound_list(&mut self) -> Result<Vec<ShellCommand>, String> {
2592        let mut commands = Vec::new();
2593        self.skip_newlines();
2594
2595        while self.current != ShellToken::Eof
2596            && self.current != ShellToken::RBrace
2597            && self.current != ShellToken::RParen
2598            && self.current != ShellToken::Fi
2599            && self.current != ShellToken::Done
2600            && self.current != ShellToken::Esac
2601            && self.current != ShellToken::Elif
2602            && self.current != ShellToken::Else
2603            && self.current != ShellToken::DoubleSemi
2604            && self.current != ShellToken::SemiAmp
2605            && self.current != ShellToken::SemiSemiAmp
2606        {
2607            let cmd = self.parse_list()?;
2608            commands.push(cmd);
2609            match &self.current {
2610                ShellToken::Newline | ShellToken::Semi => {
2611                    self.advance();
2612                }
2613                _ => {}
2614            }
2615            self.skip_newlines();
2616        }
2617
2618        Ok(commands)
2619    }
2620
2621    fn parse_compound_list_until(
2622        &mut self,
2623        terminators: &[ShellToken],
2624    ) -> Result<Vec<ShellCommand>, String> {
2625        let mut commands = Vec::new();
2626        self.skip_newlines();
2627
2628        while self.current != ShellToken::Eof && !terminators.contains(&self.current) {
2629            let cmd = self.parse_list()?;
2630            commands.push(cmd);
2631            match &self.current {
2632                ShellToken::Newline | ShellToken::Semi => {
2633                    self.advance();
2634                }
2635                _ => {}
2636            }
2637            self.skip_newlines();
2638        }
2639
2640        Ok(commands)
2641    }
2642}
2643
2644/// The Zsh Parser
2645pub struct ZshParser<'a> {
2646    lexer: ZshLexer<'a>,
2647    errors: Vec<ParseError>,
2648    /// Global iteration counter to prevent infinite loops
2649    global_iterations: usize,
2650    /// Recursion depth counter to prevent stack overflow
2651    recursion_depth: usize,
2652}
2653
2654const MAX_RECURSION_DEPTH: usize = 500;
2655
2656impl<'a> ZshParser<'a> {
2657    /// Create a new parser
2658    pub fn new(input: &'a str) -> Self {
2659        ZshParser {
2660            lexer: ZshLexer::new(input),
2661            errors: Vec::new(),
2662            global_iterations: 0,
2663            recursion_depth: 0,
2664        }
2665    }
2666
2667    /// Check iteration limit; returns true if exceeded
2668    #[inline]
2669    fn check_limit(&mut self) -> bool {
2670        self.global_iterations += 1;
2671        self.global_iterations > 10_000
2672    }
2673
2674    /// Check recursion depth; returns true if exceeded
2675    #[inline]
2676    fn check_recursion(&mut self) -> bool {
2677        self.recursion_depth > MAX_RECURSION_DEPTH
2678    }
2679
2680    /// Parse the complete input
2681    pub fn parse(&mut self) -> Result<ZshProgram, Vec<ParseError>> {
2682        self.lexer.zshlex();
2683
2684        let program = self.parse_program_until(None);
2685
2686        if !self.errors.is_empty() {
2687            return Err(std::mem::take(&mut self.errors));
2688        }
2689
2690        Ok(program)
2691    }
2692
2693    /// Parse a program (list of lists)
2694    fn parse_program(&mut self) -> ZshProgram {
2695        self.parse_program_until(None)
2696    }
2697
2698    /// Parse a program until we hit an end token
2699    fn parse_program_until(&mut self, end_tokens: Option<&[LexTok]>) -> ZshProgram {
2700        let mut lists = Vec::new();
2701
2702        loop {
2703            if self.check_limit() {
2704                self.error("parser exceeded global iteration limit");
2705                break;
2706            }
2707
2708            // Skip separators
2709            while self.lexer.tok == LexTok::Seper || self.lexer.tok == LexTok::Newlin {
2710                if self.check_limit() {
2711                    self.error("parser exceeded global iteration limit");
2712                    return ZshProgram { lists };
2713                }
2714                self.lexer.zshlex();
2715            }
2716
2717            if self.lexer.tok == LexTok::Endinput || self.lexer.tok == LexTok::Lexerr {
2718                break;
2719            }
2720
2721            // Check for end tokens
2722            if let Some(end_toks) = end_tokens {
2723                if end_toks.contains(&self.lexer.tok) {
2724                    break;
2725                }
2726            }
2727
2728            // Also stop at these tokens when not explicitly looking for them
2729            // Note: Else/Elif/Then are NOT here - they're handled by parse_if
2730            // to allow nested if statements inside case arms, loops, etc.
2731            match self.lexer.tok {
2732                LexTok::Outbrace
2733                | LexTok::Dsemi
2734                | LexTok::Semiamp
2735                | LexTok::Semibar
2736                | LexTok::Done
2737                | LexTok::Fi
2738                | LexTok::Esac
2739                | LexTok::Zend => break,
2740                _ => {}
2741            }
2742
2743            match self.parse_list() {
2744                Some(list) => lists.push(list),
2745                None => break,
2746            }
2747        }
2748
2749        ZshProgram { lists }
2750    }
2751
2752    /// Parse a list (sublist with optional & or ;)
2753    fn parse_list(&mut self) -> Option<ZshList> {
2754        let sublist = self.parse_sublist()?;
2755
2756        let flags = match self.lexer.tok {
2757            LexTok::Amper => {
2758                self.lexer.zshlex();
2759                ListFlags {
2760                    async_: true,
2761                    disown: false,
2762                }
2763            }
2764            LexTok::Amperbang => {
2765                self.lexer.zshlex();
2766                ListFlags {
2767                    async_: true,
2768                    disown: true,
2769                }
2770            }
2771            LexTok::Seper | LexTok::Semi | LexTok::Newlin => {
2772                self.lexer.zshlex();
2773                ListFlags::default()
2774            }
2775            _ => ListFlags::default(),
2776        };
2777
2778        Some(ZshList { sublist, flags })
2779    }
2780
2781    /// Parse a sublist (pipelines connected by && or ||)
2782    fn parse_sublist(&mut self) -> Option<ZshSublist> {
2783        self.recursion_depth += 1;
2784        if self.check_recursion() {
2785            self.error("parse_sublist: max recursion depth exceeded");
2786            self.recursion_depth -= 1;
2787            return None;
2788        }
2789
2790        let mut flags = SublistFlags::default();
2791
2792        // Handle coproc and !
2793        if self.lexer.tok == LexTok::Coproc {
2794            flags.coproc = true;
2795            self.lexer.zshlex();
2796        } else if self.lexer.tok == LexTok::Bang {
2797            flags.not = true;
2798            self.lexer.zshlex();
2799        }
2800
2801        let pipe = match self.parse_pipe() {
2802            Some(p) => p,
2803            None => {
2804                self.recursion_depth -= 1;
2805                return None;
2806            }
2807        };
2808
2809        // Check for && or ||
2810        let next = match self.lexer.tok {
2811            LexTok::Damper => {
2812                self.lexer.zshlex();
2813                self.skip_separators();
2814                self.parse_sublist().map(|s| (SublistOp::And, Box::new(s)))
2815            }
2816            LexTok::Dbar => {
2817                self.lexer.zshlex();
2818                self.skip_separators();
2819                self.parse_sublist().map(|s| (SublistOp::Or, Box::new(s)))
2820            }
2821            _ => None,
2822        };
2823
2824        self.recursion_depth -= 1;
2825        Some(ZshSublist { pipe, next, flags })
2826    }
2827
2828    /// Parse a pipeline
2829    fn parse_pipe(&mut self) -> Option<ZshPipe> {
2830        self.recursion_depth += 1;
2831        if self.check_recursion() {
2832            self.error("parse_pipe: max recursion depth exceeded");
2833            self.recursion_depth -= 1;
2834            return None;
2835        }
2836
2837        let lineno = self.lexer.toklineno;
2838        let cmd = match self.parse_cmd() {
2839            Some(c) => c,
2840            None => {
2841                self.recursion_depth -= 1;
2842                return None;
2843            }
2844        };
2845
2846        // Check for | or |&
2847        let next = match self.lexer.tok {
2848            LexTok::Bar | LexTok::Baramp => {
2849                let _merge_stderr = self.lexer.tok == LexTok::Baramp;
2850                self.lexer.zshlex();
2851                self.skip_separators();
2852                self.parse_pipe().map(Box::new)
2853            }
2854            _ => None,
2855        };
2856
2857        self.recursion_depth -= 1;
2858        Some(ZshPipe { cmd, next, lineno })
2859    }
2860
2861    /// Parse a command
2862    fn parse_cmd(&mut self) -> Option<ZshCommand> {
2863        // Parse leading redirections
2864        let mut redirs = Vec::new();
2865        while self.lexer.tok.is_redirop() {
2866            if let Some(redir) = self.parse_redir() {
2867                redirs.push(redir);
2868            }
2869        }
2870
2871        let cmd = match self.lexer.tok {
2872            LexTok::For | LexTok::Foreach => self.parse_for(),
2873            LexTok::Select => self.parse_select(),
2874            LexTok::Case => self.parse_case(),
2875            LexTok::If => self.parse_if(),
2876            LexTok::While => self.parse_while(false),
2877            LexTok::Until => self.parse_while(true),
2878            LexTok::Repeat => self.parse_repeat(),
2879            LexTok::Inpar => self.parse_subsh(),
2880            LexTok::Inbrace => self.parse_cursh(),
2881            LexTok::Func => self.parse_funcdef(),
2882            LexTok::Dinbrack => self.parse_cond(),
2883            LexTok::Dinpar => self.parse_arith(),
2884            LexTok::Time => self.parse_time(),
2885            _ => self.parse_simple(redirs),
2886        };
2887
2888        // Parse trailing redirections
2889        if cmd.is_some() {
2890            while self.lexer.tok.is_redirop() {
2891                if let Some(_redir) = self.parse_redir() {
2892                    // Append to command redirections
2893                    // (for non-simple commands, we'd need to handle this differently)
2894                }
2895            }
2896        }
2897
2898        cmd
2899    }
2900
2901    /// Parse a simple command
2902    fn parse_simple(&mut self, mut redirs: Vec<ZshRedir>) -> Option<ZshCommand> {
2903        let mut assigns = Vec::new();
2904        let mut words = Vec::new();
2905        const MAX_ITERATIONS: usize = 10_000;
2906        let mut iterations = 0;
2907
2908        // Parse leading assignments
2909        while self.lexer.tok == LexTok::Envstring || self.lexer.tok == LexTok::Envarray {
2910            iterations += 1;
2911            if iterations > MAX_ITERATIONS {
2912                self.error("parse_simple: exceeded max iterations in assignments");
2913                return None;
2914            }
2915            if let Some(assign) = self.parse_assign() {
2916                assigns.push(assign);
2917            }
2918            self.lexer.zshlex();
2919        }
2920
2921        // Parse words and redirections
2922        loop {
2923            iterations += 1;
2924            if iterations > MAX_ITERATIONS {
2925                self.error("parse_simple: exceeded max iterations");
2926                return None;
2927            }
2928            match self.lexer.tok {
2929                LexTok::String | LexTok::Typeset => {
2930                    let s = self.lexer.tokstr.clone();
2931                    if let Some(s) = s {
2932                        words.push(s);
2933                    }
2934                    self.lexer.zshlex();
2935                    // Check for function definition foo() { ... }
2936                    if words.len() == 1 && self.peek_inoutpar() {
2937                        return self.parse_inline_funcdef(words.pop().unwrap());
2938                    }
2939                }
2940                _ if self.lexer.tok.is_redirop() => {
2941                    match self.parse_redir() {
2942                        Some(redir) => redirs.push(redir),
2943                        None => break, // Error in redir parsing, stop
2944                    }
2945                }
2946                LexTok::Inoutpar if !words.is_empty() => {
2947                    // foo() { ... } style function
2948                    return self.parse_inline_funcdef(words.pop().unwrap());
2949                }
2950                _ => break,
2951            }
2952        }
2953
2954        if assigns.is_empty() && words.is_empty() && redirs.is_empty() {
2955            return None;
2956        }
2957
2958        Some(ZshCommand::Simple(ZshSimple {
2959            assigns,
2960            words,
2961            redirs,
2962        }))
2963    }
2964
2965    /// Parse an assignment
2966    fn parse_assign(&mut self) -> Option<ZshAssign> {
2967        use crate::tokens::char_tokens;
2968
2969        let tokstr = self.lexer.tokstr.as_ref()?;
2970
2971        // Parse name=value or name+=value
2972        // The '=' is encoded as char_tokens::EQUALS in the token string
2973        let (name, value_str, append) = if let Some(pos) = tokstr.find(char_tokens::EQUALS) {
2974            let name_part = &tokstr[..pos];
2975            let (name, append) = if name_part.ends_with('+') {
2976                (&name_part[..name_part.len() - 1], true)
2977            } else {
2978                (name_part, false)
2979            };
2980            (
2981                name.to_string(),
2982                tokstr[pos + char_tokens::EQUALS.len_utf8()..].to_string(),
2983                append,
2984            )
2985        } else if let Some(pos) = tokstr.find('=') {
2986            // Fallback to literal '=' for compatibility
2987            let name_part = &tokstr[..pos];
2988            let (name, append) = if name_part.ends_with('+') {
2989                (&name_part[..name_part.len() - 1], true)
2990            } else {
2991                (name_part, false)
2992            };
2993            (name.to_string(), tokstr[pos + 1..].to_string(), append)
2994        } else {
2995            return None;
2996        };
2997
2998        let value = if self.lexer.tok == LexTok::Envarray {
2999            // Array assignment: name=(...)
3000            let mut elements = Vec::new();
3001            self.lexer.zshlex(); // skip past token
3002
3003            let mut arr_iters = 0;
3004            const MAX_ARRAY_ELEMENTS: usize = 10_000;
3005            while matches!(
3006                self.lexer.tok,
3007                LexTok::String | LexTok::Seper | LexTok::Newlin
3008            ) {
3009                arr_iters += 1;
3010                if arr_iters > MAX_ARRAY_ELEMENTS {
3011                    self.error("array assignment exceeded maximum elements");
3012                    break;
3013                }
3014                if self.lexer.tok == LexTok::String {
3015                    if let Some(ref s) = self.lexer.tokstr {
3016                        elements.push(s.clone());
3017                    }
3018                }
3019                self.lexer.zshlex();
3020            }
3021
3022            // Expect OUTPAR
3023            if self.lexer.tok == LexTok::Outpar {
3024                self.lexer.zshlex();
3025            }
3026
3027            ZshAssignValue::Array(elements)
3028        } else {
3029            ZshAssignValue::Scalar(value_str)
3030        };
3031
3032        Some(ZshAssign {
3033            name,
3034            value,
3035            append,
3036        })
3037    }
3038
3039    /// Parse a redirection
3040    fn parse_redir(&mut self) -> Option<ZshRedir> {
3041        let rtype = match self.lexer.tok {
3042            LexTok::Outang => RedirType::Write,
3043            LexTok::Outangbang => RedirType::Writenow,
3044            LexTok::Doutang => RedirType::Append,
3045            LexTok::Doutangbang => RedirType::Appendnow,
3046            LexTok::Inang => RedirType::Read,
3047            LexTok::Inoutang => RedirType::ReadWrite,
3048            LexTok::Dinang => RedirType::Heredoc,
3049            LexTok::Dinangdash => RedirType::HeredocDash,
3050            LexTok::Trinang => RedirType::Herestr,
3051            LexTok::Inangamp => RedirType::MergeIn,
3052            LexTok::Outangamp => RedirType::MergeOut,
3053            LexTok::Ampoutang => RedirType::ErrWrite,
3054            LexTok::Outangampbang => RedirType::ErrWritenow,
3055            LexTok::Doutangamp => RedirType::ErrAppend,
3056            LexTok::Doutangampbang => RedirType::ErrAppendnow,
3057            _ => return None,
3058        };
3059
3060        let fd = if self.lexer.tokfd >= 0 {
3061            self.lexer.tokfd
3062        } else if matches!(
3063            rtype,
3064            RedirType::Read
3065                | RedirType::ReadWrite
3066                | RedirType::MergeIn
3067                | RedirType::Heredoc
3068                | RedirType::HeredocDash
3069                | RedirType::Herestr
3070        ) {
3071            0
3072        } else {
3073            1
3074        };
3075
3076        self.lexer.zshlex();
3077
3078        let name = match self.lexer.tok {
3079            LexTok::String | LexTok::Envstring => {
3080                let n = self.lexer.tokstr.clone().unwrap_or_default();
3081                self.lexer.zshlex();
3082                n
3083            }
3084            _ => {
3085                self.error("expected word after redirection");
3086                return None;
3087            }
3088        };
3089
3090        // Handle heredoc
3091        let heredoc = if matches!(rtype, RedirType::Heredoc | RedirType::HeredocDash) {
3092            // Heredoc content will be filled in by the lexer
3093            None // Placeholder
3094        } else {
3095            None
3096        };
3097
3098        Some(ZshRedir {
3099            rtype,
3100            fd,
3101            name,
3102            heredoc,
3103            varid: None,
3104        })
3105    }
3106
3107    /// Parse for/foreach loop
3108    fn parse_for(&mut self) -> Option<ZshCommand> {
3109        let is_foreach = self.lexer.tok == LexTok::Foreach;
3110        self.lexer.zshlex();
3111
3112        // Check for C-style: for (( init; cond; step ))
3113        if self.lexer.tok == LexTok::Dinpar {
3114            return self.parse_for_cstyle();
3115        }
3116
3117        // Get variable name
3118        let var = match self.lexer.tok {
3119            LexTok::String => {
3120                let v = self.lexer.tokstr.clone().unwrap_or_default();
3121                self.lexer.zshlex();
3122                v
3123            }
3124            _ => {
3125                self.error("expected variable name in for");
3126                return None;
3127            }
3128        };
3129
3130        // Skip newlines
3131        self.skip_separators();
3132
3133        // Get list
3134        let list = if self.lexer.tok == LexTok::String {
3135            let s = self.lexer.tokstr.as_ref();
3136            if s.map(|s| s == "in").unwrap_or(false) {
3137                self.lexer.zshlex();
3138                let mut words = Vec::new();
3139                let mut word_count = 0;
3140                while self.lexer.tok == LexTok::String {
3141                    word_count += 1;
3142                    if word_count > 500 || self.check_limit() {
3143                        self.error("for: too many words");
3144                        return None;
3145                    }
3146                    if let Some(ref s) = self.lexer.tokstr {
3147                        words.push(s.clone());
3148                    }
3149                    self.lexer.zshlex();
3150                }
3151                ForList::Words(words)
3152            } else {
3153                ForList::Positional
3154            }
3155        } else if self.lexer.tok == LexTok::Inpar {
3156            // for var (...)
3157            self.lexer.zshlex();
3158            let mut words = Vec::new();
3159            let mut word_count = 0;
3160            while self.lexer.tok == LexTok::String || self.lexer.tok == LexTok::Seper {
3161                word_count += 1;
3162                if word_count > 500 || self.check_limit() {
3163                    self.error("for: too many words in parens");
3164                    return None;
3165                }
3166                if self.lexer.tok == LexTok::String {
3167                    if let Some(ref s) = self.lexer.tokstr {
3168                        words.push(s.clone());
3169                    }
3170                }
3171                self.lexer.zshlex();
3172            }
3173            if self.lexer.tok == LexTok::Outpar {
3174                self.lexer.zshlex();
3175            }
3176            ForList::Words(words)
3177        } else {
3178            ForList::Positional
3179        };
3180
3181        // Skip to body
3182        self.skip_separators();
3183
3184        // Parse body
3185        let body = self.parse_loop_body(is_foreach)?;
3186
3187        Some(ZshCommand::For(ZshFor {
3188            var,
3189            list,
3190            body: Box::new(body),
3191        }))
3192    }
3193
3194    /// Parse C-style for loop: for (( init; cond; step ))
3195    fn parse_for_cstyle(&mut self) -> Option<ZshCommand> {
3196        // We're at (( (Dinpar None) - the opening ((
3197        // Lexer returns:
3198        //   Dinpar None     - opening ((
3199        //   Dinpar "init"   - init expression, semicolon consumed
3200        //   Dinpar "cond"   - cond expression, semicolon consumed
3201        //   Doutpar "step"  - step expression, closing )) consumed
3202
3203        self.lexer.zshlex(); // Get init: Dinpar "i=0"
3204
3205        if self.lexer.tok != LexTok::Dinpar {
3206            self.error("expected init expression in for ((");
3207            return None;
3208        }
3209        let init = self.lexer.tokstr.clone().unwrap_or_default();
3210
3211        self.lexer.zshlex(); // Get cond: Dinpar "i<10"
3212
3213        if self.lexer.tok != LexTok::Dinpar {
3214            self.error("expected condition in for ((");
3215            return None;
3216        }
3217        let cond = self.lexer.tokstr.clone().unwrap_or_default();
3218
3219        self.lexer.zshlex(); // Get step: Doutpar "i++"
3220
3221        if self.lexer.tok != LexTok::Doutpar {
3222            self.error("expected )) in for");
3223            return None;
3224        }
3225        let step = self.lexer.tokstr.clone().unwrap_or_default();
3226
3227        self.lexer.zshlex(); // Move past ))
3228
3229        self.skip_separators();
3230        let body = self.parse_loop_body(false)?;
3231
3232        Some(ZshCommand::For(ZshFor {
3233            var: String::new(),
3234            list: ForList::CStyle { init, cond, step },
3235            body: Box::new(body),
3236        }))
3237    }
3238
3239    /// Parse select loop (same syntax as for)
3240    fn parse_select(&mut self) -> Option<ZshCommand> {
3241        self.parse_for()
3242    }
3243
3244    /// Parse case statement
3245    fn parse_case(&mut self) -> Option<ZshCommand> {
3246        self.lexer.zshlex(); // skip 'case'
3247
3248        let word = match self.lexer.tok {
3249            LexTok::String => {
3250                let w = self.lexer.tokstr.clone().unwrap_or_default();
3251                self.lexer.zshlex();
3252                w
3253            }
3254            _ => {
3255                self.error("expected word after case");
3256                return None;
3257            }
3258        };
3259
3260        self.skip_separators();
3261
3262        // Expect 'in' or {
3263        let use_brace = self.lexer.tok == LexTok::Inbrace;
3264        if self.lexer.tok == LexTok::String {
3265            let s = self.lexer.tokstr.as_ref();
3266            if s.map(|s| s != "in").unwrap_or(true) {
3267                self.error("expected 'in' in case");
3268                return None;
3269            }
3270        } else if !use_brace {
3271            self.error("expected 'in' or '{' in case");
3272            return None;
3273        }
3274        self.lexer.zshlex();
3275
3276        let mut arms = Vec::new();
3277        const MAX_ARMS: usize = 10_000;
3278
3279        loop {
3280            if arms.len() > MAX_ARMS {
3281                self.error("parse_case: too many arms");
3282                break;
3283            }
3284
3285            // Set incasepat BEFORE skipping separators so lexer knows we're in case pattern context
3286            // This affects how [ and | are lexed
3287            self.lexer.incasepat = 1;
3288
3289            self.skip_separators();
3290
3291            // Check for end
3292            // Note: 'esac' might be String "esac" if incasepat > 0 prevents reserved word recognition
3293            let is_esac = self.lexer.tok == LexTok::Esac
3294                || (self.lexer.tok == LexTok::String
3295                    && self
3296                        .lexer
3297                        .tokstr
3298                        .as_ref()
3299                        .map(|s| s == "esac")
3300                        .unwrap_or(false));
3301            if (use_brace && self.lexer.tok == LexTok::Outbrace) || (!use_brace && is_esac) {
3302                self.lexer.incasepat = 0;
3303                self.lexer.zshlex();
3304                break;
3305            }
3306
3307            // Also break on EOF
3308            if self.lexer.tok == LexTok::Endinput || self.lexer.tok == LexTok::Lexerr {
3309                self.lexer.incasepat = 0;
3310                break;
3311            }
3312
3313            // Skip optional (
3314            if self.lexer.tok == LexTok::Inpar {
3315                self.lexer.zshlex();
3316            }
3317
3318            // incasepat is already set above
3319            let mut patterns = Vec::new();
3320            let mut pattern_iterations = 0;
3321            loop {
3322                pattern_iterations += 1;
3323                if pattern_iterations > 1000 {
3324                    self.error("parse_case: too many pattern iterations");
3325                    self.lexer.incasepat = 0;
3326                    return None;
3327                }
3328
3329                if self.lexer.tok == LexTok::String {
3330                    let s = self.lexer.tokstr.as_ref();
3331                    if s.map(|s| s == "esac").unwrap_or(false) {
3332                        break;
3333                    }
3334                    patterns.push(self.lexer.tokstr.clone().unwrap_or_default());
3335                    // After first pattern token, set incasepat=2 so ( is treated as part of pattern
3336                    self.lexer.incasepat = 2;
3337                    self.lexer.zshlex();
3338                } else if self.lexer.tok != LexTok::Bar {
3339                    break;
3340                }
3341
3342                if self.lexer.tok == LexTok::Bar {
3343                    // Reset to 1 (start of next alternative pattern)
3344                    self.lexer.incasepat = 1;
3345                    self.lexer.zshlex();
3346                } else {
3347                    break;
3348                }
3349            }
3350            self.lexer.incasepat = 0;
3351
3352            // Expect )
3353            if self.lexer.tok != LexTok::Outpar {
3354                self.error("expected ')' in case pattern");
3355                return None;
3356            }
3357            self.lexer.zshlex();
3358
3359            // Parse body
3360            let body = self.parse_program();
3361
3362            // Get terminator
3363            let terminator = match self.lexer.tok {
3364                LexTok::Dsemi => {
3365                    self.lexer.zshlex();
3366                    CaseTerm::Break
3367                }
3368                LexTok::Semiamp => {
3369                    self.lexer.zshlex();
3370                    CaseTerm::Continue
3371                }
3372                LexTok::Semibar => {
3373                    self.lexer.zshlex();
3374                    CaseTerm::TestNext
3375                }
3376                _ => CaseTerm::Break,
3377            };
3378
3379            if !patterns.is_empty() {
3380                arms.push(CaseArm {
3381                    patterns,
3382                    body,
3383                    terminator,
3384                });
3385            }
3386        }
3387
3388        Some(ZshCommand::Case(ZshCase { word, arms }))
3389    }
3390
3391    /// Parse if statement
3392    fn parse_if(&mut self) -> Option<ZshCommand> {
3393        self.lexer.zshlex(); // skip 'if'
3394
3395        // Parse condition - stops at 'then' or '{' (zsh allows { instead of then)
3396        let cond = Box::new(self.parse_program_until(Some(&[LexTok::Then, LexTok::Inbrace])));
3397
3398        self.skip_separators();
3399
3400        // Expect 'then' or {
3401        let use_brace = self.lexer.tok == LexTok::Inbrace;
3402        if self.lexer.tok != LexTok::Then && !use_brace {
3403            self.error("expected 'then' or '{' after if condition");
3404            return None;
3405        }
3406        self.lexer.zshlex();
3407
3408        // Parse then-body - stops at else/elif/fi, or } if using brace syntax
3409        let then = if use_brace {
3410            let body = self.parse_program_until(Some(&[LexTok::Outbrace]));
3411            if self.lexer.tok == LexTok::Outbrace {
3412                self.lexer.zshlex();
3413            }
3414            Box::new(body)
3415        } else {
3416            Box::new(self.parse_program_until(Some(&[LexTok::Else, LexTok::Elif, LexTok::Fi])))
3417        };
3418
3419        // Parse elif and else (only for then/fi syntax, not brace syntax)
3420        let mut elif = Vec::new();
3421        let mut else_ = None;
3422
3423        if !use_brace {
3424            loop {
3425                self.skip_separators();
3426
3427                match self.lexer.tok {
3428                    LexTok::Elif => {
3429                        self.lexer.zshlex();
3430                        // elif condition stops at 'then' or '{'
3431                        let econd =
3432                            self.parse_program_until(Some(&[LexTok::Then, LexTok::Inbrace]));
3433                        self.skip_separators();
3434
3435                        let elif_use_brace = self.lexer.tok == LexTok::Inbrace;
3436                        if self.lexer.tok != LexTok::Then && !elif_use_brace {
3437                            self.error("expected 'then' after elif");
3438                            return None;
3439                        }
3440                        self.lexer.zshlex();
3441
3442                        // elif body stops at else/elif/fi or } if using braces
3443                        let ebody = if elif_use_brace {
3444                            let body = self.parse_program_until(Some(&[LexTok::Outbrace]));
3445                            if self.lexer.tok == LexTok::Outbrace {
3446                                self.lexer.zshlex();
3447                            }
3448                            body
3449                        } else {
3450                            self.parse_program_until(Some(&[
3451                                LexTok::Else,
3452                                LexTok::Elif,
3453                                LexTok::Fi,
3454                            ]))
3455                        };
3456
3457                        elif.push((econd, ebody));
3458                    }
3459                    LexTok::Else => {
3460                        self.lexer.zshlex();
3461                        self.skip_separators();
3462
3463                        let else_use_brace = self.lexer.tok == LexTok::Inbrace;
3464                        if else_use_brace {
3465                            self.lexer.zshlex();
3466                        }
3467
3468                        // else body stops at 'fi' or '}'
3469                        else_ = Some(Box::new(if else_use_brace {
3470                            let body = self.parse_program_until(Some(&[LexTok::Outbrace]));
3471                            if self.lexer.tok == LexTok::Outbrace {
3472                                self.lexer.zshlex();
3473                            }
3474                            body
3475                        } else {
3476                            self.parse_program_until(Some(&[LexTok::Fi]))
3477                        }));
3478
3479                        // Consume the 'fi' if present (not for brace syntax)
3480                        if !else_use_brace && self.lexer.tok == LexTok::Fi {
3481                            self.lexer.zshlex();
3482                        }
3483                        break;
3484                    }
3485                    LexTok::Fi => {
3486                        self.lexer.zshlex();
3487                        break;
3488                    }
3489                    _ => break,
3490                }
3491            }
3492        }
3493
3494        Some(ZshCommand::If(ZshIf {
3495            cond,
3496            then,
3497            elif,
3498            else_,
3499        }))
3500    }
3501
3502    /// Parse while/until loop
3503    fn parse_while(&mut self, until: bool) -> Option<ZshCommand> {
3504        self.lexer.zshlex(); // skip while/until
3505
3506        let cond = Box::new(self.parse_program());
3507
3508        self.skip_separators();
3509        let body = self.parse_loop_body(false)?;
3510
3511        Some(ZshCommand::While(ZshWhile {
3512            cond,
3513            body: Box::new(body),
3514            until,
3515        }))
3516    }
3517
3518    /// Parse repeat loop
3519    fn parse_repeat(&mut self) -> Option<ZshCommand> {
3520        self.lexer.zshlex(); // skip 'repeat'
3521
3522        let count = match self.lexer.tok {
3523            LexTok::String => {
3524                let c = self.lexer.tokstr.clone().unwrap_or_default();
3525                self.lexer.zshlex();
3526                c
3527            }
3528            _ => {
3529                self.error("expected count after repeat");
3530                return None;
3531            }
3532        };
3533
3534        self.skip_separators();
3535        let body = self.parse_loop_body(false)?;
3536
3537        Some(ZshCommand::Repeat(ZshRepeat {
3538            count,
3539            body: Box::new(body),
3540        }))
3541    }
3542
3543    /// Parse loop body (do...done, {...}, or shortloop)
3544    fn parse_loop_body(&mut self, foreach_style: bool) -> Option<ZshProgram> {
3545        if self.lexer.tok == LexTok::Doloop {
3546            self.lexer.zshlex();
3547            let body = self.parse_program();
3548            if self.lexer.tok == LexTok::Done {
3549                self.lexer.zshlex();
3550            }
3551            Some(body)
3552        } else if self.lexer.tok == LexTok::Inbrace {
3553            self.lexer.zshlex();
3554            let body = self.parse_program();
3555            if self.lexer.tok == LexTok::Outbrace {
3556                self.lexer.zshlex();
3557            }
3558            Some(body)
3559        } else if foreach_style {
3560            // foreach allows 'end' terminator
3561            let body = self.parse_program();
3562            if self.lexer.tok == LexTok::Zend {
3563                self.lexer.zshlex();
3564            }
3565            Some(body)
3566        } else {
3567            // Short loop - single command
3568            match self.parse_list() {
3569                Some(list) => Some(ZshProgram { lists: vec![list] }),
3570                None => None,
3571            }
3572        }
3573    }
3574
3575    /// Parse (...) subshell
3576    fn parse_subsh(&mut self) -> Option<ZshCommand> {
3577        self.lexer.zshlex(); // skip (
3578        let prog = self.parse_program();
3579        if self.lexer.tok == LexTok::Outpar {
3580            self.lexer.zshlex();
3581        }
3582        Some(ZshCommand::Subsh(Box::new(prog)))
3583    }
3584
3585    /// Parse {...} cursh
3586    fn parse_cursh(&mut self) -> Option<ZshCommand> {
3587        self.lexer.zshlex(); // skip {
3588        let prog = self.parse_program();
3589
3590        // Check for { ... } always { ... }
3591        if self.lexer.tok == LexTok::Outbrace {
3592            self.lexer.zshlex();
3593
3594            // Check for 'always'
3595            if self.lexer.tok == LexTok::String {
3596                let s = self.lexer.tokstr.as_ref();
3597                if s.map(|s| s == "always").unwrap_or(false) {
3598                    self.lexer.zshlex();
3599                    self.skip_separators();
3600
3601                    if self.lexer.tok == LexTok::Inbrace {
3602                        self.lexer.zshlex();
3603                        let always = self.parse_program();
3604                        if self.lexer.tok == LexTok::Outbrace {
3605                            self.lexer.zshlex();
3606                        }
3607                        return Some(ZshCommand::Try(ZshTry {
3608                            try_block: Box::new(prog),
3609                            always: Box::new(always),
3610                        }));
3611                    }
3612                }
3613            }
3614        }
3615
3616        Some(ZshCommand::Cursh(Box::new(prog)))
3617    }
3618
3619    /// Parse function definition
3620    fn parse_funcdef(&mut self) -> Option<ZshCommand> {
3621        self.lexer.zshlex(); // skip 'function'
3622
3623        let mut names = Vec::new();
3624        let mut tracing = false;
3625
3626        // Handle options like -T and function names
3627        loop {
3628            match self.lexer.tok {
3629                LexTok::String => {
3630                    let s = self.lexer.tokstr.as_ref()?;
3631                    if s.starts_with('-') {
3632                        if s.contains('T') {
3633                            tracing = true;
3634                        }
3635                        self.lexer.zshlex();
3636                        continue;
3637                    }
3638                    names.push(s.clone());
3639                    self.lexer.zshlex();
3640                }
3641                LexTok::Inbrace | LexTok::Inoutpar | LexTok::Seper | LexTok::Newlin => break,
3642                _ => break,
3643            }
3644        }
3645
3646        // Optional ()
3647        if self.lexer.tok == LexTok::Inoutpar {
3648            self.lexer.zshlex();
3649        }
3650
3651        self.skip_separators();
3652
3653        // Parse body
3654        if self.lexer.tok == LexTok::Inbrace {
3655            self.lexer.zshlex();
3656            let body = self.parse_program();
3657            if self.lexer.tok == LexTok::Outbrace {
3658                self.lexer.zshlex();
3659            }
3660            Some(ZshCommand::FuncDef(ZshFuncDef {
3661                names,
3662                body: Box::new(body),
3663                tracing,
3664            }))
3665        } else {
3666            // Short form
3667            match self.parse_list() {
3668                Some(list) => Some(ZshCommand::FuncDef(ZshFuncDef {
3669                    names,
3670                    body: Box::new(ZshProgram { lists: vec![list] }),
3671                    tracing,
3672                })),
3673                None => None,
3674            }
3675        }
3676    }
3677
3678    /// Parse inline function definition: name() { ... }
3679    fn parse_inline_funcdef(&mut self, name: String) -> Option<ZshCommand> {
3680        // Skip ()
3681        if self.lexer.tok == LexTok::Inoutpar {
3682            self.lexer.zshlex();
3683        }
3684
3685        self.skip_separators();
3686
3687        // Parse body
3688        if self.lexer.tok == LexTok::Inbrace {
3689            self.lexer.zshlex();
3690            let body = self.parse_program();
3691            if self.lexer.tok == LexTok::Outbrace {
3692                self.lexer.zshlex();
3693            }
3694            Some(ZshCommand::FuncDef(ZshFuncDef {
3695                names: vec![name],
3696                body: Box::new(body),
3697                tracing: false,
3698            }))
3699        } else {
3700            match self.parse_cmd() {
3701                Some(cmd) => {
3702                    let list = ZshList {
3703                        sublist: ZshSublist {
3704                            pipe: ZshPipe {
3705                                cmd,
3706                                next: None,
3707                                lineno: self.lexer.lineno,
3708                            },
3709                            next: None,
3710                            flags: SublistFlags::default(),
3711                        },
3712                        flags: ListFlags::default(),
3713                    };
3714                    Some(ZshCommand::FuncDef(ZshFuncDef {
3715                        names: vec![name],
3716                        body: Box::new(ZshProgram { lists: vec![list] }),
3717                        tracing: false,
3718                    }))
3719                }
3720                None => None,
3721            }
3722        }
3723    }
3724
3725    /// Parse [[ ... ]] conditional
3726    fn parse_cond(&mut self) -> Option<ZshCommand> {
3727        self.lexer.zshlex(); // skip [[
3728        let cond = self.parse_cond_expr();
3729
3730        if self.lexer.tok == LexTok::Doutbrack {
3731            self.lexer.zshlex();
3732        }
3733
3734        cond.map(ZshCommand::Cond)
3735    }
3736
3737    /// Parse conditional expression
3738    fn parse_cond_expr(&mut self) -> Option<ZshCond> {
3739        self.parse_cond_or()
3740    }
3741
3742    fn parse_cond_or(&mut self) -> Option<ZshCond> {
3743        self.recursion_depth += 1;
3744        if self.check_recursion() {
3745            self.error("parse_cond_or: max recursion depth exceeded");
3746            self.recursion_depth -= 1;
3747            return None;
3748        }
3749
3750        let left = match self.parse_cond_and() {
3751            Some(l) => l,
3752            None => {
3753                self.recursion_depth -= 1;
3754                return None;
3755            }
3756        };
3757
3758        self.skip_cond_separators();
3759
3760        let result = if self.lexer.tok == LexTok::Dbar {
3761            self.lexer.zshlex();
3762            self.skip_cond_separators();
3763            match self.parse_cond_or() {
3764                Some(right) => Some(ZshCond::Or(Box::new(left), Box::new(right))),
3765                None => None,
3766            }
3767        } else {
3768            Some(left)
3769        };
3770
3771        self.recursion_depth -= 1;
3772        result
3773    }
3774
3775    fn parse_cond_and(&mut self) -> Option<ZshCond> {
3776        self.recursion_depth += 1;
3777        if self.check_recursion() {
3778            self.error("parse_cond_and: max recursion depth exceeded");
3779            self.recursion_depth -= 1;
3780            return None;
3781        }
3782
3783        let left = match self.parse_cond_not() {
3784            Some(l) => l,
3785            None => {
3786                self.recursion_depth -= 1;
3787                return None;
3788            }
3789        };
3790
3791        self.skip_cond_separators();
3792
3793        let result = if self.lexer.tok == LexTok::Damper {
3794            self.lexer.zshlex();
3795            self.skip_cond_separators();
3796            match self.parse_cond_and() {
3797                Some(right) => Some(ZshCond::And(Box::new(left), Box::new(right))),
3798                None => None,
3799            }
3800        } else {
3801            Some(left)
3802        };
3803
3804        self.recursion_depth -= 1;
3805        result
3806    }
3807
3808    fn parse_cond_not(&mut self) -> Option<ZshCond> {
3809        self.recursion_depth += 1;
3810        if self.check_recursion() {
3811            self.error("parse_cond_not: max recursion depth exceeded");
3812            self.recursion_depth -= 1;
3813            return None;
3814        }
3815
3816        self.skip_cond_separators();
3817
3818        // ! can be either LexTok::Bang or String "!"
3819        let is_not = self.lexer.tok == LexTok::Bang
3820            || (self.lexer.tok == LexTok::String
3821                && self
3822                    .lexer
3823                    .tokstr
3824                    .as_ref()
3825                    .map(|s| s == "!")
3826                    .unwrap_or(false));
3827        if is_not {
3828            self.lexer.zshlex();
3829            let inner = match self.parse_cond_not() {
3830                Some(i) => i,
3831                None => {
3832                    self.recursion_depth -= 1;
3833                    return None;
3834                }
3835            };
3836            self.recursion_depth -= 1;
3837            return Some(ZshCond::Not(Box::new(inner)));
3838        }
3839
3840        if self.lexer.tok == LexTok::Inpar {
3841            self.lexer.zshlex();
3842            self.skip_cond_separators();
3843            let inner = match self.parse_cond_expr() {
3844                Some(i) => i,
3845                None => {
3846                    self.recursion_depth -= 1;
3847                    return None;
3848                }
3849            };
3850            self.skip_cond_separators();
3851            if self.lexer.tok == LexTok::Outpar {
3852                self.lexer.zshlex();
3853            }
3854            self.recursion_depth -= 1;
3855            return Some(inner);
3856        }
3857
3858        let result = self.parse_cond_primary();
3859        self.recursion_depth -= 1;
3860        result
3861    }
3862
3863    fn parse_cond_primary(&mut self) -> Option<ZshCond> {
3864        let s1 = match self.lexer.tok {
3865            LexTok::String => {
3866                let s = self.lexer.tokstr.clone().unwrap_or_default();
3867                self.lexer.zshlex();
3868                s
3869            }
3870            _ => return None,
3871        };
3872
3873        self.skip_cond_separators();
3874
3875        // Check for unary operator
3876        if s1.starts_with('-') && s1.len() == 2 {
3877            let s2 = match self.lexer.tok {
3878                LexTok::String => {
3879                    let s = self.lexer.tokstr.clone().unwrap_or_default();
3880                    self.lexer.zshlex();
3881                    s
3882                }
3883                _ => return Some(ZshCond::Unary("-n".to_string(), s1)),
3884            };
3885            return Some(ZshCond::Unary(s1, s2));
3886        }
3887
3888        // Check for binary operator
3889        let op = match self.lexer.tok {
3890            LexTok::String => {
3891                let s = self.lexer.tokstr.clone().unwrap_or_default();
3892                self.lexer.zshlex();
3893                s
3894            }
3895            LexTok::Inang => {
3896                self.lexer.zshlex();
3897                "<".to_string()
3898            }
3899            LexTok::Outang => {
3900                self.lexer.zshlex();
3901                ">".to_string()
3902            }
3903            _ => return Some(ZshCond::Unary("-n".to_string(), s1)),
3904        };
3905
3906        self.skip_cond_separators();
3907
3908        let s2 = match self.lexer.tok {
3909            LexTok::String => {
3910                let s = self.lexer.tokstr.clone().unwrap_or_default();
3911                self.lexer.zshlex();
3912                s
3913            }
3914            _ => return Some(ZshCond::Binary(s1, op, String::new())),
3915        };
3916
3917        if op == "=~" {
3918            Some(ZshCond::Regex(s1, s2))
3919        } else {
3920            Some(ZshCond::Binary(s1, op, s2))
3921        }
3922    }
3923
3924    fn skip_cond_separators(&mut self) {
3925        while self.lexer.tok == LexTok::Seper && {
3926            let s = self.lexer.tokstr.as_ref();
3927            s.map(|s| !s.contains(';')).unwrap_or(true)
3928        } {
3929            self.lexer.zshlex();
3930        }
3931    }
3932
3933    /// Parse (( ... )) arithmetic command
3934    fn parse_arith(&mut self) -> Option<ZshCommand> {
3935        let expr = self.lexer.tokstr.clone().unwrap_or_default();
3936        self.lexer.zshlex();
3937        Some(ZshCommand::Arith(expr))
3938    }
3939
3940    /// Parse time command
3941    fn parse_time(&mut self) -> Option<ZshCommand> {
3942        self.lexer.zshlex(); // skip 'time'
3943
3944        // Check if there's a pipeline to time
3945        if self.lexer.tok == LexTok::Seper
3946            || self.lexer.tok == LexTok::Newlin
3947            || self.lexer.tok == LexTok::Endinput
3948        {
3949            Some(ZshCommand::Time(None))
3950        } else {
3951            let sublist = self.parse_sublist();
3952            Some(ZshCommand::Time(sublist.map(Box::new)))
3953        }
3954    }
3955
3956    /// Check if next token is ()
3957    fn peek_inoutpar(&mut self) -> bool {
3958        self.lexer.tok == LexTok::Inoutpar
3959    }
3960
3961    /// Skip separator tokens
3962    fn skip_separators(&mut self) {
3963        let mut iterations = 0;
3964        while self.lexer.tok == LexTok::Seper || self.lexer.tok == LexTok::Newlin {
3965            iterations += 1;
3966            if iterations > 100_000 {
3967                self.error("skip_separators: too many iterations");
3968                return;
3969            }
3970            self.lexer.zshlex();
3971        }
3972    }
3973
3974    /// Record an error
3975    fn error(&mut self, msg: &str) {
3976        self.errors.push(ParseError {
3977            message: msg.to_string(),
3978            line: self.lexer.lineno,
3979        });
3980    }
3981}
3982
3983#[cfg(test)]
3984mod tests {
3985    use super::*;
3986
3987    fn parse(input: &str) -> Result<ZshProgram, Vec<ParseError>> {
3988        let mut parser = ZshParser::new(input);
3989        parser.parse()
3990    }
3991
3992    #[test]
3993    fn test_simple_command() {
3994        let prog = parse("echo hello world").unwrap();
3995        assert_eq!(prog.lists.len(), 1);
3996        match &prog.lists[0].sublist.pipe.cmd {
3997            ZshCommand::Simple(s) => {
3998                assert_eq!(s.words, vec!["echo", "hello", "world"]);
3999            }
4000            _ => panic!("expected simple command"),
4001        }
4002    }
4003
4004    #[test]
4005    fn test_pipeline() {
4006        let prog = parse("ls | grep foo | wc -l").unwrap();
4007        assert_eq!(prog.lists.len(), 1);
4008
4009        let pipe = &prog.lists[0].sublist.pipe;
4010        assert!(pipe.next.is_some());
4011
4012        let pipe2 = pipe.next.as_ref().unwrap();
4013        assert!(pipe2.next.is_some());
4014    }
4015
4016    #[test]
4017    fn test_and_or() {
4018        let prog = parse("cmd1 && cmd2 || cmd3").unwrap();
4019        let sublist = &prog.lists[0].sublist;
4020
4021        assert!(sublist.next.is_some());
4022        let (op, _) = sublist.next.as_ref().unwrap();
4023        assert_eq!(*op, SublistOp::And);
4024    }
4025
4026    #[test]
4027    fn test_if_then() {
4028        let prog = parse("if test -f foo; then echo yes; fi").unwrap();
4029        match &prog.lists[0].sublist.pipe.cmd {
4030            ZshCommand::If(_) => {}
4031            _ => panic!("expected if command"),
4032        }
4033    }
4034
4035    #[test]
4036    fn test_for_loop() {
4037        let prog = parse("for i in a b c; do echo $i; done").unwrap();
4038        match &prog.lists[0].sublist.pipe.cmd {
4039            ZshCommand::For(f) => {
4040                assert_eq!(f.var, "i");
4041                match &f.list {
4042                    ForList::Words(w) => assert_eq!(w, &vec!["a", "b", "c"]),
4043                    _ => panic!("expected word list"),
4044                }
4045            }
4046            _ => panic!("expected for command"),
4047        }
4048    }
4049
4050    #[test]
4051    fn test_case() {
4052        let prog = parse("case $x in a) echo a;; b) echo b;; esac").unwrap();
4053        match &prog.lists[0].sublist.pipe.cmd {
4054            ZshCommand::Case(c) => {
4055                assert_eq!(c.arms.len(), 2);
4056            }
4057            _ => panic!("expected case command"),
4058        }
4059    }
4060
4061    #[test]
4062    fn test_function() {
4063        // First test just parsing "function foo" to see what happens
4064        let prog = parse("function foo { }").unwrap();
4065        match &prog.lists[0].sublist.pipe.cmd {
4066            ZshCommand::FuncDef(f) => {
4067                assert_eq!(f.names, vec!["foo"]);
4068            }
4069            _ => panic!(
4070                "expected function, got {:?}",
4071                prog.lists[0].sublist.pipe.cmd
4072            ),
4073        }
4074    }
4075
4076    #[test]
4077    fn test_redirection() {
4078        let prog = parse("echo hello > file.txt").unwrap();
4079        match &prog.lists[0].sublist.pipe.cmd {
4080            ZshCommand::Simple(s) => {
4081                assert_eq!(s.redirs.len(), 1);
4082                assert_eq!(s.redirs[0].rtype, RedirType::Write);
4083            }
4084            _ => panic!("expected simple command"),
4085        }
4086    }
4087
4088    #[test]
4089    fn test_assignment() {
4090        let prog = parse("FOO=bar echo $FOO").unwrap();
4091        match &prog.lists[0].sublist.pipe.cmd {
4092            ZshCommand::Simple(s) => {
4093                assert_eq!(s.assigns.len(), 1);
4094                assert_eq!(s.assigns[0].name, "FOO");
4095            }
4096            _ => panic!("expected simple command"),
4097        }
4098    }
4099
4100    #[test]
4101    fn test_parse_completion_function() {
4102        let input = r#"_2to3_fixes() {
4103  local -a fixes
4104  fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
4105  (( ${#fixes} )) && _describe -t fixes 'fix' fixes
4106}"#;
4107        let result = parse(input);
4108        assert!(
4109            result.is_ok(),
4110            "Failed to parse completion function: {:?}",
4111            result.err()
4112        );
4113        let prog = result.unwrap();
4114        assert!(
4115            !prog.lists.is_empty(),
4116            "Expected at least one list in program"
4117        );
4118    }
4119
4120    #[test]
4121    fn test_parse_array_with_complex_elements() {
4122        let input = r#"arguments=(
4123  '(- * :)'{-h,--help}'[show this help message and exit]'
4124  {-d,--doctests_only}'[fix up doctests only]'
4125  '*:filename:_files'
4126)"#;
4127        let result = parse(input);
4128        assert!(
4129            result.is_ok(),
4130            "Failed to parse array assignment: {:?}",
4131            result.err()
4132        );
4133    }
4134
4135    #[test]
4136    fn test_parse_full_completion_file() {
4137        let input = r##"#compdef 2to3
4138
4139# zsh completions for '2to3'
4140
4141_2to3_fixes() {
4142  local -a fixes
4143  fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
4144  (( ${#fixes} )) && _describe -t fixes 'fix' fixes
4145}
4146
4147local -a arguments
4148
4149arguments=(
4150  '(- * :)'{-h,--help}'[show this help message and exit]'
4151  {-d,--doctests_only}'[fix up doctests only]'
4152  {-f,--fix}'[each FIX specifies a transformation; default: all]:fix name:_2to3_fixes'
4153  {-j,--processes}'[run 2to3 concurrently]:number: '
4154  {-x,--nofix}'[prevent a transformation from being run]:fix name:_2to3_fixes'
4155  {-l,--list-fixes}'[list available transformations]'
4156  {-p,--print-function}'[modify the grammar so that print() is a function]'
4157  {-v,--verbose}'[more verbose logging]'
4158  '--no-diffs[do not show diffs of the refactoring]'
4159  {-w,--write}'[write back modified files]'
4160  {-n,--nobackups}'[do not write backups for modified files]'
4161  {-o,--output-dir}'[put output files in this directory instead of overwriting]:directory:_directories'
4162  {-W,--write-unchanged-files}'[also write files even if no changes were required]'
4163  '--add-suffix[append this string to all output filenames]:suffix: '
4164  '*:filename:_files'
4165)
4166
4167_arguments -s -S $arguments
4168"##;
4169        let result = parse(input);
4170        assert!(
4171            result.is_ok(),
4172            "Failed to parse full completion file: {:?}",
4173            result.err()
4174        );
4175        let prog = result.unwrap();
4176        // Should have parsed successfully with at least one statement
4177        assert!(!prog.lists.is_empty(), "Expected at least one list");
4178    }
4179
4180    #[test]
4181    fn test_parse_logs_sh() {
4182        let input = r#"#!/usr/bin/env bash
4183shopt -s globstar
4184
4185if [[ $(uname) == Darwin ]]; then
4186    tail -f /var/log/**/*.log /var/log/**/*.out | lolcat
4187else
4188    if [[ $ZPWR_DISTRO_NAME == raspbian ]]; then
4189        tail -f /var/log/**/*.log | lolcat
4190    else
4191        printf "Unsupported...\n" >&2
4192    fi
4193fi
4194"#;
4195        let result = parse(input);
4196        assert!(
4197            result.is_ok(),
4198            "Failed to parse logs.sh: {:?}",
4199            result.err()
4200        );
4201    }
4202
4203    #[test]
4204    fn test_parse_case_with_glob() {
4205        let input = r#"case "$ZPWR_OS_TYPE" in
4206    darwin*)  open_cmd='open'
4207      ;;
4208    cygwin*)  open_cmd='cygstart'
4209      ;;
4210    linux*)
4211        open_cmd='xdg-open'
4212      ;;
4213esac"#;
4214        let result = parse(input);
4215        assert!(
4216            result.is_ok(),
4217            "Failed to parse case with glob: {:?}",
4218            result.err()
4219        );
4220    }
4221
4222    #[test]
4223    fn test_parse_case_with_nested_if() {
4224        // Test case with nested if and glob patterns
4225        let input = r##"function zpwrGetOpenCommand(){
4226    local open_cmd
4227    case "$ZPWR_OS_TYPE" in
4228        darwin*)  open_cmd='open' ;;
4229        cygwin*)  open_cmd='cygstart' ;;
4230        linux*)
4231            if [[ "$_zpwr_uname_r" != *icrosoft* ]];then
4232                open_cmd='nohup xdg-open'
4233            fi
4234            ;;
4235    esac
4236}"##;
4237        let result = parse(input);
4238        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
4239    }
4240
4241    #[test]
4242    fn test_parse_zpwr_scripts() {
4243        use std::fs;
4244        use std::path::Path;
4245        use std::sync::mpsc;
4246        use std::thread;
4247        use std::time::{Duration, Instant};
4248
4249        let scripts_dir = Path::new("/Users/wizard/.zpwr/scripts");
4250        if !scripts_dir.exists() {
4251            eprintln!("Skipping test: scripts directory not found");
4252            return;
4253        }
4254
4255        let mut total = 0;
4256        let mut passed = 0;
4257        let mut failed_files = Vec::new();
4258        let mut timeout_files = Vec::new();
4259
4260        for ext in &["sh", "zsh"] {
4261            let pattern = scripts_dir.join(format!("*.{}", ext));
4262            if let Ok(entries) = glob::glob(pattern.to_str().unwrap()) {
4263                for entry in entries.flatten() {
4264                    total += 1;
4265                    let file_path = entry.display().to_string();
4266                    let content = match fs::read_to_string(&entry) {
4267                        Ok(c) => c,
4268                        Err(e) => {
4269                            failed_files.push((file_path, format!("read error: {}", e)));
4270                            continue;
4271                        }
4272                    };
4273
4274                    // Parse with timeout
4275                    let content_clone = content.clone();
4276                    let (tx, rx) = mpsc::channel();
4277                    let handle = thread::spawn(move || {
4278                        let result = parse(&content_clone);
4279                        let _ = tx.send(result);
4280                    });
4281
4282                    match rx.recv_timeout(Duration::from_secs(2)) {
4283                        Ok(Ok(_)) => passed += 1,
4284                        Ok(Err(errors)) => {
4285                            let first_err = errors
4286                                .first()
4287                                .map(|e| format!("line {}: {}", e.line, e.message))
4288                                .unwrap_or_default();
4289                            failed_files.push((file_path, first_err));
4290                        }
4291                        Err(_) => {
4292                            timeout_files.push(file_path);
4293                            // Thread will be abandoned
4294                        }
4295                    }
4296                }
4297            }
4298        }
4299
4300        eprintln!("\n=== ZPWR Scripts Parse Results ===");
4301        eprintln!("Passed: {}/{}", passed, total);
4302
4303        if !timeout_files.is_empty() {
4304            eprintln!("\nTimeout files (>2s):");
4305            for file in &timeout_files {
4306                eprintln!("  {}", file);
4307            }
4308        }
4309
4310        if !failed_files.is_empty() {
4311            eprintln!("\nFailed files:");
4312            for (file, err) in &failed_files {
4313                eprintln!("  {} - {}", file, err);
4314            }
4315        }
4316
4317        // Allow some failures initially, but track progress
4318        let pass_rate = if total > 0 {
4319            (passed as f64 / total as f64) * 100.0
4320        } else {
4321            0.0
4322        };
4323        eprintln!("Pass rate: {:.1}%", pass_rate);
4324
4325        // Require at least 50% pass rate for now
4326        assert!(pass_rate >= 50.0, "Pass rate too low: {:.1}%", pass_rate);
4327    }
4328
4329    #[test]
4330    #[ignore] // Uses threads that can't be killed on timeout; use integration test instead
4331    fn test_parse_zsh_stdlib_functions() {
4332        use std::fs;
4333        use std::path::Path;
4334        use std::sync::mpsc;
4335        use std::thread;
4336        use std::time::Duration;
4337
4338        let functions_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/zsh_functions");
4339        if !functions_dir.exists() {
4340            eprintln!(
4341                "Skipping test: zsh_functions directory not found at {:?}",
4342                functions_dir
4343            );
4344            return;
4345        }
4346
4347        let mut total = 0;
4348        let mut passed = 0;
4349        let mut failed_files = Vec::new();
4350        let mut timeout_files = Vec::new();
4351
4352        if let Ok(entries) = fs::read_dir(&functions_dir) {
4353            for entry in entries.flatten() {
4354                let path = entry.path();
4355                if !path.is_file() {
4356                    continue;
4357                }
4358
4359                total += 1;
4360                let file_path = path.display().to_string();
4361                let content = match fs::read_to_string(&path) {
4362                    Ok(c) => c,
4363                    Err(e) => {
4364                        failed_files.push((file_path, format!("read error: {}", e)));
4365                        continue;
4366                    }
4367                };
4368
4369                // Parse with timeout
4370                let content_clone = content.clone();
4371                let (tx, rx) = mpsc::channel();
4372                thread::spawn(move || {
4373                    let result = parse(&content_clone);
4374                    let _ = tx.send(result);
4375                });
4376
4377                match rx.recv_timeout(Duration::from_secs(2)) {
4378                    Ok(Ok(_)) => passed += 1,
4379                    Ok(Err(errors)) => {
4380                        let first_err = errors
4381                            .first()
4382                            .map(|e| format!("line {}: {}", e.line, e.message))
4383                            .unwrap_or_default();
4384                        failed_files.push((file_path, first_err));
4385                    }
4386                    Err(_) => {
4387                        timeout_files.push(file_path);
4388                    }
4389                }
4390            }
4391        }
4392
4393        eprintln!("\n=== Zsh Stdlib Functions Parse Results ===");
4394        eprintln!("Passed: {}/{}", passed, total);
4395
4396        if !timeout_files.is_empty() {
4397            eprintln!("\nTimeout files (>2s): {}", timeout_files.len());
4398            for file in timeout_files.iter().take(10) {
4399                eprintln!("  {}", file);
4400            }
4401            if timeout_files.len() > 10 {
4402                eprintln!("  ... and {} more", timeout_files.len() - 10);
4403            }
4404        }
4405
4406        if !failed_files.is_empty() {
4407            eprintln!("\nFailed files: {}", failed_files.len());
4408            for (file, err) in failed_files.iter().take(20) {
4409                let filename = Path::new(file)
4410                    .file_name()
4411                    .unwrap_or_default()
4412                    .to_string_lossy();
4413                eprintln!("  {} - {}", filename, err);
4414            }
4415            if failed_files.len() > 20 {
4416                eprintln!("  ... and {} more", failed_files.len() - 20);
4417            }
4418        }
4419
4420        let pass_rate = if total > 0 {
4421            (passed as f64 / total as f64) * 100.0
4422        } else {
4423            0.0
4424        };
4425        eprintln!("Pass rate: {:.1}%", pass_rate);
4426
4427        // Require at least 50% pass rate
4428        assert!(pass_rate >= 50.0, "Pass rate too low: {:.1}%", pass_rate);
4429    }
4430}