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::{Serialize, Deserialize};
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 == '(' { depth += 1; }
1327                            if ch == ')' { depth -= 1; if depth == 0 { break; } }
1328                            cmd.push(ch);
1329                        }
1330                        // Parse the command substitution content into an AST
1331                        let mut sub_parser = ShellParser::new(&cmd);
1332                        if let Ok(cmds) = sub_parser.parse_script() {
1333                            if cmds.len() == 1 {
1334                                elements.push(ShellWord::CommandSub(Box::new(cmds.into_iter().next().unwrap())));
1335                            } else if !cmds.is_empty() {
1336                                // Multiple commands — wrap in a brace group
1337                                elements.push(ShellWord::CommandSub(Box::new(
1338                                    ShellCommand::Compound(CompoundCommand::BraceGroup(cmds))
1339                                )));
1340                            }
1341                        }
1342                    }
1343                    ' ' | '\t' | '\n' => {
1344                        if !current.is_empty() {
1345                            elements.push(ShellWord::Literal(current.clone()));
1346                            current.clear();
1347                        }
1348                    }
1349                    '\\' => {
1350                        if let Some(next) = chars.next() {
1351                            current.push(next);
1352                        }
1353                    }
1354                    _ => current.push(c),
1355                }
1356            }
1357        }
1358
1359        if !current.is_empty() {
1360            elements.push(ShellWord::Literal(current));
1361        }
1362
1363        elements
1364    }
1365
1366    fn advance(&mut self) -> ShellToken {
1367        std::mem::replace(&mut self.current, self.lexer.next_token())
1368    }
1369
1370    fn expect(&mut self, expected: ShellToken) -> Result<(), String> {
1371        if self.current == expected {
1372            self.advance();
1373            Ok(())
1374        } else {
1375            Err(format!("Expected {:?}, got {:?}", expected, self.current))
1376        }
1377    }
1378
1379    fn skip_newlines(&mut self) {
1380        while self.current == ShellToken::Newline {
1381            self.advance();
1382        }
1383    }
1384
1385    fn skip_separators(&mut self) {
1386        while self.current == ShellToken::Newline || self.current == ShellToken::Semi {
1387            self.advance();
1388        }
1389    }
1390
1391    #[tracing::instrument(level = "trace", skip_all)]
1392    pub fn parse_script(&mut self) -> Result<Vec<ShellCommand>, String> {
1393        let mut commands = Vec::new();
1394        self.skip_newlines();
1395        while self.current != ShellToken::Eof {
1396            if let Some(cmd) = self.parse_complete_command()? {
1397                commands.push(cmd);
1398            }
1399            self.skip_newlines();
1400        }
1401        Ok(commands)
1402    }
1403
1404    fn parse_complete_command(&mut self) -> Result<Option<ShellCommand>, String> {
1405        self.skip_newlines();
1406        if self.current == ShellToken::Eof {
1407            return Ok(None);
1408        }
1409        let cmd = self.parse_list()?;
1410        match &self.current {
1411            ShellToken::Newline | ShellToken::Semi | ShellToken::Amp => {
1412                self.advance();
1413            }
1414            _ => {}
1415        }
1416        Ok(Some(cmd))
1417    }
1418
1419    fn parse_list(&mut self) -> Result<ShellCommand, String> {
1420        let first = self.parse_pipeline()?;
1421        let mut items = vec![(first, ListOp::Semi)];
1422
1423        loop {
1424            let op = match &self.current {
1425                ShellToken::AmpAmp => ListOp::And,
1426                ShellToken::PipePipe => ListOp::Or,
1427                ShellToken::Semi => ListOp::Semi,
1428                ShellToken::Amp => ListOp::Amp,
1429                ShellToken::Newline => break,
1430                _ => break,
1431            };
1432
1433            self.advance();
1434            self.skip_newlines();
1435
1436            if let Some(last) = items.last_mut() {
1437                last.1 = op;
1438            }
1439
1440            if self.current == ShellToken::Eof
1441                || self.current == ShellToken::Then
1442                || self.current == ShellToken::Else
1443                || self.current == ShellToken::Elif
1444                || self.current == ShellToken::Fi
1445                || self.current == ShellToken::Do
1446                || self.current == ShellToken::Done
1447                || self.current == ShellToken::Esac
1448                || self.current == ShellToken::RBrace
1449                || self.current == ShellToken::RParen
1450            {
1451                break;
1452            }
1453
1454            let next = self.parse_pipeline()?;
1455            items.push((next, ListOp::Semi));
1456        }
1457
1458        if items.len() == 1 {
1459            let (cmd, op) = items.pop().unwrap();
1460            if matches!(op, ListOp::Amp) {
1461                Ok(ShellCommand::List(vec![(cmd, op)]))
1462            } else {
1463                Ok(cmd)
1464            }
1465        } else {
1466            Ok(ShellCommand::List(items))
1467        }
1468    }
1469
1470    fn parse_pipeline(&mut self) -> Result<ShellCommand, String> {
1471        let negated = if self.current == ShellToken::Bang {
1472            self.advance();
1473            true
1474        } else {
1475            false
1476        };
1477
1478        let first = self.parse_command()?;
1479        let mut cmds = vec![first];
1480
1481        while self.current == ShellToken::Pipe {
1482            self.advance();
1483            self.skip_newlines();
1484            cmds.push(self.parse_command()?);
1485        }
1486
1487        if cmds.len() == 1 && !negated {
1488            Ok(cmds.pop().unwrap())
1489        } else {
1490            Ok(ShellCommand::Pipeline(cmds, negated))
1491        }
1492    }
1493
1494    fn parse_command(&mut self) -> Result<ShellCommand, String> {
1495        let cmd = match &self.current {
1496            ShellToken::If => self.parse_if(),
1497            ShellToken::For => self.parse_for(),
1498            ShellToken::While => self.parse_while(),
1499            ShellToken::Until => self.parse_until(),
1500            ShellToken::Case => self.parse_case(),
1501            ShellToken::Repeat => self.parse_repeat(),
1502            ShellToken::LBrace => self.parse_brace_group_or_try(),
1503            ShellToken::LParen => {
1504                self.advance();
1505                if self.current == ShellToken::RParen {
1506                    self.advance();
1507                    self.skip_newlines();
1508                    if self.current == ShellToken::LBrace {
1509                        let body = self.parse_brace_group()?;
1510                        Ok(ShellCommand::FunctionDef(String::new(), Box::new(body)))
1511                    } else {
1512                        Ok(ShellCommand::Compound(CompoundCommand::Subshell(vec![])))
1513                    }
1514                } else {
1515                    self.skip_newlines();
1516                    let body = self.parse_compound_list()?;
1517                    self.expect(ShellToken::RParen)?;
1518                    Ok(ShellCommand::Compound(CompoundCommand::Subshell(body)))
1519                }
1520            }
1521            ShellToken::DoubleLBracket => self.parse_cond_command(),
1522            ShellToken::DoubleLParen => self.parse_arith_command(),
1523            ShellToken::Function => self.parse_function(),
1524            ShellToken::Coproc => self.parse_coproc(),
1525            _ => self.parse_simple_command(),
1526        }?;
1527
1528        let mut redirects = Vec::new();
1529        loop {
1530            if let ShellToken::Word(w) = &self.current {
1531                if w.chars().all(|c| c.is_ascii_digit()) {
1532                    let fd_str = w.clone();
1533                    self.advance();
1534                    match &self.current {
1535                        ShellToken::Less
1536                        | ShellToken::Greater
1537                        | ShellToken::GreaterGreater
1538                        | ShellToken::LessAmp
1539                        | ShellToken::GreaterAmp
1540                        | ShellToken::LessLess
1541                        | ShellToken::LessLessLess
1542                        | ShellToken::LessGreater
1543                        | ShellToken::GreaterPipe => {
1544                            let fd = fd_str.parse::<i32>().ok();
1545                            redirects.push(self.parse_redirect_with_fd(fd)?);
1546                            continue;
1547                        }
1548                        _ => break,
1549                    }
1550                }
1551            }
1552
1553            match &self.current {
1554                ShellToken::Less
1555                | ShellToken::Greater
1556                | ShellToken::GreaterGreater
1557                | ShellToken::LessAmp
1558                | ShellToken::GreaterAmp
1559                | ShellToken::LessLess
1560                | ShellToken::LessLessLess
1561                | ShellToken::LessGreater
1562                | ShellToken::GreaterPipe
1563                | ShellToken::AmpGreater
1564                | ShellToken::AmpGreaterGreater => {
1565                    redirects.push(self.parse_redirect_with_fd(None)?);
1566                }
1567                _ => break,
1568            }
1569        }
1570
1571        if !redirects.is_empty() {
1572            Ok(ShellCommand::Compound(CompoundCommand::WithRedirects(
1573                Box::new(cmd),
1574                redirects,
1575            )))
1576        } else {
1577            Ok(cmd)
1578        }
1579    }
1580
1581    fn parse_simple_command(&mut self) -> Result<ShellCommand, String> {
1582        let mut cmd = SimpleCommand {
1583            assignments: Vec::new(),
1584            words: Vec::new(),
1585            redirects: Vec::new(),
1586        };
1587
1588        loop {
1589            match &self.current {
1590                ShellToken::Word(w) => {
1591                    if w.starts_with('{') && w.ends_with('}') && w.len() > 2 {
1592                        let varname = w[1..w.len() - 1].to_string();
1593                        if varname.chars().all(|c| c.is_alphanumeric() || c == '_') {
1594                            let saved_word = w.clone();
1595                            self.advance();
1596                            match &self.current {
1597                                ShellToken::Less
1598                                | ShellToken::Greater
1599                                | ShellToken::GreaterGreater
1600                                | ShellToken::LessAmp
1601                                | ShellToken::GreaterAmp
1602                                | ShellToken::LessLess
1603                                | ShellToken::LessLessLess
1604                                | ShellToken::LessGreater
1605                                | ShellToken::GreaterPipe => {
1606                                    let mut redir = self.parse_redirect_with_fd(None)?;
1607                                    redir.fd_var = Some(varname);
1608                                    cmd.redirects.push(redir);
1609                                    continue;
1610                                }
1611                                _ => {
1612                                    cmd.words.push(ShellWord::Literal(saved_word));
1613                                    continue;
1614                                }
1615                            }
1616                        }
1617                    }
1618
1619                    if w.chars().all(|c| c.is_ascii_digit()) {
1620                        let fd_str = w.clone();
1621                        self.advance();
1622                        match &self.current {
1623                            ShellToken::Less
1624                            | ShellToken::Greater
1625                            | ShellToken::GreaterGreater
1626                            | ShellToken::LessAmp
1627                            | ShellToken::GreaterAmp
1628                            | ShellToken::LessLess
1629                            | ShellToken::LessLessLess
1630                            | ShellToken::LessGreater
1631                            | ShellToken::GreaterPipe => {
1632                                let fd = fd_str.parse::<i32>().ok();
1633                                cmd.redirects.push(self.parse_redirect_with_fd(fd)?);
1634                                continue;
1635                            }
1636                            _ => {
1637                                cmd.words.push(ShellWord::Literal(fd_str));
1638                                continue;
1639                            }
1640                        }
1641                    }
1642
1643                    if cmd.words.is_empty() && w.contains('=') && !w.starts_with('=') {
1644                        let (eq_pos, is_append) = if let Some(pos) = w.find("+=") {
1645                            (pos, true)
1646                        } else if let Some(pos) = w.find('=') {
1647                            (pos, false)
1648                        } else {
1649                            (0, false)
1650                        };
1651
1652                        if eq_pos > 0 {
1653                            let var = w[..eq_pos].to_string();
1654                            let val_start = if is_append { eq_pos + 2 } else { eq_pos + 1 };
1655                            let val = w[val_start..].to_string();
1656
1657                            let is_valid_var = if let Some(bracket_pos) = var.find('[') {
1658                                let name = &var[..bracket_pos];
1659                                let rest = &var[bracket_pos..];
1660                                name.chars().all(|c| c.is_alphanumeric() || c == '_')
1661                                    && rest.ends_with(']')
1662                            } else {
1663                                var.chars().all(|c| c.is_alphanumeric() || c == '_')
1664                            };
1665                            if is_valid_var {
1666                                if val.starts_with('(') && val.ends_with(')') {
1667                                    let array_content = &val[1..val.len() - 1];
1668                                    let elements = Self::parse_array_elements(array_content);
1669                                    cmd.assignments.push((
1670                                        var,
1671                                        ShellWord::ArrayLiteral(elements),
1672                                        is_append,
1673                                    ));
1674                                } else {
1675                                    cmd.assignments
1676                                        .push((var, ShellWord::Literal(val), is_append));
1677                                }
1678                                self.advance();
1679                                continue;
1680                            }
1681                        }
1682                    }
1683
1684                    cmd.words.push(self.parse_word()?);
1685                }
1686
1687                ShellToken::LBracket => {
1688                    cmd.words.push(ShellWord::Literal("[".to_string()));
1689                    self.advance();
1690                }
1691                ShellToken::RBracket => {
1692                    if !cmd.words.is_empty() {
1693                        cmd.words.push(ShellWord::Literal("]".to_string()));
1694                        self.advance();
1695                    } else {
1696                        break;
1697                    }
1698                }
1699                ShellToken::If
1700                | ShellToken::Then
1701                | ShellToken::Else
1702                | ShellToken::Elif
1703                | ShellToken::Fi
1704                | ShellToken::Case
1705                | ShellToken::Esac
1706                | ShellToken::For
1707                | ShellToken::While
1708                | ShellToken::Until
1709                | ShellToken::Do
1710                | ShellToken::Done
1711                | ShellToken::In
1712                | ShellToken::Function
1713                | ShellToken::Select
1714                | ShellToken::Time
1715                | ShellToken::Coproc
1716                | ShellToken::Repeat => {
1717                    if !cmd.words.is_empty() {
1718                        cmd.words.push(self.parse_word()?);
1719                    } else {
1720                        break;
1721                    }
1722                }
1723
1724                ShellToken::Typeset(ref name) => {
1725                    if cmd.words.is_empty() {
1726                        let name = name.clone();
1727                        cmd.words.push(ShellWord::Literal(name));
1728                        self.advance();
1729                    } else {
1730                        cmd.words.push(self.parse_word()?);
1731                    }
1732                }
1733
1734                ShellToken::Less
1735                | ShellToken::Greater
1736                | ShellToken::GreaterGreater
1737                | ShellToken::LessAmp
1738                | ShellToken::GreaterAmp
1739                | ShellToken::LessLess
1740                | ShellToken::LessLessLess
1741                | ShellToken::LessGreater
1742                | ShellToken::GreaterPipe
1743                | ShellToken::AmpGreater
1744                | ShellToken::AmpGreaterGreater
1745                | ShellToken::HereDoc(_, _) => {
1746                    cmd.redirects.push(self.parse_redirect_with_fd(None)?);
1747                }
1748
1749                _ => break,
1750            }
1751        }
1752
1753        if cmd.words.len() == 1 && self.current == ShellToken::LParen {
1754            if let ShellWord::Literal(name) = &cmd.words[0] {
1755                let name = name.clone();
1756                self.advance();
1757                self.expect(ShellToken::RParen)?;
1758                self.skip_newlines();
1759                let body = self.parse_command()?;
1760                return Ok(ShellCommand::FunctionDef(name, Box::new(body)));
1761            }
1762        }
1763
1764        Ok(ShellCommand::Simple(cmd))
1765    }
1766
1767    fn parse_word(&mut self) -> Result<ShellWord, String> {
1768        let token = self.advance();
1769        match token {
1770            ShellToken::Word(w) => Ok(ShellWord::Literal(w)),
1771            ShellToken::LBracket => Ok(ShellWord::Literal("[".to_string())),
1772            ShellToken::If => Ok(ShellWord::Literal("if".to_string())),
1773            ShellToken::Then => Ok(ShellWord::Literal("then".to_string())),
1774            ShellToken::Else => Ok(ShellWord::Literal("else".to_string())),
1775            ShellToken::Elif => Ok(ShellWord::Literal("elif".to_string())),
1776            ShellToken::Fi => Ok(ShellWord::Literal("fi".to_string())),
1777            ShellToken::Case => Ok(ShellWord::Literal("case".to_string())),
1778            ShellToken::Esac => Ok(ShellWord::Literal("esac".to_string())),
1779            ShellToken::For => Ok(ShellWord::Literal("for".to_string())),
1780            ShellToken::While => Ok(ShellWord::Literal("while".to_string())),
1781            ShellToken::Until => Ok(ShellWord::Literal("until".to_string())),
1782            ShellToken::Do => Ok(ShellWord::Literal("do".to_string())),
1783            ShellToken::Done => Ok(ShellWord::Literal("done".to_string())),
1784            ShellToken::In => Ok(ShellWord::Literal("in".to_string())),
1785            ShellToken::Function => Ok(ShellWord::Literal("function".to_string())),
1786            ShellToken::Select => Ok(ShellWord::Literal("select".to_string())),
1787            ShellToken::Time => Ok(ShellWord::Literal("time".to_string())),
1788            ShellToken::Coproc => Ok(ShellWord::Literal("coproc".to_string())),
1789            ShellToken::Typeset(name) => Ok(ShellWord::Literal(name)),
1790            ShellToken::Repeat => Ok(ShellWord::Literal("repeat".to_string())),
1791            _ => Err("Expected word".to_string()),
1792        }
1793    }
1794
1795    fn parse_redirect_with_fd(&mut self, fd: Option<i32>) -> Result<Redirect, String> {
1796        if let ShellToken::HereDoc(delimiter, content) = &self.current {
1797            let delimiter = delimiter.clone();
1798            let content = content.clone();
1799            self.advance();
1800            return Ok(Redirect {
1801                fd,
1802                op: RedirectOp::HereDoc,
1803                target: ShellWord::Literal(delimiter),
1804                heredoc_content: Some(content),
1805                fd_var: None,
1806            });
1807        }
1808
1809        let mut fd_var = None;
1810        if let ShellToken::Word(w) = &self.current {
1811            if w.starts_with('{') && w.ends_with('}') && w.len() > 2 {
1812                let varname = w[1..w.len() - 1].to_string();
1813                fd_var = Some(varname);
1814                self.advance();
1815            }
1816        }
1817
1818        let op = match self.advance() {
1819            ShellToken::Less => RedirectOp::Read,
1820            ShellToken::Greater => RedirectOp::Write,
1821            ShellToken::GreaterGreater => RedirectOp::Append,
1822            ShellToken::LessAmp => RedirectOp::DupRead,
1823            ShellToken::GreaterAmp => RedirectOp::DupWrite,
1824            ShellToken::LessLess => RedirectOp::HereDoc,
1825            ShellToken::LessLessLess => RedirectOp::HereString,
1826            ShellToken::LessGreater => RedirectOp::ReadWrite,
1827            ShellToken::GreaterPipe => RedirectOp::Clobber,
1828            ShellToken::AmpGreater => RedirectOp::WriteBoth,
1829            ShellToken::AmpGreaterGreater => RedirectOp::AppendBoth,
1830            _ => return Err("Expected redirect operator".to_string()),
1831        };
1832
1833        let target = self.parse_word()?;
1834
1835        Ok(Redirect {
1836            fd,
1837            op,
1838            target,
1839            heredoc_content: None,
1840            fd_var,
1841        })
1842    }
1843
1844    fn parse_if(&mut self) -> Result<ShellCommand, String> {
1845        let mut conditions = Vec::new();
1846        let mut else_part = None;
1847        let mut usebrace = false;
1848
1849        let mut xtok = self.current.clone();
1850        loop {
1851            if xtok == ShellToken::Fi {
1852                self.advance();
1853                break;
1854            }
1855
1856            self.advance();
1857
1858            if xtok == ShellToken::Else {
1859                break;
1860            }
1861
1862            self.skip_separators();
1863
1864            if xtok != ShellToken::If && xtok != ShellToken::Elif {
1865                return Err(format!("Expected If or Elif, got {:?}", xtok));
1866            }
1867
1868            let cond = self.parse_compound_list_until(&[ShellToken::Then, ShellToken::LBrace])?;
1869            self.skip_separators();
1870            xtok = ShellToken::Fi;
1871
1872            if self.current == ShellToken::Then {
1873                usebrace = false;
1874                self.advance();
1875                let body = self.parse_compound_list()?;
1876                conditions.push((cond, body));
1877            } else if self.current == ShellToken::LBrace {
1878                usebrace = true;
1879                self.advance();
1880                self.skip_separators();
1881                let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
1882                if self.current != ShellToken::RBrace {
1883                    return Err(format!("Expected RBrace, got {:?}", self.current));
1884                }
1885                conditions.push((cond, body));
1886                self.advance();
1887                if self.current == ShellToken::Newline || self.current == ShellToken::Semi {
1888                    break;
1889                }
1890            } else {
1891                return Err(format!(
1892                    "Expected Then or LBrace after condition, got {:?}",
1893                    self.current
1894                ));
1895            }
1896
1897            xtok = self.current.clone();
1898            if xtok != ShellToken::Elif && xtok != ShellToken::Else && xtok != ShellToken::Fi {
1899                break;
1900            }
1901        }
1902
1903        if xtok == ShellToken::Else || self.current == ShellToken::Else {
1904            if self.current == ShellToken::Else {
1905                self.advance();
1906            }
1907            self.skip_separators();
1908
1909            if self.current == ShellToken::LBrace && usebrace {
1910                self.advance();
1911                self.skip_separators();
1912                let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
1913                if self.current != ShellToken::RBrace {
1914                    return Err(format!("Expected RBrace in else, got {:?}", self.current));
1915                }
1916                self.advance();
1917                else_part = Some(body);
1918            } else {
1919                let body = self.parse_compound_list()?;
1920                if self.current != ShellToken::Fi {
1921                    return Err(format!("Expected Fi, got {:?}", self.current));
1922                }
1923                self.advance();
1924                else_part = Some(body);
1925            }
1926        }
1927
1928        Ok(ShellCommand::Compound(CompoundCommand::If {
1929            conditions,
1930            else_part,
1931        }))
1932    }
1933
1934    fn parse_for(&mut self) -> Result<ShellCommand, String> {
1935        self.expect(ShellToken::For)?;
1936        self.skip_newlines();
1937
1938        if self.current == ShellToken::DoubleLParen {
1939            return self.parse_for_arith();
1940        }
1941
1942        let var = if let ShellToken::Word(w) = self.advance() {
1943            w
1944        } else {
1945            return Err("Expected variable name after 'for'".to_string());
1946        };
1947
1948        while self.current == ShellToken::Newline {
1949            self.advance();
1950        }
1951
1952        let words = if self.current == ShellToken::In {
1953            self.advance();
1954            let mut words = Vec::new();
1955            while let ShellToken::Word(_) = &self.current {
1956                words.push(self.parse_word()?);
1957            }
1958            Some(words)
1959        } else if self.current == ShellToken::LParen {
1960            self.advance();
1961            let mut words = Vec::new();
1962            while self.current != ShellToken::RParen && self.current != ShellToken::Eof {
1963                if let ShellToken::Word(_) = &self.current {
1964                    words.push(self.parse_word()?);
1965                } else if self.current == ShellToken::Newline {
1966                    self.advance();
1967                } else {
1968                    break;
1969                }
1970            }
1971            self.expect(ShellToken::RParen)?;
1972            Some(words)
1973        } else {
1974            None
1975        };
1976
1977        self.skip_separators();
1978
1979        let body = if self.current == ShellToken::LBrace {
1980            self.advance();
1981            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
1982            self.expect(ShellToken::RBrace)?;
1983            body
1984        } else {
1985            self.expect(ShellToken::Do)?;
1986            let body = self.parse_compound_list()?;
1987            self.expect(ShellToken::Done)?;
1988            body
1989        };
1990
1991        Ok(ShellCommand::Compound(CompoundCommand::For {
1992            var,
1993            words,
1994            body,
1995        }))
1996    }
1997
1998    fn parse_for_arith(&mut self) -> Result<ShellCommand, String> {
1999        self.expect(ShellToken::DoubleLParen)?;
2000
2001        let mut parts = Vec::new();
2002        let mut current_part = String::new();
2003        let mut depth = 0;
2004
2005        loop {
2006            match &self.current {
2007                ShellToken::DoubleRParen if depth == 0 => break,
2008                ShellToken::DoubleLParen => {
2009                    depth += 1;
2010                    current_part.push_str("((");
2011                    self.advance();
2012                }
2013                ShellToken::DoubleRParen => {
2014                    depth -= 1;
2015                    current_part.push_str("))");
2016                    self.advance();
2017                }
2018                ShellToken::Semi => {
2019                    parts.push(current_part.trim().to_string());
2020                    current_part = String::new();
2021                    self.advance();
2022                }
2023                ShellToken::Word(w) => {
2024                    current_part.push_str(w);
2025                    current_part.push(' ');
2026                    self.advance();
2027                }
2028                ShellToken::Less => {
2029                    current_part.push('<');
2030                    self.advance();
2031                }
2032                ShellToken::Greater => {
2033                    current_part.push('>');
2034                    self.advance();
2035                }
2036                ShellToken::LessLess => {
2037                    current_part.push_str("<<");
2038                    self.advance();
2039                }
2040                ShellToken::GreaterGreater => {
2041                    current_part.push_str(">>");
2042                    self.advance();
2043                }
2044                _ => {
2045                    self.advance();
2046                }
2047            }
2048        }
2049        parts.push(current_part.trim().to_string());
2050
2051        self.expect(ShellToken::DoubleRParen)?;
2052        self.skip_newlines();
2053
2054        match &self.current {
2055            ShellToken::Semi | ShellToken::Newline => {
2056                self.advance();
2057            }
2058            _ => {}
2059        }
2060        self.skip_newlines();
2061
2062        self.expect(ShellToken::Do)?;
2063        self.skip_newlines();
2064        let body = self.parse_compound_list()?;
2065        self.expect(ShellToken::Done)?;
2066
2067        Ok(ShellCommand::Compound(CompoundCommand::ForArith {
2068            init: parts.first().cloned().unwrap_or_default(),
2069            cond: parts.get(1).cloned().unwrap_or_default(),
2070            step: parts.get(2).cloned().unwrap_or_default(),
2071            body,
2072        }))
2073    }
2074
2075    fn parse_while(&mut self) -> Result<ShellCommand, String> {
2076        self.expect(ShellToken::While)?;
2077        let condition = self.parse_compound_list_until(&[ShellToken::Do, ShellToken::LBrace])?;
2078        self.skip_separators();
2079
2080        let body = if self.current == ShellToken::LBrace {
2081            self.advance();
2082            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
2083            self.expect(ShellToken::RBrace)?;
2084            body
2085        } else {
2086            self.expect(ShellToken::Do)?;
2087            let body = self.parse_compound_list()?;
2088            self.expect(ShellToken::Done)?;
2089            body
2090        };
2091
2092        Ok(ShellCommand::Compound(CompoundCommand::While {
2093            condition,
2094            body,
2095        }))
2096    }
2097
2098    fn parse_until(&mut self) -> Result<ShellCommand, String> {
2099        self.expect(ShellToken::Until)?;
2100        let condition = self.parse_compound_list_until(&[ShellToken::Do, ShellToken::LBrace])?;
2101        self.skip_separators();
2102
2103        let body = if self.current == ShellToken::LBrace {
2104            self.advance();
2105            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
2106            self.expect(ShellToken::RBrace)?;
2107            body
2108        } else {
2109            self.expect(ShellToken::Do)?;
2110            let body = self.parse_compound_list()?;
2111            self.expect(ShellToken::Done)?;
2112            body
2113        };
2114
2115        Ok(ShellCommand::Compound(CompoundCommand::Until {
2116            condition,
2117            body,
2118        }))
2119    }
2120
2121    fn parse_case(&mut self) -> Result<ShellCommand, String> {
2122        self.expect(ShellToken::Case)?;
2123        self.skip_newlines();
2124        let word = self.parse_word()?;
2125        self.skip_newlines();
2126        self.expect(ShellToken::In)?;
2127        self.skip_newlines();
2128
2129        let mut cases = Vec::new();
2130
2131        while self.current != ShellToken::Esac {
2132            let mut patterns = Vec::new();
2133            if self.current == ShellToken::LParen {
2134                self.advance();
2135            }
2136
2137            loop {
2138                patterns.push(self.parse_word()?);
2139                if self.current == ShellToken::Pipe {
2140                    self.advance();
2141                } else {
2142                    break;
2143                }
2144            }
2145
2146            self.expect(ShellToken::RParen)?;
2147            self.skip_newlines();
2148
2149            let body = self.parse_compound_list()?;
2150
2151            let term = match &self.current {
2152                ShellToken::DoubleSemi => {
2153                    self.advance();
2154                    CaseTerminator::Break
2155                }
2156                ShellToken::SemiAmp => {
2157                    self.advance();
2158                    CaseTerminator::Fallthrough
2159                }
2160                ShellToken::SemiSemiAmp => {
2161                    self.advance();
2162                    CaseTerminator::Continue
2163                }
2164                _ => CaseTerminator::Break,
2165            };
2166
2167            cases.push((patterns, body, term));
2168            self.skip_newlines();
2169        }
2170
2171        self.expect(ShellToken::Esac)?;
2172
2173        Ok(ShellCommand::Compound(CompoundCommand::Case {
2174            word,
2175            cases,
2176        }))
2177    }
2178
2179    fn parse_repeat(&mut self) -> Result<ShellCommand, String> {
2180        self.expect(ShellToken::Repeat)?;
2181
2182        let count = match &self.current {
2183            ShellToken::Word(w) => {
2184                let c = w.clone();
2185                self.advance();
2186                c
2187            }
2188            _ => return Err("expected count after 'repeat'".to_string()),
2189        };
2190
2191        self.skip_separators();
2192
2193        let body = if self.current == ShellToken::LBrace {
2194            self.advance();
2195            self.skip_newlines();
2196            let body = self.parse_compound_list_until(&[ShellToken::RBrace])?;
2197            self.expect(ShellToken::RBrace)?;
2198            body
2199        } else if self.current == ShellToken::Do {
2200            self.expect(ShellToken::Do)?;
2201            let body = self.parse_compound_list()?;
2202            self.expect(ShellToken::Done)?;
2203            body
2204        } else {
2205            // repeat N SIMPLE_COMMAND (no braces or do/done needed)
2206            let cmd = self.parse_command()?;
2207            vec![cmd]
2208        };
2209
2210        Ok(ShellCommand::Compound(CompoundCommand::Repeat {
2211            count,
2212            body,
2213        }))
2214    }
2215
2216    fn parse_brace_group_or_try(&mut self) -> Result<ShellCommand, String> {
2217        self.expect(ShellToken::LBrace)?;
2218        self.skip_newlines();
2219        let try_body = self.parse_compound_list()?;
2220        self.expect(ShellToken::RBrace)?;
2221
2222        // C zsh: tok == STRING && !strcmp(tokstr, "always")
2223        // "always" is context-dependent, only keyword after OUTBRACE.
2224        if matches!(&self.current, ShellToken::Word(w) if w == "always") {
2225            self.advance();
2226            self.expect(ShellToken::LBrace)?;
2227            self.skip_newlines();
2228            let always_body = self.parse_compound_list()?;
2229            self.expect(ShellToken::RBrace)?;
2230
2231            Ok(ShellCommand::Compound(CompoundCommand::Try {
2232                try_body,
2233                always_body,
2234            }))
2235        } else {
2236            Ok(ShellCommand::Compound(CompoundCommand::BraceGroup(
2237                try_body,
2238            )))
2239        }
2240    }
2241
2242    fn parse_brace_group(&mut self) -> Result<ShellCommand, String> {
2243        self.expect(ShellToken::LBrace)?;
2244        self.skip_newlines();
2245        let body = self.parse_compound_list()?;
2246        self.expect(ShellToken::RBrace)?;
2247
2248        Ok(ShellCommand::Compound(CompoundCommand::BraceGroup(body)))
2249    }
2250
2251    fn parse_cond_command(&mut self) -> Result<ShellCommand, String> {
2252        self.expect(ShellToken::DoubleLBracket)?;
2253
2254        let mut tokens: Vec<String> = Vec::new();
2255        while self.current != ShellToken::DoubleRBracket && self.current != ShellToken::Eof {
2256            match &self.current {
2257                ShellToken::Word(w) => tokens.push(w.clone()),
2258                ShellToken::Bang => tokens.push("!".to_string()),
2259                ShellToken::AmpAmp => tokens.push("&&".to_string()),
2260                ShellToken::PipePipe => tokens.push("||".to_string()),
2261                ShellToken::LParen => tokens.push("(".to_string()),
2262                ShellToken::RParen => tokens.push(")".to_string()),
2263                ShellToken::Less => tokens.push("<".to_string()),
2264                ShellToken::Greater => tokens.push(">".to_string()),
2265                _ => {}
2266            }
2267            self.advance();
2268        }
2269
2270        self.expect(ShellToken::DoubleRBracket)?;
2271
2272        let expr = self.parse_cond_tokens(&tokens)?;
2273        Ok(ShellCommand::Compound(CompoundCommand::Cond(expr)))
2274    }
2275
2276    fn parse_cond_tokens(&self, tokens: &[String]) -> Result<CondExpr, String> {
2277        if tokens.is_empty() {
2278            return Ok(CondExpr::StringNonEmpty(ShellWord::Literal(String::new())));
2279        }
2280
2281        if tokens[0] == "!" {
2282            let inner = self.parse_cond_tokens(&tokens[1..])?;
2283            return Ok(CondExpr::Not(Box::new(inner)));
2284        }
2285
2286        // Precedence: || (lowest) > && > comparisons (highest).
2287        // Scan for || first, then &&, then binary operators.
2288        // This matches the C implementation's precedence in cond.c.
2289
2290        // Level 1: || (lowest precedence — split on rightmost to get left-associativity)
2291        for i in (0..tokens.len()).rev() {
2292            if tokens[i] == "||" {
2293                let left = self.parse_cond_tokens(&tokens[..i])?;
2294                let right = self.parse_cond_tokens(&tokens[i + 1..])?;
2295                return Ok(CondExpr::Or(Box::new(left), Box::new(right)));
2296            }
2297        }
2298
2299        // Level 2: &&
2300        for i in (0..tokens.len()).rev() {
2301            if tokens[i] == "&&" {
2302                let left = self.parse_cond_tokens(&tokens[..i])?;
2303                let right = self.parse_cond_tokens(&tokens[i + 1..])?;
2304                return Ok(CondExpr::And(Box::new(left), Box::new(right)));
2305            }
2306        }
2307
2308        // Level 3: binary comparison operators (highest precedence)
2309        for (i, tok) in tokens.iter().enumerate() {
2310            match tok.as_str() {
2311                "=" | "==" => {
2312                    let left = tokens[..i].join(" ");
2313                    let right = tokens[i + 1..].join(" ");
2314                    return Ok(CondExpr::StringEqual(
2315                        ShellWord::Literal(left),
2316                        ShellWord::Literal(right),
2317                    ));
2318                }
2319                "!=" => {
2320                    let left = tokens[..i].join(" ");
2321                    let right = tokens[i + 1..].join(" ");
2322                    return Ok(CondExpr::StringNotEqual(
2323                        ShellWord::Literal(left),
2324                        ShellWord::Literal(right),
2325                    ));
2326                }
2327                "=~" => {
2328                    let left = tokens[..i].join(" ");
2329                    let right = tokens[i + 1..].join(" ");
2330                    return Ok(CondExpr::StringMatch(
2331                        ShellWord::Literal(left),
2332                        ShellWord::Literal(right),
2333                    ));
2334                }
2335                "-eq" => {
2336                    let left = tokens[..i].join(" ");
2337                    let right = tokens[i + 1..].join(" ");
2338                    return Ok(CondExpr::NumEqual(
2339                        ShellWord::Literal(left),
2340                        ShellWord::Literal(right),
2341                    ));
2342                }
2343                "-ne" => {
2344                    let left = tokens[..i].join(" ");
2345                    let right = tokens[i + 1..].join(" ");
2346                    return Ok(CondExpr::NumNotEqual(
2347                        ShellWord::Literal(left),
2348                        ShellWord::Literal(right),
2349                    ));
2350                }
2351                "-lt" => {
2352                    let left = tokens[..i].join(" ");
2353                    let right = tokens[i + 1..].join(" ");
2354                    return Ok(CondExpr::NumLess(
2355                        ShellWord::Literal(left),
2356                        ShellWord::Literal(right),
2357                    ));
2358                }
2359                "-le" => {
2360                    let left = tokens[..i].join(" ");
2361                    let right = tokens[i + 1..].join(" ");
2362                    return Ok(CondExpr::NumLessEqual(
2363                        ShellWord::Literal(left),
2364                        ShellWord::Literal(right),
2365                    ));
2366                }
2367                "-gt" => {
2368                    let left = tokens[..i].join(" ");
2369                    let right = tokens[i + 1..].join(" ");
2370                    return Ok(CondExpr::NumGreater(
2371                        ShellWord::Literal(left),
2372                        ShellWord::Literal(right),
2373                    ));
2374                }
2375                "-ge" => {
2376                    let left = tokens[..i].join(" ");
2377                    let right = tokens[i + 1..].join(" ");
2378                    return Ok(CondExpr::NumGreaterEqual(
2379                        ShellWord::Literal(left),
2380                        ShellWord::Literal(right),
2381                    ));
2382                }
2383                "<" => {
2384                    let left = tokens[..i].join(" ");
2385                    let right = tokens[i + 1..].join(" ");
2386                    return Ok(CondExpr::StringLess(
2387                        ShellWord::Literal(left),
2388                        ShellWord::Literal(right),
2389                    ));
2390                }
2391                ">" => {
2392                    let left = tokens[..i].join(" ");
2393                    let right = tokens[i + 1..].join(" ");
2394                    return Ok(CondExpr::StringGreater(
2395                        ShellWord::Literal(left),
2396                        ShellWord::Literal(right),
2397                    ));
2398                }
2399                _ => {}
2400            }
2401        }
2402
2403        if tokens.len() >= 2 {
2404            let op = &tokens[0];
2405            let arg = tokens[1..].join(" ");
2406            match op.as_str() {
2407                "-e" => return Ok(CondExpr::FileExists(ShellWord::Literal(arg))),
2408                "-f" => return Ok(CondExpr::FileRegular(ShellWord::Literal(arg))),
2409                "-d" => return Ok(CondExpr::FileDirectory(ShellWord::Literal(arg))),
2410                "-L" | "-h" => return Ok(CondExpr::FileSymlink(ShellWord::Literal(arg))),
2411                "-r" => return Ok(CondExpr::FileReadable(ShellWord::Literal(arg))),
2412                "-w" => return Ok(CondExpr::FileWritable(ShellWord::Literal(arg))),
2413                "-x" => return Ok(CondExpr::FileExecutable(ShellWord::Literal(arg))),
2414                "-s" => return Ok(CondExpr::FileNonEmpty(ShellWord::Literal(arg))),
2415                "-z" => return Ok(CondExpr::StringEmpty(ShellWord::Literal(arg))),
2416                "-n" => return Ok(CondExpr::StringNonEmpty(ShellWord::Literal(arg))),
2417                _ => {}
2418            }
2419        }
2420
2421        let expr_str = tokens.join(" ");
2422        Ok(CondExpr::StringNonEmpty(ShellWord::Literal(expr_str)))
2423    }
2424
2425    fn parse_arith_command(&mut self) -> Result<ShellCommand, String> {
2426        self.expect(ShellToken::DoubleLParen)?;
2427
2428        let mut expr = String::new();
2429        let mut depth = 1;
2430
2431        while depth > 0 {
2432            match &self.current {
2433                ShellToken::DoubleLParen => {
2434                    depth += 1;
2435                    expr.push_str("((");
2436                }
2437                ShellToken::DoubleRParen => {
2438                    depth -= 1;
2439                    if depth > 0 {
2440                        expr.push_str("))");
2441                    }
2442                }
2443                ShellToken::Word(w) => {
2444                    expr.push_str(w);
2445                    expr.push(' ');
2446                }
2447                ShellToken::LParen => expr.push('('),
2448                ShellToken::RParen => expr.push(')'),
2449                ShellToken::LBracket => expr.push('['),
2450                ShellToken::RBracket => expr.push(']'),
2451                ShellToken::Less => expr.push('<'),
2452                ShellToken::Greater => expr.push('>'),
2453                ShellToken::LessLess => expr.push_str("<<"),
2454                ShellToken::GreaterGreater => expr.push_str(">>"),
2455                ShellToken::Bang => expr.push('!'),
2456                ShellToken::Eof => break,
2457                _ => {}
2458            }
2459            self.advance();
2460        }
2461
2462        Ok(ShellCommand::Compound(CompoundCommand::Arith(
2463            expr.trim().to_string(),
2464        )))
2465    }
2466
2467    fn parse_function(&mut self) -> Result<ShellCommand, String> {
2468        self.expect(ShellToken::Function)?;
2469        self.skip_newlines();
2470        let name = if let ShellToken::Word(w) = self.advance() {
2471            w
2472        } else {
2473            return Err("Expected function name".to_string());
2474        };
2475
2476        self.skip_newlines();
2477        if self.current == ShellToken::LParen {
2478            self.advance();
2479            self.expect(ShellToken::RParen)?;
2480            self.skip_newlines();
2481        }
2482
2483        let body = self.parse_command()?;
2484        Ok(ShellCommand::FunctionDef(name, Box::new(body)))
2485    }
2486
2487    fn parse_coproc(&mut self) -> Result<ShellCommand, String> {
2488        self.expect(ShellToken::Coproc)?;
2489        self.skip_newlines();
2490
2491        let name = if let ShellToken::Word(w) = &self.current {
2492            let n = w.clone();
2493            self.advance();
2494            self.skip_newlines();
2495            Some(n)
2496        } else {
2497            None
2498        };
2499
2500        let body = self.parse_command()?;
2501        Ok(ShellCommand::Compound(CompoundCommand::Coproc {
2502            name,
2503            body: Box::new(body),
2504        }))
2505    }
2506
2507    fn parse_compound_list(&mut self) -> Result<Vec<ShellCommand>, String> {
2508        let mut commands = Vec::new();
2509        self.skip_newlines();
2510
2511        while self.current != ShellToken::Eof
2512            && self.current != ShellToken::RBrace
2513            && self.current != ShellToken::RParen
2514            && self.current != ShellToken::Fi
2515            && self.current != ShellToken::Done
2516            && self.current != ShellToken::Esac
2517            && self.current != ShellToken::Elif
2518            && self.current != ShellToken::Else
2519            && self.current != ShellToken::DoubleSemi
2520            && self.current != ShellToken::SemiAmp
2521            && self.current != ShellToken::SemiSemiAmp
2522        {
2523            let cmd = self.parse_list()?;
2524            commands.push(cmd);
2525            match &self.current {
2526                ShellToken::Newline | ShellToken::Semi => {
2527                    self.advance();
2528                }
2529                _ => {}
2530            }
2531            self.skip_newlines();
2532        }
2533
2534        Ok(commands)
2535    }
2536
2537    fn parse_compound_list_until(
2538        &mut self,
2539        terminators: &[ShellToken],
2540    ) -> Result<Vec<ShellCommand>, String> {
2541        let mut commands = Vec::new();
2542        self.skip_newlines();
2543
2544        while self.current != ShellToken::Eof && !terminators.contains(&self.current) {
2545            let cmd = self.parse_list()?;
2546            commands.push(cmd);
2547            match &self.current {
2548                ShellToken::Newline | ShellToken::Semi => {
2549                    self.advance();
2550                }
2551                _ => {}
2552            }
2553            self.skip_newlines();
2554        }
2555
2556        Ok(commands)
2557    }
2558}
2559
2560/// The Zsh Parser
2561pub struct ZshParser<'a> {
2562    lexer: ZshLexer<'a>,
2563    errors: Vec<ParseError>,
2564    /// Global iteration counter to prevent infinite loops
2565    global_iterations: usize,
2566    /// Recursion depth counter to prevent stack overflow
2567    recursion_depth: usize,
2568}
2569
2570const MAX_RECURSION_DEPTH: usize = 500;
2571
2572impl<'a> ZshParser<'a> {
2573    /// Create a new parser
2574    pub fn new(input: &'a str) -> Self {
2575        ZshParser {
2576            lexer: ZshLexer::new(input),
2577            errors: Vec::new(),
2578            global_iterations: 0,
2579            recursion_depth: 0,
2580        }
2581    }
2582
2583    /// Check iteration limit; returns true if exceeded
2584    #[inline]
2585    fn check_limit(&mut self) -> bool {
2586        self.global_iterations += 1;
2587        self.global_iterations > 10_000
2588    }
2589
2590    /// Check recursion depth; returns true if exceeded
2591    #[inline]
2592    fn check_recursion(&mut self) -> bool {
2593        self.recursion_depth > MAX_RECURSION_DEPTH
2594    }
2595
2596    /// Parse the complete input
2597    pub fn parse(&mut self) -> Result<ZshProgram, Vec<ParseError>> {
2598        self.lexer.zshlex();
2599
2600        let program = self.parse_program_until(None);
2601
2602        if !self.errors.is_empty() {
2603            return Err(std::mem::take(&mut self.errors));
2604        }
2605
2606        Ok(program)
2607    }
2608
2609    /// Parse a program (list of lists)
2610    fn parse_program(&mut self) -> ZshProgram {
2611        self.parse_program_until(None)
2612    }
2613
2614    /// Parse a program until we hit an end token
2615    fn parse_program_until(&mut self, end_tokens: Option<&[LexTok]>) -> ZshProgram {
2616        let mut lists = Vec::new();
2617
2618        loop {
2619            if self.check_limit() {
2620                self.error("parser exceeded global iteration limit");
2621                break;
2622            }
2623
2624            // Skip separators
2625            while self.lexer.tok == LexTok::Seper || self.lexer.tok == LexTok::Newlin {
2626                if self.check_limit() {
2627                    self.error("parser exceeded global iteration limit");
2628                    return ZshProgram { lists };
2629                }
2630                self.lexer.zshlex();
2631            }
2632
2633            if self.lexer.tok == LexTok::Endinput || self.lexer.tok == LexTok::Lexerr {
2634                break;
2635            }
2636
2637            // Check for end tokens
2638            if let Some(end_toks) = end_tokens {
2639                if end_toks.contains(&self.lexer.tok) {
2640                    break;
2641                }
2642            }
2643
2644            // Also stop at these tokens when not explicitly looking for them
2645            // Note: Else/Elif/Then are NOT here - they're handled by parse_if
2646            // to allow nested if statements inside case arms, loops, etc.
2647            match self.lexer.tok {
2648                LexTok::Outbrace
2649                | LexTok::Dsemi
2650                | LexTok::Semiamp
2651                | LexTok::Semibar
2652                | LexTok::Done
2653                | LexTok::Fi
2654                | LexTok::Esac
2655                | LexTok::Zend => break,
2656                _ => {}
2657            }
2658
2659            match self.parse_list() {
2660                Some(list) => lists.push(list),
2661                None => break,
2662            }
2663        }
2664
2665        ZshProgram { lists }
2666    }
2667
2668    /// Parse a list (sublist with optional & or ;)
2669    fn parse_list(&mut self) -> Option<ZshList> {
2670        let sublist = self.parse_sublist()?;
2671
2672        let flags = match self.lexer.tok {
2673            LexTok::Amper => {
2674                self.lexer.zshlex();
2675                ListFlags {
2676                    async_: true,
2677                    disown: false,
2678                }
2679            }
2680            LexTok::Amperbang => {
2681                self.lexer.zshlex();
2682                ListFlags {
2683                    async_: true,
2684                    disown: true,
2685                }
2686            }
2687            LexTok::Seper | LexTok::Semi | LexTok::Newlin => {
2688                self.lexer.zshlex();
2689                ListFlags::default()
2690            }
2691            _ => ListFlags::default(),
2692        };
2693
2694        Some(ZshList { sublist, flags })
2695    }
2696
2697    /// Parse a sublist (pipelines connected by && or ||)
2698    fn parse_sublist(&mut self) -> Option<ZshSublist> {
2699        self.recursion_depth += 1;
2700        if self.check_recursion() {
2701            self.error("parse_sublist: max recursion depth exceeded");
2702            self.recursion_depth -= 1;
2703            return None;
2704        }
2705
2706        let mut flags = SublistFlags::default();
2707
2708        // Handle coproc and !
2709        if self.lexer.tok == LexTok::Coproc {
2710            flags.coproc = true;
2711            self.lexer.zshlex();
2712        } else if self.lexer.tok == LexTok::Bang {
2713            flags.not = true;
2714            self.lexer.zshlex();
2715        }
2716
2717        let pipe = match self.parse_pipe() {
2718            Some(p) => p,
2719            None => {
2720                self.recursion_depth -= 1;
2721                return None;
2722            }
2723        };
2724
2725        // Check for && or ||
2726        let next = match self.lexer.tok {
2727            LexTok::Damper => {
2728                self.lexer.zshlex();
2729                self.skip_separators();
2730                self.parse_sublist().map(|s| (SublistOp::And, Box::new(s)))
2731            }
2732            LexTok::Dbar => {
2733                self.lexer.zshlex();
2734                self.skip_separators();
2735                self.parse_sublist().map(|s| (SublistOp::Or, Box::new(s)))
2736            }
2737            _ => None,
2738        };
2739
2740        self.recursion_depth -= 1;
2741        Some(ZshSublist { pipe, next, flags })
2742    }
2743
2744    /// Parse a pipeline
2745    fn parse_pipe(&mut self) -> Option<ZshPipe> {
2746        self.recursion_depth += 1;
2747        if self.check_recursion() {
2748            self.error("parse_pipe: max recursion depth exceeded");
2749            self.recursion_depth -= 1;
2750            return None;
2751        }
2752
2753        let lineno = self.lexer.toklineno;
2754        let cmd = match self.parse_cmd() {
2755            Some(c) => c,
2756            None => {
2757                self.recursion_depth -= 1;
2758                return None;
2759            }
2760        };
2761
2762        // Check for | or |&
2763        let next = match self.lexer.tok {
2764            LexTok::Bar | LexTok::Baramp => {
2765                let _merge_stderr = self.lexer.tok == LexTok::Baramp;
2766                self.lexer.zshlex();
2767                self.skip_separators();
2768                self.parse_pipe().map(Box::new)
2769            }
2770            _ => None,
2771        };
2772
2773        self.recursion_depth -= 1;
2774        Some(ZshPipe { cmd, next, lineno })
2775    }
2776
2777    /// Parse a command
2778    fn parse_cmd(&mut self) -> Option<ZshCommand> {
2779        // Parse leading redirections
2780        let mut redirs = Vec::new();
2781        while self.lexer.tok.is_redirop() {
2782            if let Some(redir) = self.parse_redir() {
2783                redirs.push(redir);
2784            }
2785        }
2786
2787        let cmd = match self.lexer.tok {
2788            LexTok::For | LexTok::Foreach => self.parse_for(),
2789            LexTok::Select => self.parse_select(),
2790            LexTok::Case => self.parse_case(),
2791            LexTok::If => self.parse_if(),
2792            LexTok::While => self.parse_while(false),
2793            LexTok::Until => self.parse_while(true),
2794            LexTok::Repeat => self.parse_repeat(),
2795            LexTok::Inpar => self.parse_subsh(),
2796            LexTok::Inbrace => self.parse_cursh(),
2797            LexTok::Func => self.parse_funcdef(),
2798            LexTok::Dinbrack => self.parse_cond(),
2799            LexTok::Dinpar => self.parse_arith(),
2800            LexTok::Time => self.parse_time(),
2801            _ => self.parse_simple(redirs),
2802        };
2803
2804        // Parse trailing redirections
2805        if cmd.is_some() {
2806            while self.lexer.tok.is_redirop() {
2807                if let Some(_redir) = self.parse_redir() {
2808                    // Append to command redirections
2809                    // (for non-simple commands, we'd need to handle this differently)
2810                }
2811            }
2812        }
2813
2814        cmd
2815    }
2816
2817    /// Parse a simple command
2818    fn parse_simple(&mut self, mut redirs: Vec<ZshRedir>) -> Option<ZshCommand> {
2819        let mut assigns = Vec::new();
2820        let mut words = Vec::new();
2821        const MAX_ITERATIONS: usize = 10_000;
2822        let mut iterations = 0;
2823
2824        // Parse leading assignments
2825        while self.lexer.tok == LexTok::Envstring || self.lexer.tok == LexTok::Envarray {
2826            iterations += 1;
2827            if iterations > MAX_ITERATIONS {
2828                self.error("parse_simple: exceeded max iterations in assignments");
2829                return None;
2830            }
2831            if let Some(assign) = self.parse_assign() {
2832                assigns.push(assign);
2833            }
2834            self.lexer.zshlex();
2835        }
2836
2837        // Parse words and redirections
2838        loop {
2839            iterations += 1;
2840            if iterations > MAX_ITERATIONS {
2841                self.error("parse_simple: exceeded max iterations");
2842                return None;
2843            }
2844            match self.lexer.tok {
2845                LexTok::String | LexTok::Typeset => {
2846                    let s = self.lexer.tokstr.clone();
2847                    if let Some(s) = s {
2848                        words.push(s);
2849                    }
2850                    self.lexer.zshlex();
2851                    // Check for function definition foo() { ... }
2852                    if words.len() == 1 && self.peek_inoutpar() {
2853                        return self.parse_inline_funcdef(words.pop().unwrap());
2854                    }
2855                }
2856                _ if self.lexer.tok.is_redirop() => {
2857                    match self.parse_redir() {
2858                        Some(redir) => redirs.push(redir),
2859                        None => break, // Error in redir parsing, stop
2860                    }
2861                }
2862                LexTok::Inoutpar if !words.is_empty() => {
2863                    // foo() { ... } style function
2864                    return self.parse_inline_funcdef(words.pop().unwrap());
2865                }
2866                _ => break,
2867            }
2868        }
2869
2870        if assigns.is_empty() && words.is_empty() && redirs.is_empty() {
2871            return None;
2872        }
2873
2874        Some(ZshCommand::Simple(ZshSimple {
2875            assigns,
2876            words,
2877            redirs,
2878        }))
2879    }
2880
2881    /// Parse an assignment
2882    fn parse_assign(&mut self) -> Option<ZshAssign> {
2883        use crate::tokens::char_tokens;
2884
2885        let tokstr = self.lexer.tokstr.as_ref()?;
2886
2887        // Parse name=value or name+=value
2888        // The '=' is encoded as char_tokens::EQUALS in the token string
2889        let (name, value_str, append) = if let Some(pos) = tokstr.find(char_tokens::EQUALS) {
2890            let name_part = &tokstr[..pos];
2891            let (name, append) = if name_part.ends_with('+') {
2892                (&name_part[..name_part.len() - 1], true)
2893            } else {
2894                (name_part, false)
2895            };
2896            (
2897                name.to_string(),
2898                tokstr[pos + char_tokens::EQUALS.len_utf8()..].to_string(),
2899                append,
2900            )
2901        } else if let Some(pos) = tokstr.find('=') {
2902            // Fallback to literal '=' for compatibility
2903            let name_part = &tokstr[..pos];
2904            let (name, append) = if name_part.ends_with('+') {
2905                (&name_part[..name_part.len() - 1], true)
2906            } else {
2907                (name_part, false)
2908            };
2909            (name.to_string(), tokstr[pos + 1..].to_string(), append)
2910        } else {
2911            return None;
2912        };
2913
2914        let value = if self.lexer.tok == LexTok::Envarray {
2915            // Array assignment: name=(...)
2916            let mut elements = Vec::new();
2917            self.lexer.zshlex(); // skip past token
2918
2919            let mut arr_iters = 0;
2920            const MAX_ARRAY_ELEMENTS: usize = 10_000;
2921            while matches!(
2922                self.lexer.tok,
2923                LexTok::String | LexTok::Seper | LexTok::Newlin
2924            ) {
2925                arr_iters += 1;
2926                if arr_iters > MAX_ARRAY_ELEMENTS {
2927                    self.error("array assignment exceeded maximum elements");
2928                    break;
2929                }
2930                if self.lexer.tok == LexTok::String {
2931                    if let Some(ref s) = self.lexer.tokstr {
2932                        elements.push(s.clone());
2933                    }
2934                }
2935                self.lexer.zshlex();
2936            }
2937
2938            // Expect OUTPAR
2939            if self.lexer.tok == LexTok::Outpar {
2940                self.lexer.zshlex();
2941            }
2942
2943            ZshAssignValue::Array(elements)
2944        } else {
2945            ZshAssignValue::Scalar(value_str)
2946        };
2947
2948        Some(ZshAssign {
2949            name,
2950            value,
2951            append,
2952        })
2953    }
2954
2955    /// Parse a redirection
2956    fn parse_redir(&mut self) -> Option<ZshRedir> {
2957        let rtype = match self.lexer.tok {
2958            LexTok::Outang => RedirType::Write,
2959            LexTok::Outangbang => RedirType::Writenow,
2960            LexTok::Doutang => RedirType::Append,
2961            LexTok::Doutangbang => RedirType::Appendnow,
2962            LexTok::Inang => RedirType::Read,
2963            LexTok::Inoutang => RedirType::ReadWrite,
2964            LexTok::Dinang => RedirType::Heredoc,
2965            LexTok::Dinangdash => RedirType::HeredocDash,
2966            LexTok::Trinang => RedirType::Herestr,
2967            LexTok::Inangamp => RedirType::MergeIn,
2968            LexTok::Outangamp => RedirType::MergeOut,
2969            LexTok::Ampoutang => RedirType::ErrWrite,
2970            LexTok::Outangampbang => RedirType::ErrWritenow,
2971            LexTok::Doutangamp => RedirType::ErrAppend,
2972            LexTok::Doutangampbang => RedirType::ErrAppendnow,
2973            _ => return None,
2974        };
2975
2976        let fd = if self.lexer.tokfd >= 0 {
2977            self.lexer.tokfd
2978        } else if matches!(
2979            rtype,
2980            RedirType::Read
2981                | RedirType::ReadWrite
2982                | RedirType::MergeIn
2983                | RedirType::Heredoc
2984                | RedirType::HeredocDash
2985                | RedirType::Herestr
2986        ) {
2987            0
2988        } else {
2989            1
2990        };
2991
2992        self.lexer.zshlex();
2993
2994        let name = match self.lexer.tok {
2995            LexTok::String | LexTok::Envstring => {
2996                let n = self.lexer.tokstr.clone().unwrap_or_default();
2997                self.lexer.zshlex();
2998                n
2999            }
3000            _ => {
3001                self.error("expected word after redirection");
3002                return None;
3003            }
3004        };
3005
3006        // Handle heredoc
3007        let heredoc = if matches!(rtype, RedirType::Heredoc | RedirType::HeredocDash) {
3008            // Heredoc content will be filled in by the lexer
3009            None // Placeholder
3010        } else {
3011            None
3012        };
3013
3014        Some(ZshRedir {
3015            rtype,
3016            fd,
3017            name,
3018            heredoc,
3019            varid: None,
3020        })
3021    }
3022
3023    /// Parse for/foreach loop
3024    fn parse_for(&mut self) -> Option<ZshCommand> {
3025        let is_foreach = self.lexer.tok == LexTok::Foreach;
3026        self.lexer.zshlex();
3027
3028        // Check for C-style: for (( init; cond; step ))
3029        if self.lexer.tok == LexTok::Dinpar {
3030            return self.parse_for_cstyle();
3031        }
3032
3033        // Get variable name
3034        let var = match self.lexer.tok {
3035            LexTok::String => {
3036                let v = self.lexer.tokstr.clone().unwrap_or_default();
3037                self.lexer.zshlex();
3038                v
3039            }
3040            _ => {
3041                self.error("expected variable name in for");
3042                return None;
3043            }
3044        };
3045
3046        // Skip newlines
3047        self.skip_separators();
3048
3049        // Get list
3050        let list = if self.lexer.tok == LexTok::String {
3051            let s = self.lexer.tokstr.as_ref();
3052            if s.map(|s| s == "in").unwrap_or(false) {
3053                self.lexer.zshlex();
3054                let mut words = Vec::new();
3055                let mut word_count = 0;
3056                while self.lexer.tok == LexTok::String {
3057                    word_count += 1;
3058                    if word_count > 500 || self.check_limit() {
3059                        self.error("for: too many words");
3060                        return None;
3061                    }
3062                    if let Some(ref s) = self.lexer.tokstr {
3063                        words.push(s.clone());
3064                    }
3065                    self.lexer.zshlex();
3066                }
3067                ForList::Words(words)
3068            } else {
3069                ForList::Positional
3070            }
3071        } else if self.lexer.tok == LexTok::Inpar {
3072            // for var (...)
3073            self.lexer.zshlex();
3074            let mut words = Vec::new();
3075            let mut word_count = 0;
3076            while self.lexer.tok == LexTok::String || self.lexer.tok == LexTok::Seper {
3077                word_count += 1;
3078                if word_count > 500 || self.check_limit() {
3079                    self.error("for: too many words in parens");
3080                    return None;
3081                }
3082                if self.lexer.tok == LexTok::String {
3083                    if let Some(ref s) = self.lexer.tokstr {
3084                        words.push(s.clone());
3085                    }
3086                }
3087                self.lexer.zshlex();
3088            }
3089            if self.lexer.tok == LexTok::Outpar {
3090                self.lexer.zshlex();
3091            }
3092            ForList::Words(words)
3093        } else {
3094            ForList::Positional
3095        };
3096
3097        // Skip to body
3098        self.skip_separators();
3099
3100        // Parse body
3101        let body = self.parse_loop_body(is_foreach)?;
3102
3103        Some(ZshCommand::For(ZshFor {
3104            var,
3105            list,
3106            body: Box::new(body),
3107        }))
3108    }
3109
3110    /// Parse C-style for loop: for (( init; cond; step ))
3111    fn parse_for_cstyle(&mut self) -> Option<ZshCommand> {
3112        // We're at (( (Dinpar None) - the opening ((
3113        // Lexer returns:
3114        //   Dinpar None     - opening ((
3115        //   Dinpar "init"   - init expression, semicolon consumed
3116        //   Dinpar "cond"   - cond expression, semicolon consumed
3117        //   Doutpar "step"  - step expression, closing )) consumed
3118
3119        self.lexer.zshlex(); // Get init: Dinpar "i=0"
3120
3121        if self.lexer.tok != LexTok::Dinpar {
3122            self.error("expected init expression in for ((");
3123            return None;
3124        }
3125        let init = self.lexer.tokstr.clone().unwrap_or_default();
3126
3127        self.lexer.zshlex(); // Get cond: Dinpar "i<10"
3128
3129        if self.lexer.tok != LexTok::Dinpar {
3130            self.error("expected condition in for ((");
3131            return None;
3132        }
3133        let cond = self.lexer.tokstr.clone().unwrap_or_default();
3134
3135        self.lexer.zshlex(); // Get step: Doutpar "i++"
3136
3137        if self.lexer.tok != LexTok::Doutpar {
3138            self.error("expected )) in for");
3139            return None;
3140        }
3141        let step = self.lexer.tokstr.clone().unwrap_or_default();
3142
3143        self.lexer.zshlex(); // Move past ))
3144
3145        self.skip_separators();
3146        let body = self.parse_loop_body(false)?;
3147
3148        Some(ZshCommand::For(ZshFor {
3149            var: String::new(),
3150            list: ForList::CStyle { init, cond, step },
3151            body: Box::new(body),
3152        }))
3153    }
3154
3155    /// Parse select loop (same syntax as for)
3156    fn parse_select(&mut self) -> Option<ZshCommand> {
3157        self.parse_for()
3158    }
3159
3160    /// Parse case statement
3161    fn parse_case(&mut self) -> Option<ZshCommand> {
3162        self.lexer.zshlex(); // skip 'case'
3163
3164        let word = match self.lexer.tok {
3165            LexTok::String => {
3166                let w = self.lexer.tokstr.clone().unwrap_or_default();
3167                self.lexer.zshlex();
3168                w
3169            }
3170            _ => {
3171                self.error("expected word after case");
3172                return None;
3173            }
3174        };
3175
3176        self.skip_separators();
3177
3178        // Expect 'in' or {
3179        let use_brace = self.lexer.tok == LexTok::Inbrace;
3180        if self.lexer.tok == LexTok::String {
3181            let s = self.lexer.tokstr.as_ref();
3182            if s.map(|s| s != "in").unwrap_or(true) {
3183                self.error("expected 'in' in case");
3184                return None;
3185            }
3186        } else if !use_brace {
3187            self.error("expected 'in' or '{' in case");
3188            return None;
3189        }
3190        self.lexer.zshlex();
3191
3192        let mut arms = Vec::new();
3193        const MAX_ARMS: usize = 10_000;
3194
3195        loop {
3196            if arms.len() > MAX_ARMS {
3197                self.error("parse_case: too many arms");
3198                break;
3199            }
3200
3201            // Set incasepat BEFORE skipping separators so lexer knows we're in case pattern context
3202            // This affects how [ and | are lexed
3203            self.lexer.incasepat = 1;
3204
3205            self.skip_separators();
3206
3207            // Check for end
3208            // Note: 'esac' might be String "esac" if incasepat > 0 prevents reserved word recognition
3209            let is_esac = self.lexer.tok == LexTok::Esac
3210                || (self.lexer.tok == LexTok::String
3211                    && self
3212                        .lexer
3213                        .tokstr
3214                        .as_ref()
3215                        .map(|s| s == "esac")
3216                        .unwrap_or(false));
3217            if (use_brace && self.lexer.tok == LexTok::Outbrace) || (!use_brace && is_esac) {
3218                self.lexer.incasepat = 0;
3219                self.lexer.zshlex();
3220                break;
3221            }
3222
3223            // Also break on EOF
3224            if self.lexer.tok == LexTok::Endinput || self.lexer.tok == LexTok::Lexerr {
3225                self.lexer.incasepat = 0;
3226                break;
3227            }
3228
3229            // Skip optional (
3230            if self.lexer.tok == LexTok::Inpar {
3231                self.lexer.zshlex();
3232            }
3233
3234            // incasepat is already set above
3235            let mut patterns = Vec::new();
3236            let mut pattern_iterations = 0;
3237            loop {
3238                pattern_iterations += 1;
3239                if pattern_iterations > 1000 {
3240                    self.error("parse_case: too many pattern iterations");
3241                    self.lexer.incasepat = 0;
3242                    return None;
3243                }
3244
3245                if self.lexer.tok == LexTok::String {
3246                    let s = self.lexer.tokstr.as_ref();
3247                    if s.map(|s| s == "esac").unwrap_or(false) {
3248                        break;
3249                    }
3250                    patterns.push(self.lexer.tokstr.clone().unwrap_or_default());
3251                    // After first pattern token, set incasepat=2 so ( is treated as part of pattern
3252                    self.lexer.incasepat = 2;
3253                    self.lexer.zshlex();
3254                } else if self.lexer.tok != LexTok::Bar {
3255                    break;
3256                }
3257
3258                if self.lexer.tok == LexTok::Bar {
3259                    // Reset to 1 (start of next alternative pattern)
3260                    self.lexer.incasepat = 1;
3261                    self.lexer.zshlex();
3262                } else {
3263                    break;
3264                }
3265            }
3266            self.lexer.incasepat = 0;
3267
3268            // Expect )
3269            if self.lexer.tok != LexTok::Outpar {
3270                self.error("expected ')' in case pattern");
3271                return None;
3272            }
3273            self.lexer.zshlex();
3274
3275            // Parse body
3276            let body = self.parse_program();
3277
3278            // Get terminator
3279            let terminator = match self.lexer.tok {
3280                LexTok::Dsemi => {
3281                    self.lexer.zshlex();
3282                    CaseTerm::Break
3283                }
3284                LexTok::Semiamp => {
3285                    self.lexer.zshlex();
3286                    CaseTerm::Continue
3287                }
3288                LexTok::Semibar => {
3289                    self.lexer.zshlex();
3290                    CaseTerm::TestNext
3291                }
3292                _ => CaseTerm::Break,
3293            };
3294
3295            if !patterns.is_empty() {
3296                arms.push(CaseArm {
3297                    patterns,
3298                    body,
3299                    terminator,
3300                });
3301            }
3302        }
3303
3304        Some(ZshCommand::Case(ZshCase { word, arms }))
3305    }
3306
3307    /// Parse if statement
3308    fn parse_if(&mut self) -> Option<ZshCommand> {
3309        self.lexer.zshlex(); // skip 'if'
3310
3311        // Parse condition - stops at 'then' or '{' (zsh allows { instead of then)
3312        let cond = Box::new(self.parse_program_until(Some(&[LexTok::Then, LexTok::Inbrace])));
3313
3314        self.skip_separators();
3315
3316        // Expect 'then' or {
3317        let use_brace = self.lexer.tok == LexTok::Inbrace;
3318        if self.lexer.tok != LexTok::Then && !use_brace {
3319            self.error("expected 'then' or '{' after if condition");
3320            return None;
3321        }
3322        self.lexer.zshlex();
3323
3324        // Parse then-body - stops at else/elif/fi, or } if using brace syntax
3325        let then = if use_brace {
3326            let body = self.parse_program_until(Some(&[LexTok::Outbrace]));
3327            if self.lexer.tok == LexTok::Outbrace {
3328                self.lexer.zshlex();
3329            }
3330            Box::new(body)
3331        } else {
3332            Box::new(self.parse_program_until(Some(&[LexTok::Else, LexTok::Elif, LexTok::Fi])))
3333        };
3334
3335        // Parse elif and else (only for then/fi syntax, not brace syntax)
3336        let mut elif = Vec::new();
3337        let mut else_ = None;
3338
3339        if !use_brace {
3340            loop {
3341                self.skip_separators();
3342
3343                match self.lexer.tok {
3344                    LexTok::Elif => {
3345                        self.lexer.zshlex();
3346                        // elif condition stops at 'then' or '{'
3347                        let econd =
3348                            self.parse_program_until(Some(&[LexTok::Then, LexTok::Inbrace]));
3349                        self.skip_separators();
3350
3351                        let elif_use_brace = self.lexer.tok == LexTok::Inbrace;
3352                        if self.lexer.tok != LexTok::Then && !elif_use_brace {
3353                            self.error("expected 'then' after elif");
3354                            return None;
3355                        }
3356                        self.lexer.zshlex();
3357
3358                        // elif body stops at else/elif/fi or } if using braces
3359                        let ebody = if elif_use_brace {
3360                            let body = self.parse_program_until(Some(&[LexTok::Outbrace]));
3361                            if self.lexer.tok == LexTok::Outbrace {
3362                                self.lexer.zshlex();
3363                            }
3364                            body
3365                        } else {
3366                            self.parse_program_until(Some(&[
3367                                LexTok::Else,
3368                                LexTok::Elif,
3369                                LexTok::Fi,
3370                            ]))
3371                        };
3372
3373                        elif.push((econd, ebody));
3374                    }
3375                    LexTok::Else => {
3376                        self.lexer.zshlex();
3377                        self.skip_separators();
3378
3379                        let else_use_brace = self.lexer.tok == LexTok::Inbrace;
3380                        if else_use_brace {
3381                            self.lexer.zshlex();
3382                        }
3383
3384                        // else body stops at 'fi' or '}'
3385                        else_ = Some(Box::new(if else_use_brace {
3386                            let body = self.parse_program_until(Some(&[LexTok::Outbrace]));
3387                            if self.lexer.tok == LexTok::Outbrace {
3388                                self.lexer.zshlex();
3389                            }
3390                            body
3391                        } else {
3392                            self.parse_program_until(Some(&[LexTok::Fi]))
3393                        }));
3394
3395                        // Consume the 'fi' if present (not for brace syntax)
3396                        if !else_use_brace && self.lexer.tok == LexTok::Fi {
3397                            self.lexer.zshlex();
3398                        }
3399                        break;
3400                    }
3401                    LexTok::Fi => {
3402                        self.lexer.zshlex();
3403                        break;
3404                    }
3405                    _ => break,
3406                }
3407            }
3408        }
3409
3410        Some(ZshCommand::If(ZshIf {
3411            cond,
3412            then,
3413            elif,
3414            else_,
3415        }))
3416    }
3417
3418    /// Parse while/until loop
3419    fn parse_while(&mut self, until: bool) -> Option<ZshCommand> {
3420        self.lexer.zshlex(); // skip while/until
3421
3422        let cond = Box::new(self.parse_program());
3423
3424        self.skip_separators();
3425        let body = self.parse_loop_body(false)?;
3426
3427        Some(ZshCommand::While(ZshWhile {
3428            cond,
3429            body: Box::new(body),
3430            until,
3431        }))
3432    }
3433
3434    /// Parse repeat loop
3435    fn parse_repeat(&mut self) -> Option<ZshCommand> {
3436        self.lexer.zshlex(); // skip 'repeat'
3437
3438        let count = match self.lexer.tok {
3439            LexTok::String => {
3440                let c = self.lexer.tokstr.clone().unwrap_or_default();
3441                self.lexer.zshlex();
3442                c
3443            }
3444            _ => {
3445                self.error("expected count after repeat");
3446                return None;
3447            }
3448        };
3449
3450        self.skip_separators();
3451        let body = self.parse_loop_body(false)?;
3452
3453        Some(ZshCommand::Repeat(ZshRepeat {
3454            count,
3455            body: Box::new(body),
3456        }))
3457    }
3458
3459    /// Parse loop body (do...done, {...}, or shortloop)
3460    fn parse_loop_body(&mut self, foreach_style: bool) -> Option<ZshProgram> {
3461        if self.lexer.tok == LexTok::Doloop {
3462            self.lexer.zshlex();
3463            let body = self.parse_program();
3464            if self.lexer.tok == LexTok::Done {
3465                self.lexer.zshlex();
3466            }
3467            Some(body)
3468        } else if self.lexer.tok == LexTok::Inbrace {
3469            self.lexer.zshlex();
3470            let body = self.parse_program();
3471            if self.lexer.tok == LexTok::Outbrace {
3472                self.lexer.zshlex();
3473            }
3474            Some(body)
3475        } else if foreach_style {
3476            // foreach allows 'end' terminator
3477            let body = self.parse_program();
3478            if self.lexer.tok == LexTok::Zend {
3479                self.lexer.zshlex();
3480            }
3481            Some(body)
3482        } else {
3483            // Short loop - single command
3484            match self.parse_list() {
3485                Some(list) => Some(ZshProgram { lists: vec![list] }),
3486                None => None,
3487            }
3488        }
3489    }
3490
3491    /// Parse (...) subshell
3492    fn parse_subsh(&mut self) -> Option<ZshCommand> {
3493        self.lexer.zshlex(); // skip (
3494        let prog = self.parse_program();
3495        if self.lexer.tok == LexTok::Outpar {
3496            self.lexer.zshlex();
3497        }
3498        Some(ZshCommand::Subsh(Box::new(prog)))
3499    }
3500
3501    /// Parse {...} cursh
3502    fn parse_cursh(&mut self) -> Option<ZshCommand> {
3503        self.lexer.zshlex(); // skip {
3504        let prog = self.parse_program();
3505
3506        // Check for { ... } always { ... }
3507        if self.lexer.tok == LexTok::Outbrace {
3508            self.lexer.zshlex();
3509
3510            // Check for 'always'
3511            if self.lexer.tok == LexTok::String {
3512                let s = self.lexer.tokstr.as_ref();
3513                if s.map(|s| s == "always").unwrap_or(false) {
3514                    self.lexer.zshlex();
3515                    self.skip_separators();
3516
3517                    if self.lexer.tok == LexTok::Inbrace {
3518                        self.lexer.zshlex();
3519                        let always = self.parse_program();
3520                        if self.lexer.tok == LexTok::Outbrace {
3521                            self.lexer.zshlex();
3522                        }
3523                        return Some(ZshCommand::Try(ZshTry {
3524                            try_block: Box::new(prog),
3525                            always: Box::new(always),
3526                        }));
3527                    }
3528                }
3529            }
3530        }
3531
3532        Some(ZshCommand::Cursh(Box::new(prog)))
3533    }
3534
3535    /// Parse function definition
3536    fn parse_funcdef(&mut self) -> Option<ZshCommand> {
3537        self.lexer.zshlex(); // skip 'function'
3538
3539        let mut names = Vec::new();
3540        let mut tracing = false;
3541
3542        // Handle options like -T and function names
3543        loop {
3544            match self.lexer.tok {
3545                LexTok::String => {
3546                    let s = self.lexer.tokstr.as_ref()?;
3547                    if s.starts_with('-') {
3548                        if s.contains('T') {
3549                            tracing = true;
3550                        }
3551                        self.lexer.zshlex();
3552                        continue;
3553                    }
3554                    names.push(s.clone());
3555                    self.lexer.zshlex();
3556                }
3557                LexTok::Inbrace | LexTok::Inoutpar | LexTok::Seper | LexTok::Newlin => break,
3558                _ => break,
3559            }
3560        }
3561
3562        // Optional ()
3563        if self.lexer.tok == LexTok::Inoutpar {
3564            self.lexer.zshlex();
3565        }
3566
3567        self.skip_separators();
3568
3569        // Parse body
3570        if self.lexer.tok == LexTok::Inbrace {
3571            self.lexer.zshlex();
3572            let body = self.parse_program();
3573            if self.lexer.tok == LexTok::Outbrace {
3574                self.lexer.zshlex();
3575            }
3576            Some(ZshCommand::FuncDef(ZshFuncDef {
3577                names,
3578                body: Box::new(body),
3579                tracing,
3580            }))
3581        } else {
3582            // Short form
3583            match self.parse_list() {
3584                Some(list) => Some(ZshCommand::FuncDef(ZshFuncDef {
3585                    names,
3586                    body: Box::new(ZshProgram { lists: vec![list] }),
3587                    tracing,
3588                })),
3589                None => None,
3590            }
3591        }
3592    }
3593
3594    /// Parse inline function definition: name() { ... }
3595    fn parse_inline_funcdef(&mut self, name: String) -> Option<ZshCommand> {
3596        // Skip ()
3597        if self.lexer.tok == LexTok::Inoutpar {
3598            self.lexer.zshlex();
3599        }
3600
3601        self.skip_separators();
3602
3603        // Parse body
3604        if self.lexer.tok == LexTok::Inbrace {
3605            self.lexer.zshlex();
3606            let body = self.parse_program();
3607            if self.lexer.tok == LexTok::Outbrace {
3608                self.lexer.zshlex();
3609            }
3610            Some(ZshCommand::FuncDef(ZshFuncDef {
3611                names: vec![name],
3612                body: Box::new(body),
3613                tracing: false,
3614            }))
3615        } else {
3616            match self.parse_cmd() {
3617                Some(cmd) => {
3618                    let list = ZshList {
3619                        sublist: ZshSublist {
3620                            pipe: ZshPipe {
3621                                cmd,
3622                                next: None,
3623                                lineno: self.lexer.lineno,
3624                            },
3625                            next: None,
3626                            flags: SublistFlags::default(),
3627                        },
3628                        flags: ListFlags::default(),
3629                    };
3630                    Some(ZshCommand::FuncDef(ZshFuncDef {
3631                        names: vec![name],
3632                        body: Box::new(ZshProgram { lists: vec![list] }),
3633                        tracing: false,
3634                    }))
3635                }
3636                None => None,
3637            }
3638        }
3639    }
3640
3641    /// Parse [[ ... ]] conditional
3642    fn parse_cond(&mut self) -> Option<ZshCommand> {
3643        self.lexer.zshlex(); // skip [[
3644        let cond = self.parse_cond_expr();
3645
3646        if self.lexer.tok == LexTok::Doutbrack {
3647            self.lexer.zshlex();
3648        }
3649
3650        cond.map(ZshCommand::Cond)
3651    }
3652
3653    /// Parse conditional expression
3654    fn parse_cond_expr(&mut self) -> Option<ZshCond> {
3655        self.parse_cond_or()
3656    }
3657
3658    fn parse_cond_or(&mut self) -> Option<ZshCond> {
3659        self.recursion_depth += 1;
3660        if self.check_recursion() {
3661            self.error("parse_cond_or: max recursion depth exceeded");
3662            self.recursion_depth -= 1;
3663            return None;
3664        }
3665
3666        let left = match self.parse_cond_and() {
3667            Some(l) => l,
3668            None => {
3669                self.recursion_depth -= 1;
3670                return None;
3671            }
3672        };
3673
3674        self.skip_cond_separators();
3675
3676        let result = if self.lexer.tok == LexTok::Dbar {
3677            self.lexer.zshlex();
3678            self.skip_cond_separators();
3679            match self.parse_cond_or() {
3680                Some(right) => Some(ZshCond::Or(Box::new(left), Box::new(right))),
3681                None => None,
3682            }
3683        } else {
3684            Some(left)
3685        };
3686
3687        self.recursion_depth -= 1;
3688        result
3689    }
3690
3691    fn parse_cond_and(&mut self) -> Option<ZshCond> {
3692        self.recursion_depth += 1;
3693        if self.check_recursion() {
3694            self.error("parse_cond_and: max recursion depth exceeded");
3695            self.recursion_depth -= 1;
3696            return None;
3697        }
3698
3699        let left = match self.parse_cond_not() {
3700            Some(l) => l,
3701            None => {
3702                self.recursion_depth -= 1;
3703                return None;
3704            }
3705        };
3706
3707        self.skip_cond_separators();
3708
3709        let result = if self.lexer.tok == LexTok::Damper {
3710            self.lexer.zshlex();
3711            self.skip_cond_separators();
3712            match self.parse_cond_and() {
3713                Some(right) => Some(ZshCond::And(Box::new(left), Box::new(right))),
3714                None => None,
3715            }
3716        } else {
3717            Some(left)
3718        };
3719
3720        self.recursion_depth -= 1;
3721        result
3722    }
3723
3724    fn parse_cond_not(&mut self) -> Option<ZshCond> {
3725        self.recursion_depth += 1;
3726        if self.check_recursion() {
3727            self.error("parse_cond_not: max recursion depth exceeded");
3728            self.recursion_depth -= 1;
3729            return None;
3730        }
3731
3732        self.skip_cond_separators();
3733
3734        // ! can be either LexTok::Bang or String "!"
3735        let is_not = self.lexer.tok == LexTok::Bang
3736            || (self.lexer.tok == LexTok::String
3737                && self
3738                    .lexer
3739                    .tokstr
3740                    .as_ref()
3741                    .map(|s| s == "!")
3742                    .unwrap_or(false));
3743        if is_not {
3744            self.lexer.zshlex();
3745            let inner = match self.parse_cond_not() {
3746                Some(i) => i,
3747                None => {
3748                    self.recursion_depth -= 1;
3749                    return None;
3750                }
3751            };
3752            self.recursion_depth -= 1;
3753            return Some(ZshCond::Not(Box::new(inner)));
3754        }
3755
3756        if self.lexer.tok == LexTok::Inpar {
3757            self.lexer.zshlex();
3758            self.skip_cond_separators();
3759            let inner = match self.parse_cond_expr() {
3760                Some(i) => i,
3761                None => {
3762                    self.recursion_depth -= 1;
3763                    return None;
3764                }
3765            };
3766            self.skip_cond_separators();
3767            if self.lexer.tok == LexTok::Outpar {
3768                self.lexer.zshlex();
3769            }
3770            self.recursion_depth -= 1;
3771            return Some(inner);
3772        }
3773
3774        let result = self.parse_cond_primary();
3775        self.recursion_depth -= 1;
3776        result
3777    }
3778
3779    fn parse_cond_primary(&mut self) -> Option<ZshCond> {
3780        let s1 = match self.lexer.tok {
3781            LexTok::String => {
3782                let s = self.lexer.tokstr.clone().unwrap_or_default();
3783                self.lexer.zshlex();
3784                s
3785            }
3786            _ => return None,
3787        };
3788
3789        self.skip_cond_separators();
3790
3791        // Check for unary operator
3792        if s1.starts_with('-') && s1.len() == 2 {
3793            let s2 = match self.lexer.tok {
3794                LexTok::String => {
3795                    let s = self.lexer.tokstr.clone().unwrap_or_default();
3796                    self.lexer.zshlex();
3797                    s
3798                }
3799                _ => return Some(ZshCond::Unary("-n".to_string(), s1)),
3800            };
3801            return Some(ZshCond::Unary(s1, s2));
3802        }
3803
3804        // Check for binary operator
3805        let op = match self.lexer.tok {
3806            LexTok::String => {
3807                let s = self.lexer.tokstr.clone().unwrap_or_default();
3808                self.lexer.zshlex();
3809                s
3810            }
3811            LexTok::Inang => {
3812                self.lexer.zshlex();
3813                "<".to_string()
3814            }
3815            LexTok::Outang => {
3816                self.lexer.zshlex();
3817                ">".to_string()
3818            }
3819            _ => return Some(ZshCond::Unary("-n".to_string(), s1)),
3820        };
3821
3822        self.skip_cond_separators();
3823
3824        let s2 = match self.lexer.tok {
3825            LexTok::String => {
3826                let s = self.lexer.tokstr.clone().unwrap_or_default();
3827                self.lexer.zshlex();
3828                s
3829            }
3830            _ => return Some(ZshCond::Binary(s1, op, String::new())),
3831        };
3832
3833        if op == "=~" {
3834            Some(ZshCond::Regex(s1, s2))
3835        } else {
3836            Some(ZshCond::Binary(s1, op, s2))
3837        }
3838    }
3839
3840    fn skip_cond_separators(&mut self) {
3841        while self.lexer.tok == LexTok::Seper && {
3842            let s = self.lexer.tokstr.as_ref();
3843            s.map(|s| !s.contains(';')).unwrap_or(true)
3844        } {
3845            self.lexer.zshlex();
3846        }
3847    }
3848
3849    /// Parse (( ... )) arithmetic command
3850    fn parse_arith(&mut self) -> Option<ZshCommand> {
3851        let expr = self.lexer.tokstr.clone().unwrap_or_default();
3852        self.lexer.zshlex();
3853        Some(ZshCommand::Arith(expr))
3854    }
3855
3856    /// Parse time command
3857    fn parse_time(&mut self) -> Option<ZshCommand> {
3858        self.lexer.zshlex(); // skip 'time'
3859
3860        // Check if there's a pipeline to time
3861        if self.lexer.tok == LexTok::Seper
3862            || self.lexer.tok == LexTok::Newlin
3863            || self.lexer.tok == LexTok::Endinput
3864        {
3865            Some(ZshCommand::Time(None))
3866        } else {
3867            let sublist = self.parse_sublist();
3868            Some(ZshCommand::Time(sublist.map(Box::new)))
3869        }
3870    }
3871
3872    /// Check if next token is ()
3873    fn peek_inoutpar(&mut self) -> bool {
3874        self.lexer.tok == LexTok::Inoutpar
3875    }
3876
3877    /// Skip separator tokens
3878    fn skip_separators(&mut self) {
3879        let mut iterations = 0;
3880        while self.lexer.tok == LexTok::Seper || self.lexer.tok == LexTok::Newlin {
3881            iterations += 1;
3882            if iterations > 100_000 {
3883                self.error("skip_separators: too many iterations");
3884                return;
3885            }
3886            self.lexer.zshlex();
3887        }
3888    }
3889
3890    /// Record an error
3891    fn error(&mut self, msg: &str) {
3892        self.errors.push(ParseError {
3893            message: msg.to_string(),
3894            line: self.lexer.lineno,
3895        });
3896    }
3897}
3898
3899#[cfg(test)]
3900mod tests {
3901    use super::*;
3902
3903    fn parse(input: &str) -> Result<ZshProgram, Vec<ParseError>> {
3904        let mut parser = ZshParser::new(input);
3905        parser.parse()
3906    }
3907
3908    #[test]
3909    fn test_simple_command() {
3910        let prog = parse("echo hello world").unwrap();
3911        assert_eq!(prog.lists.len(), 1);
3912        match &prog.lists[0].sublist.pipe.cmd {
3913            ZshCommand::Simple(s) => {
3914                assert_eq!(s.words, vec!["echo", "hello", "world"]);
3915            }
3916            _ => panic!("expected simple command"),
3917        }
3918    }
3919
3920    #[test]
3921    fn test_pipeline() {
3922        let prog = parse("ls | grep foo | wc -l").unwrap();
3923        assert_eq!(prog.lists.len(), 1);
3924
3925        let pipe = &prog.lists[0].sublist.pipe;
3926        assert!(pipe.next.is_some());
3927
3928        let pipe2 = pipe.next.as_ref().unwrap();
3929        assert!(pipe2.next.is_some());
3930    }
3931
3932    #[test]
3933    fn test_and_or() {
3934        let prog = parse("cmd1 && cmd2 || cmd3").unwrap();
3935        let sublist = &prog.lists[0].sublist;
3936
3937        assert!(sublist.next.is_some());
3938        let (op, _) = sublist.next.as_ref().unwrap();
3939        assert_eq!(*op, SublistOp::And);
3940    }
3941
3942    #[test]
3943    fn test_if_then() {
3944        let prog = parse("if test -f foo; then echo yes; fi").unwrap();
3945        match &prog.lists[0].sublist.pipe.cmd {
3946            ZshCommand::If(_) => {}
3947            _ => panic!("expected if command"),
3948        }
3949    }
3950
3951    #[test]
3952    fn test_for_loop() {
3953        let prog = parse("for i in a b c; do echo $i; done").unwrap();
3954        match &prog.lists[0].sublist.pipe.cmd {
3955            ZshCommand::For(f) => {
3956                assert_eq!(f.var, "i");
3957                match &f.list {
3958                    ForList::Words(w) => assert_eq!(w, &vec!["a", "b", "c"]),
3959                    _ => panic!("expected word list"),
3960                }
3961            }
3962            _ => panic!("expected for command"),
3963        }
3964    }
3965
3966    #[test]
3967    fn test_case() {
3968        let prog = parse("case $x in a) echo a;; b) echo b;; esac").unwrap();
3969        match &prog.lists[0].sublist.pipe.cmd {
3970            ZshCommand::Case(c) => {
3971                assert_eq!(c.arms.len(), 2);
3972            }
3973            _ => panic!("expected case command"),
3974        }
3975    }
3976
3977    #[test]
3978    fn test_function() {
3979        // First test just parsing "function foo" to see what happens
3980        let prog = parse("function foo { }").unwrap();
3981        match &prog.lists[0].sublist.pipe.cmd {
3982            ZshCommand::FuncDef(f) => {
3983                assert_eq!(f.names, vec!["foo"]);
3984            }
3985            _ => panic!(
3986                "expected function, got {:?}",
3987                prog.lists[0].sublist.pipe.cmd
3988            ),
3989        }
3990    }
3991
3992    #[test]
3993    fn test_redirection() {
3994        let prog = parse("echo hello > file.txt").unwrap();
3995        match &prog.lists[0].sublist.pipe.cmd {
3996            ZshCommand::Simple(s) => {
3997                assert_eq!(s.redirs.len(), 1);
3998                assert_eq!(s.redirs[0].rtype, RedirType::Write);
3999            }
4000            _ => panic!("expected simple command"),
4001        }
4002    }
4003
4004    #[test]
4005    fn test_assignment() {
4006        let prog = parse("FOO=bar echo $FOO").unwrap();
4007        match &prog.lists[0].sublist.pipe.cmd {
4008            ZshCommand::Simple(s) => {
4009                assert_eq!(s.assigns.len(), 1);
4010                assert_eq!(s.assigns[0].name, "FOO");
4011            }
4012            _ => panic!("expected simple command"),
4013        }
4014    }
4015
4016    #[test]
4017    fn test_parse_completion_function() {
4018        let input = r#"_2to3_fixes() {
4019  local -a fixes
4020  fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
4021  (( ${#fixes} )) && _describe -t fixes 'fix' fixes
4022}"#;
4023        let result = parse(input);
4024        assert!(
4025            result.is_ok(),
4026            "Failed to parse completion function: {:?}",
4027            result.err()
4028        );
4029        let prog = result.unwrap();
4030        assert!(
4031            !prog.lists.is_empty(),
4032            "Expected at least one list in program"
4033        );
4034    }
4035
4036    #[test]
4037    fn test_parse_array_with_complex_elements() {
4038        let input = r#"arguments=(
4039  '(- * :)'{-h,--help}'[show this help message and exit]'
4040  {-d,--doctests_only}'[fix up doctests only]'
4041  '*:filename:_files'
4042)"#;
4043        let result = parse(input);
4044        assert!(
4045            result.is_ok(),
4046            "Failed to parse array assignment: {:?}",
4047            result.err()
4048        );
4049    }
4050
4051    #[test]
4052    fn test_parse_full_completion_file() {
4053        let input = r##"#compdef 2to3
4054
4055# zsh completions for '2to3'
4056
4057_2to3_fixes() {
4058  local -a fixes
4059  fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
4060  (( ${#fixes} )) && _describe -t fixes 'fix' fixes
4061}
4062
4063local -a arguments
4064
4065arguments=(
4066  '(- * :)'{-h,--help}'[show this help message and exit]'
4067  {-d,--doctests_only}'[fix up doctests only]'
4068  {-f,--fix}'[each FIX specifies a transformation; default: all]:fix name:_2to3_fixes'
4069  {-j,--processes}'[run 2to3 concurrently]:number: '
4070  {-x,--nofix}'[prevent a transformation from being run]:fix name:_2to3_fixes'
4071  {-l,--list-fixes}'[list available transformations]'
4072  {-p,--print-function}'[modify the grammar so that print() is a function]'
4073  {-v,--verbose}'[more verbose logging]'
4074  '--no-diffs[do not show diffs of the refactoring]'
4075  {-w,--write}'[write back modified files]'
4076  {-n,--nobackups}'[do not write backups for modified files]'
4077  {-o,--output-dir}'[put output files in this directory instead of overwriting]:directory:_directories'
4078  {-W,--write-unchanged-files}'[also write files even if no changes were required]'
4079  '--add-suffix[append this string to all output filenames]:suffix: '
4080  '*:filename:_files'
4081)
4082
4083_arguments -s -S $arguments
4084"##;
4085        let result = parse(input);
4086        assert!(
4087            result.is_ok(),
4088            "Failed to parse full completion file: {:?}",
4089            result.err()
4090        );
4091        let prog = result.unwrap();
4092        // Should have parsed successfully with at least one statement
4093        assert!(!prog.lists.is_empty(), "Expected at least one list");
4094    }
4095
4096    #[test]
4097    fn test_parse_logs_sh() {
4098        let input = r#"#!/usr/bin/env bash
4099shopt -s globstar
4100
4101if [[ $(uname) == Darwin ]]; then
4102    tail -f /var/log/**/*.log /var/log/**/*.out | lolcat
4103else
4104    if [[ $ZPWR_DISTRO_NAME == raspbian ]]; then
4105        tail -f /var/log/**/*.log | lolcat
4106    else
4107        printf "Unsupported...\n" >&2
4108    fi
4109fi
4110"#;
4111        let result = parse(input);
4112        assert!(
4113            result.is_ok(),
4114            "Failed to parse logs.sh: {:?}",
4115            result.err()
4116        );
4117    }
4118
4119    #[test]
4120    fn test_parse_case_with_glob() {
4121        let input = r#"case "$ZPWR_OS_TYPE" in
4122    darwin*)  open_cmd='open'
4123      ;;
4124    cygwin*)  open_cmd='cygstart'
4125      ;;
4126    linux*)
4127        open_cmd='xdg-open'
4128      ;;
4129esac"#;
4130        let result = parse(input);
4131        assert!(
4132            result.is_ok(),
4133            "Failed to parse case with glob: {:?}",
4134            result.err()
4135        );
4136    }
4137
4138    #[test]
4139    fn test_parse_case_with_nested_if() {
4140        // Test case with nested if and glob patterns
4141        let input = r##"function zpwrGetOpenCommand(){
4142    local open_cmd
4143    case "$ZPWR_OS_TYPE" in
4144        darwin*)  open_cmd='open' ;;
4145        cygwin*)  open_cmd='cygstart' ;;
4146        linux*)
4147            if [[ "$_zpwr_uname_r" != *icrosoft* ]];then
4148                open_cmd='nohup xdg-open'
4149            fi
4150            ;;
4151    esac
4152}"##;
4153        let result = parse(input);
4154        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
4155    }
4156
4157    #[test]
4158    fn test_parse_zpwr_scripts() {
4159        use std::fs;
4160        use std::path::Path;
4161        use std::sync::mpsc;
4162        use std::thread;
4163        use std::time::{Duration, Instant};
4164
4165        let scripts_dir = Path::new("/Users/wizard/.zpwr/scripts");
4166        if !scripts_dir.exists() {
4167            eprintln!("Skipping test: scripts directory not found");
4168            return;
4169        }
4170
4171        let mut total = 0;
4172        let mut passed = 0;
4173        let mut failed_files = Vec::new();
4174        let mut timeout_files = Vec::new();
4175
4176        for ext in &["sh", "zsh"] {
4177            let pattern = scripts_dir.join(format!("*.{}", ext));
4178            if let Ok(entries) = glob::glob(pattern.to_str().unwrap()) {
4179                for entry in entries.flatten() {
4180                    total += 1;
4181                    let file_path = entry.display().to_string();
4182                    let content = match fs::read_to_string(&entry) {
4183                        Ok(c) => c,
4184                        Err(e) => {
4185                            failed_files.push((file_path, format!("read error: {}", e)));
4186                            continue;
4187                        }
4188                    };
4189
4190                    // Parse with timeout
4191                    let content_clone = content.clone();
4192                    let (tx, rx) = mpsc::channel();
4193                    let handle = thread::spawn(move || {
4194                        let result = parse(&content_clone);
4195                        let _ = tx.send(result);
4196                    });
4197
4198                    match rx.recv_timeout(Duration::from_secs(2)) {
4199                        Ok(Ok(_)) => passed += 1,
4200                        Ok(Err(errors)) => {
4201                            let first_err = errors
4202                                .first()
4203                                .map(|e| format!("line {}: {}", e.line, e.message))
4204                                .unwrap_or_default();
4205                            failed_files.push((file_path, first_err));
4206                        }
4207                        Err(_) => {
4208                            timeout_files.push(file_path);
4209                            // Thread will be abandoned
4210                        }
4211                    }
4212                }
4213            }
4214        }
4215
4216        eprintln!("\n=== ZPWR Scripts Parse Results ===");
4217        eprintln!("Passed: {}/{}", passed, total);
4218
4219        if !timeout_files.is_empty() {
4220            eprintln!("\nTimeout files (>2s):");
4221            for file in &timeout_files {
4222                eprintln!("  {}", file);
4223            }
4224        }
4225
4226        if !failed_files.is_empty() {
4227            eprintln!("\nFailed files:");
4228            for (file, err) in &failed_files {
4229                eprintln!("  {} - {}", file, err);
4230            }
4231        }
4232
4233        // Allow some failures initially, but track progress
4234        let pass_rate = if total > 0 {
4235            (passed as f64 / total as f64) * 100.0
4236        } else {
4237            0.0
4238        };
4239        eprintln!("Pass rate: {:.1}%", pass_rate);
4240
4241        // Require at least 50% pass rate for now
4242        assert!(pass_rate >= 50.0, "Pass rate too low: {:.1}%", pass_rate);
4243    }
4244
4245    #[test]
4246    #[ignore] // Uses threads that can't be killed on timeout; use integration test instead
4247    fn test_parse_zsh_stdlib_functions() {
4248        use std::fs;
4249        use std::path::Path;
4250        use std::sync::mpsc;
4251        use std::thread;
4252        use std::time::Duration;
4253
4254        let functions_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/zsh_functions");
4255        if !functions_dir.exists() {
4256            eprintln!(
4257                "Skipping test: zsh_functions directory not found at {:?}",
4258                functions_dir
4259            );
4260            return;
4261        }
4262
4263        let mut total = 0;
4264        let mut passed = 0;
4265        let mut failed_files = Vec::new();
4266        let mut timeout_files = Vec::new();
4267
4268        if let Ok(entries) = fs::read_dir(&functions_dir) {
4269            for entry in entries.flatten() {
4270                let path = entry.path();
4271                if !path.is_file() {
4272                    continue;
4273                }
4274
4275                total += 1;
4276                let file_path = path.display().to_string();
4277                let content = match fs::read_to_string(&path) {
4278                    Ok(c) => c,
4279                    Err(e) => {
4280                        failed_files.push((file_path, format!("read error: {}", e)));
4281                        continue;
4282                    }
4283                };
4284
4285                // Parse with timeout
4286                let content_clone = content.clone();
4287                let (tx, rx) = mpsc::channel();
4288                thread::spawn(move || {
4289                    let result = parse(&content_clone);
4290                    let _ = tx.send(result);
4291                });
4292
4293                match rx.recv_timeout(Duration::from_secs(2)) {
4294                    Ok(Ok(_)) => passed += 1,
4295                    Ok(Err(errors)) => {
4296                        let first_err = errors
4297                            .first()
4298                            .map(|e| format!("line {}: {}", e.line, e.message))
4299                            .unwrap_or_default();
4300                        failed_files.push((file_path, first_err));
4301                    }
4302                    Err(_) => {
4303                        timeout_files.push(file_path);
4304                    }
4305                }
4306            }
4307        }
4308
4309        eprintln!("\n=== Zsh Stdlib Functions Parse Results ===");
4310        eprintln!("Passed: {}/{}", passed, total);
4311
4312        if !timeout_files.is_empty() {
4313            eprintln!("\nTimeout files (>2s): {}", timeout_files.len());
4314            for file in timeout_files.iter().take(10) {
4315                eprintln!("  {}", file);
4316            }
4317            if timeout_files.len() > 10 {
4318                eprintln!("  ... and {} more", timeout_files.len() - 10);
4319            }
4320        }
4321
4322        if !failed_files.is_empty() {
4323            eprintln!("\nFailed files: {}", failed_files.len());
4324            for (file, err) in failed_files.iter().take(20) {
4325                let filename = Path::new(file)
4326                    .file_name()
4327                    .unwrap_or_default()
4328                    .to_string_lossy();
4329                eprintln!("  {} - {}", filename, err);
4330            }
4331            if failed_files.len() > 20 {
4332                eprintln!("  ... and {} more", failed_files.len() - 20);
4333            }
4334        }
4335
4336        let pass_rate = if total > 0 {
4337            (passed as f64 / total as f64) * 100.0
4338        } else {
4339            0.0
4340        };
4341        eprintln!("Pass rate: {:.1}%", pass_rate);
4342
4343        // Require at least 50% pass rate
4344        assert!(pass_rate >= 50.0, "Pass rate too low: {:.1}%", pass_rate);
4345    }
4346}