Skip to main content

shuck_parser/parser/
commands.rs

1use super::*;
2use smallvec::SmallVec;
3
4#[derive(Debug, Clone, Copy)]
5enum ForHeaderSurface {
6    In {
7        in_span: Option<Span>,
8    },
9    Paren {
10        left_paren_span: Span,
11        right_paren_span: Span,
12    },
13}
14
15#[derive(Debug, Clone, Copy)]
16struct ZshCaseScanState {
17    position: Position,
18    paren_depth: usize,
19    bracket_depth: usize,
20    brace_depth: usize,
21    in_single: bool,
22    in_double: bool,
23    in_backtick: bool,
24    escaped: bool,
25}
26
27impl ZshCaseScanState {
28    fn new(position: Position) -> Self {
29        Self {
30            position,
31            paren_depth: 0,
32            bracket_depth: 0,
33            brace_depth: 0,
34            in_single: false,
35            in_double: false,
36            in_backtick: false,
37            escaped: false,
38        }
39    }
40}
41
42impl<'a> Parser<'a> {
43    fn apply_word_command_effects(&mut self, name: &Word, args: &[Word]) {
44        let Some(name) = self.literal_word_text(name) else {
45            return;
46        };
47
48        match name.as_str() {
49            "shopt" => {
50                let mut toggle = None;
51                for arg in args {
52                    let Some(arg) = self.literal_word_text(arg) else {
53                        continue;
54                    };
55                    match arg.as_str() {
56                        "-s" => toggle = Some(true),
57                        "-u" => toggle = Some(false),
58                        "expand_aliases" => {
59                            if let Some(toggle) = toggle {
60                                self.expand_aliases = toggle;
61                            }
62                        }
63                        _ => {}
64                    }
65                }
66            }
67            "alias" => {
68                for arg in args {
69                    let Some(arg) = self.literal_word_text(arg) else {
70                        continue;
71                    };
72                    if arg == "--" {
73                        continue;
74                    }
75                    let Some((alias_name, value)) = arg.split_once('=') else {
76                        continue;
77                    };
78                    self.aliases
79                        .insert(alias_name.to_string(), self.compile_alias_definition(value));
80                }
81            }
82            "unalias" => {
83                for arg in args {
84                    let Some(arg) = self.literal_word_text(arg) else {
85                        continue;
86                    };
87                    match arg.as_str() {
88                        "--" => {}
89                        "-a" => self.aliases.clear(),
90                        _ => {
91                            self.aliases.remove(arg.as_str());
92                        }
93                    }
94                }
95            }
96            _ => {}
97        }
98    }
99
100    fn apply_stmt_effects(&mut self, stmt: &Stmt) {
101        match &stmt.command {
102            AstCommand::Simple(simple) => {
103                self.apply_word_command_effects(&simple.name, &simple.args)
104            }
105            AstCommand::Binary(binary) if matches!(binary.op, BinaryOp::And | BinaryOp::Or) => {
106                self.apply_stmt_effects(&binary.left);
107                self.apply_stmt_effects(&binary.right);
108            }
109            _ => {}
110        }
111    }
112
113    fn apply_stmt_list_effects(&mut self, stmts: &[Stmt]) {
114        for stmt in stmts {
115            self.apply_stmt_effects(stmt);
116        }
117    }
118
119    fn parse_command_list_required(&mut self) -> Result<Vec<Stmt>> {
120        self.parse_command_list()?
121            .ok_or_else(|| self.error("expected command"))
122    }
123
124    fn skip_command_separators(&mut self) -> Result<()> {
125        loop {
126            self.skip_newlines()?;
127            if self.at(TokenKind::Semicolon) {
128                self.advance();
129                continue;
130            }
131            break;
132        }
133        Ok(())
134    }
135
136    fn is_recovery_separator(kind: TokenKind) -> bool {
137        matches!(
138            kind,
139            TokenKind::Newline
140                | TokenKind::Semicolon
141                | TokenKind::Background
142                | TokenKind::BackgroundPipe
143                | TokenKind::BackgroundBang
144                | TokenKind::And
145                | TokenKind::Or
146                | TokenKind::Pipe
147                | TokenKind::DoubleSemicolon
148                | TokenKind::SemiAmp
149                | TokenKind::SemiPipe
150                | TokenKind::DoubleSemiAmp
151        )
152    }
153
154    fn recover_to_command_boundary(&mut self, failed_offset: usize) -> bool {
155        let mut advanced = false;
156
157        while let Some(kind) = self.current_token_kind {
158            if Self::is_recovery_separator(kind) {
159                while let Some(kind) = self.current_token_kind {
160                    if !Self::is_recovery_separator(kind) {
161                        break;
162                    }
163                    self.advance();
164                    advanced = true;
165                }
166                break;
167            }
168
169            let before_offset = self.current_span.start.offset;
170            self.advance();
171            advanced = true;
172
173            if self.current_token.is_none() {
174                break;
175            }
176
177            if self.current_span.start.offset > failed_offset
178                && before_offset != self.current_span.start.offset
179            {
180                continue;
181            }
182        }
183
184        advanced
185    }
186
187    fn parse_impl(&mut self) -> ParseResult {
188        let file_span =
189            Span::from_positions(Position::new(), Position::new().advanced_by(self.input));
190        let mut stmts = Vec::new();
191        let mut diagnostics = Vec::new();
192        let mut terminal_error = None;
193
194        while self.current_token.is_some() {
195            let checkpoint = self.current_span.start.offset;
196
197            if let Err(error) = self.tick() {
198                diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
199                terminal_error.get_or_insert(error);
200                break;
201            }
202            if let Err(error) = self.skip_newlines() {
203                diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
204                terminal_error.get_or_insert(error);
205                break;
206            }
207            if let Err(error) = self.check_error_token() {
208                diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
209                let recovered = self.recover_to_command_boundary(checkpoint);
210                if recovered
211                    || (self.current_token.is_some()
212                        && self.current_span.start.offset < self.input.len())
213                {
214                    terminal_error.get_or_insert(error);
215                }
216                if !recovered && terminal_error.is_some() {
217                    break;
218                }
219                continue;
220            }
221            if self.current_token.is_none() {
222                break;
223            }
224
225            let command_start = self.current_span.start.offset;
226            match self.parse_command_list_required() {
227                Ok(command_stmts) => {
228                    self.apply_stmt_list_effects(&command_stmts);
229                    stmts.extend(command_stmts);
230                }
231                Err(error) => {
232                    diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
233                    let recovered = self.recover_to_command_boundary(command_start);
234                    if recovered
235                        || (self.current_token.is_some()
236                            && self.current_span.start.offset < self.input.len())
237                    {
238                        terminal_error.get_or_insert(error);
239                    }
240                    if !recovered && terminal_error.is_some() {
241                        break;
242                    }
243                }
244            }
245        }
246
247        let mut file = File {
248            body: Self::stmt_seq_with_span(file_span, stmts),
249            span: file_span,
250        };
251        self.attach_comments_to_file(&mut file);
252
253        let status = if terminal_error.is_some() {
254            ParseStatus::Fatal
255        } else if diagnostics.is_empty() {
256            ParseStatus::Clean
257        } else {
258            ParseStatus::Recovered
259        };
260
261        ParseResult {
262            file,
263            diagnostics,
264            status,
265            terminal_error,
266            syntax_facts: std::mem::take(&mut self.syntax_facts),
267        }
268    }
269
270    /// Parse the input and return the AST, recovery diagnostics, and syntax facts.
271    pub fn parse(mut self) -> ParseResult {
272        self.parse_impl()
273    }
274
275    #[cfg(feature = "benchmarking")]
276    #[doc(hidden)]
277    pub fn parse_with_benchmark_counters(self) -> (ParseResult, ParserBenchmarkCounters) {
278        let mut parser = self.rebuild_with_benchmark_counters();
279        let output = parser.parse_impl();
280        (output, parser.finish_benchmark_counters())
281    }
282
283    fn parse_command_list(&mut self) -> Result<Option<Vec<Stmt>>> {
284        self.tick()?;
285        let mut current = match self.parse_pipeline()? {
286            Some(stmt) => stmt,
287            None => return Ok(None),
288        };
289
290        let mut stmts = Vec::with_capacity(2);
291
292        loop {
293            let (op, terminator, allow_empty_tail) = match self.current_token_kind {
294                Some(TokenKind::And) => (Some(BinaryOp::And), None, false),
295                Some(TokenKind::Or) => (Some(BinaryOp::Or), None, false),
296                Some(TokenKind::Semicolon) => (None, Some(StmtTerminator::Semicolon), true),
297                Some(TokenKind::Background) => (
298                    None,
299                    Some(StmtTerminator::Background(BackgroundOperator::Plain)),
300                    true,
301                ),
302                Some(TokenKind::BackgroundPipe) => (
303                    None,
304                    Some(StmtTerminator::Background(BackgroundOperator::Pipe)),
305                    true,
306                ),
307                Some(TokenKind::BackgroundBang) => (
308                    None,
309                    Some(StmtTerminator::Background(BackgroundOperator::Bang)),
310                    true,
311                ),
312                _ => break,
313            };
314            let operator_span = self.current_span;
315            self.advance();
316
317            self.skip_newlines()?;
318            if allow_empty_tail && self.current_token.is_none() {
319                current.terminator = terminator;
320                current.terminator_span = Some(operator_span);
321                stmts.push(current);
322                return Ok(Some(stmts));
323            }
324
325            if let Some(binary_op) = op {
326                if let Some(right) = self.parse_pipeline()? {
327                    current = Self::binary_stmt(current, binary_op, operator_span, right);
328                } else {
329                    break;
330                }
331                continue;
332            }
333
334            let terminator = terminator.expect("list terminator should be present");
335            if let Some(next) = self.parse_pipeline()? {
336                current.terminator = Some(terminator);
337                current.terminator_span = Some(operator_span);
338                stmts.push(current);
339                current = next;
340            } else if allow_empty_tail {
341                if self
342                    .current_keyword()
343                    .is_some_and(Self::is_non_command_keyword)
344                {
345                    break;
346                }
347                if matches!(
348                    self.current_token_kind,
349                    Some(TokenKind::Semicolon | TokenKind::Newline)
350                ) {
351                    self.advance();
352                }
353                current.terminator = Some(terminator);
354                current.terminator_span = Some(operator_span);
355                stmts.push(current);
356                return Ok(Some(stmts));
357            } else {
358                break;
359            }
360        }
361
362        stmts.push(current);
363        Ok(Some(stmts))
364    }
365
366    /// Parse a pipeline (commands connected by |)
367    ///
368    /// Handles `!` pipeline negation: `! cmd | cmd2` negates the exit code.
369    fn parse_pipeline(&mut self) -> Result<Option<Stmt>> {
370        let start_span = self.current_span;
371
372        // Check for pipeline negation: `! command`
373        let negated = self.at(TokenKind::Word) && self.current_word_str() == Some("!");
374        if negated {
375            self.advance();
376        }
377
378        let mut stmt = match self.parse_command()? {
379            Some(cmd) => Self::lower_non_sequence_command_to_stmt(cmd),
380            None => {
381                if negated {
382                    return Err(self.error("expected command after !"));
383                }
384                return Ok(None);
385            }
386        };
387
388        let mut saw_pipe = false;
389        while self.at_in_set(PIPE_OPERATOR_TOKENS) {
390            saw_pipe = true;
391            let op = if self.at(TokenKind::PipeBoth) {
392                BinaryOp::PipeAll
393            } else {
394                BinaryOp::Pipe
395            };
396            let operator_span = self.current_span;
397            self.advance();
398            self.skip_newlines()?;
399
400            if let Some(cmd) = self.parse_command()? {
401                let right = Self::lower_non_sequence_command_to_stmt(cmd);
402                stmt = Self::binary_stmt(stmt, op, operator_span, right);
403            } else {
404                return Err(self.error("expected command after |"));
405            }
406        }
407
408        if negated || saw_pipe {
409            stmt.negated = negated;
410            stmt.span = start_span.merge(self.current_span);
411        }
412        Ok(Some(stmt))
413    }
414
415    fn parse_compound_with_redirects(
416        &mut self,
417        parser: impl FnOnce(&mut Self) -> Result<CompoundCommand>,
418    ) -> Result<Option<Command>> {
419        let compound = parser(self)?;
420        let redirects = self.parse_trailing_redirects();
421        Ok(Some(Command::Compound(Box::new(compound), redirects)))
422    }
423
424    fn current_starts_prefix_redirect_compound(&self) -> bool {
425        match self.current_keyword() {
426            Some(Keyword::If)
427            | Some(Keyword::While)
428            | Some(Keyword::Until)
429            | Some(Keyword::Case)
430            | Some(Keyword::Select)
431            | Some(Keyword::Time)
432            | Some(Keyword::Coproc) => true,
433            Some(Keyword::For) => self.dialect == ShellDialect::Zsh,
434            Some(Keyword::Repeat) => self.zsh_short_repeat_enabled(),
435            Some(Keyword::Foreach) => self.zsh_short_loops_enabled(),
436            Some(Keyword::Function) => false,
437            None => matches!(
438                self.current_token_kind,
439                Some(
440                    TokenKind::DoubleLeftBracket
441                        | TokenKind::DoubleLeftParen
442                        | TokenKind::LeftParen
443                        | TokenKind::LeftBrace
444                )
445            ),
446            _ => false,
447        }
448    }
449
450    fn parse_prefix_redirected_compound_command(&mut self) -> Result<Option<Command>> {
451        if !self.current_token_kind.is_some_and(Self::is_redirect_kind) {
452            return Ok(None);
453        }
454
455        let checkpoint = self.checkpoint();
456        let mut redirects = self.parse_trailing_redirects();
457        if redirects.is_empty() || !self.current_starts_prefix_redirect_compound() {
458            self.restore(checkpoint);
459            return Ok(None);
460        }
461
462        let Some(mut command) = self.parse_command()? else {
463            self.restore(checkpoint);
464            return Ok(None);
465        };
466
467        match &mut command {
468            Command::Compound(_, trailing) => {
469                redirects.append(trailing);
470                *trailing = redirects;
471                Ok(Some(command))
472            }
473            _ => {
474                self.restore(checkpoint);
475                Ok(None)
476            }
477        }
478    }
479
480    fn classify_flow_control_name(&self, word: &Word) -> Option<FlowControlBuiltinKind> {
481        let name = self.single_literal_word_text(word)?;
482        match name {
483            "break" => Some(FlowControlBuiltinKind::Break),
484            "continue" => Some(FlowControlBuiltinKind::Continue),
485            "return" => Some(FlowControlBuiltinKind::Return),
486            "exit" => Some(FlowControlBuiltinKind::Exit),
487            _ => None,
488        }
489    }
490
491    fn classify_decl_variant_name(&self, word: &Word) -> Option<Name> {
492        let name = self.single_literal_word_text(word)?;
493        match name {
494            "declare" | "local" | "export" | "readonly" | "typeset" => Some(Name::from(name)),
495            _ => None,
496        }
497    }
498
499    fn classify_simple_command(&mut self, command: SimpleCommand) -> Command {
500        let kind = self.classify_flow_control_name(&command.name);
501
502        if let Some(kind) = kind {
503            let SimpleCommand {
504                args,
505                redirects,
506                assignments,
507                span,
508                ..
509            } = command;
510            let mut args = args.into_iter();
511
512            return match kind {
513                FlowControlBuiltinKind::Break => {
514                    Command::Builtin(BuiltinCommand::Break(BreakCommand {
515                        depth: args.next(),
516                        extra_args: args.collect(),
517                        redirects,
518                        assignments,
519                        span,
520                    }))
521                }
522                FlowControlBuiltinKind::Continue => {
523                    Command::Builtin(BuiltinCommand::Continue(ContinueCommand {
524                        depth: args.next(),
525                        extra_args: args.collect(),
526                        redirects,
527                        assignments,
528                        span,
529                    }))
530                }
531                FlowControlBuiltinKind::Return => {
532                    Command::Builtin(BuiltinCommand::Return(ReturnCommand {
533                        code: args.next(),
534                        extra_args: args.collect(),
535                        redirects,
536                        assignments,
537                        span,
538                    }))
539                }
540                FlowControlBuiltinKind::Exit => {
541                    Command::Builtin(BuiltinCommand::Exit(ExitCommand {
542                        code: args.next(),
543                        extra_args: args.collect(),
544                        redirects,
545                        assignments,
546                        span,
547                    }))
548                }
549            };
550        }
551
552        if let Some(variant) = self.classify_decl_variant_name(&command.name) {
553            let SimpleCommand {
554                name,
555                args,
556                redirects,
557                assignments,
558                span,
559            } = command;
560            return Command::Decl(Box::new(DeclClause {
561                variant,
562                variant_span: name.span,
563                operands: self.classify_decl_operands(args),
564                redirects,
565                assignments,
566                span,
567            }));
568        }
569
570        Command::Simple(command)
571    }
572
573    fn is_operand_like_double_paren_token(token: &LexedToken<'_>) -> bool {
574        match token.kind {
575            TokenKind::LiteralWord | TokenKind::QuotedWord => true,
576            TokenKind::Word => token.word_string().is_some_and(|text| {
577                !text.chars().all(|ch| ch.is_ascii_punctuation())
578                    && !Self::word_contains_obvious_arithmetic_punctuation(&text)
579            }),
580            _ => false,
581        }
582    }
583
584    fn word_contains_obvious_arithmetic_punctuation(text: &str) -> bool {
585        text.chars().any(|ch| {
586            matches!(
587                ch,
588                ',' | '='
589                    | '+'
590                    | '*'
591                    | '/'
592                    | '%'
593                    | '<'
594                    | '>'
595                    | '&'
596                    | '|'
597                    | '^'
598                    | '!'
599                    | '?'
600                    | ':'
601                    | '['
602                    | ']'
603            )
604        })
605    }
606
607    fn suspicious_double_paren_is_command_style(
608        &mut self,
609        checkpoint: &ParserCheckpoint<'a>,
610    ) -> bool {
611        self.restore(checkpoint.clone());
612        let parses_as_arithmetic = self.parse_arithmetic_command().is_ok();
613        self.restore(checkpoint.clone());
614        !parses_as_arithmetic
615    }
616
617    fn looks_like_command_style_double_paren(&mut self) -> bool {
618        if self.current_token_kind != Some(TokenKind::DoubleLeftParen) {
619            return false;
620        }
621
622        let checkpoint = self.checkpoint();
623        self.advance();
624        let mut paren_depth = 0_i32;
625        let mut previous_top_level_operand = false;
626
627        loop {
628            match self.current_token_kind {
629                Some(TokenKind::DoubleLeftParen) => {
630                    paren_depth += 2;
631                    previous_top_level_operand = false;
632                    self.advance();
633                }
634                Some(TokenKind::LeftParen) => {
635                    paren_depth += 1;
636                    previous_top_level_operand = false;
637                    self.advance();
638                }
639                Some(TokenKind::DoubleRightParen) => {
640                    if paren_depth == 0 {
641                        self.restore(checkpoint);
642                        return false;
643                    }
644                    if paren_depth == 1 {
645                        self.restore(checkpoint);
646                        return false;
647                    }
648                    paren_depth -= 2;
649                    previous_top_level_operand = false;
650                    self.advance();
651                }
652                Some(TokenKind::RightParen) => {
653                    if paren_depth == 0 {
654                        return self.suspicious_double_paren_is_command_style(&checkpoint);
655                    }
656                    paren_depth -= 1;
657                    previous_top_level_operand = false;
658                    self.advance();
659                }
660                Some(TokenKind::Newline) | Some(TokenKind::Semicolon) if paren_depth == 0 => {
661                    previous_top_level_operand = false;
662                    self.advance();
663                }
664                Some(TokenKind::Comment) if self.dialect == ShellDialect::Zsh => {
665                    self.restore(checkpoint);
666                    return false;
667                }
668                Some(_)
669                    if paren_depth == 0
670                        && self
671                            .current_token
672                            .as_ref()
673                            .is_some_and(Self::is_operand_like_double_paren_token) =>
674                {
675                    if previous_top_level_operand {
676                        return self.suspicious_double_paren_is_command_style(&checkpoint);
677                    }
678                    previous_top_level_operand = true;
679                    self.advance();
680                }
681                Some(_) => {
682                    previous_top_level_operand = false;
683                    self.advance();
684                }
685                None => {
686                    self.restore(checkpoint);
687                    return false;
688                }
689            }
690        }
691    }
692
693    fn split_current_double_left_paren(&mut self) {
694        let (left_span, right_span) = Self::split_double_left_paren(self.current_span);
695        self.set_current_kind(TokenKind::LeftParen, left_span);
696        self.synthetic_tokens
697            .push_front(SyntheticToken::punctuation(
698                TokenKind::LeftParen,
699                right_span,
700            ));
701    }
702
703    fn split_current_double_right_paren(&mut self) {
704        let (left_span, right_span) = Self::split_double_right_paren(self.current_span);
705        self.set_current_kind(TokenKind::RightParen, left_span);
706        self.synthetic_tokens
707            .push_front(SyntheticToken::punctuation(
708                TokenKind::RightParen,
709                right_span,
710            ));
711    }
712
713    /// Parse a single command (simple or compound)
714    fn parse_command(&mut self) -> Result<Option<Command>> {
715        self.skip_newlines()?;
716        self.check_error_token()?;
717        self.maybe_expand_current_alias_chain();
718        self.check_error_token()?;
719
720        if !self.zsh_short_repeat_enabled() && self.looks_like_disabled_repeat_loop()? {
721            self.ensure_repeat_loop()?;
722        }
723        if !self.zsh_short_loops_enabled() && self.looks_like_disabled_foreach_loop()? {
724            self.ensure_foreach_loop()?;
725        }
726
727        if let Some(command) = self.parse_prefix_redirected_compound_command()? {
728            return Ok(Some(command));
729        }
730
731        if let Some(command) = self.try_parse_zsh_attached_parens_function()? {
732            return Ok(Some(command));
733        }
734
735        // Check for compound commands and function keyword
736        match self.current_keyword() {
737            Some(Keyword::If) => return self.parse_compound_with_redirects(|s| s.parse_if()),
738            Some(Keyword::For) => return self.parse_compound_with_redirects(|s| s.parse_for()),
739            Some(Keyword::Repeat) if self.zsh_short_repeat_enabled() => {
740                return self.parse_compound_with_redirects(|s| s.parse_repeat());
741            }
742            Some(Keyword::Foreach) if self.zsh_short_loops_enabled() => {
743                return self.parse_compound_with_redirects(|s| s.parse_foreach());
744            }
745            Some(Keyword::While) => {
746                return self.parse_compound_with_redirects(|s| s.parse_while());
747            }
748            Some(Keyword::Until) => {
749                return self.parse_compound_with_redirects(|s| s.parse_until());
750            }
751            Some(Keyword::Case) => return self.parse_compound_with_redirects(|s| s.parse_case()),
752            Some(Keyword::Select) => {
753                return self.parse_compound_with_redirects(|s| s.parse_select());
754            }
755            Some(Keyword::Time) => return self.parse_compound_with_redirects(|s| s.parse_time()),
756            Some(Keyword::Coproc) => {
757                return self.parse_compound_with_redirects(|s| s.parse_coproc());
758            }
759            Some(Keyword::Function) => return self.parse_function_keyword().map(Some),
760            _ => {}
761        }
762
763        if self.at(TokenKind::Word)
764            && let Some(word) = self.current_source_like_word_text()
765            && self.peek_next_is(TokenKind::LeftParen)
766        {
767            let checkpoint = self.checkpoint();
768            self.advance();
769            self.advance();
770            let is_right_paren = self.at(TokenKind::RightParen);
771            self.restore(checkpoint);
772            if is_right_paren {
773                // Check for POSIX-style function: name() { body }
774                // Exclude obvious assignment-like heads such as `a[(1+2)*3]=9`.
775                if !word.contains('=') && !word.contains('[') {
776                    return self.parse_function_posix().map(Some);
777                }
778            } else if word.contains('$') && !word.contains('=') {
779                return Err(self.error("unexpected '(' after command word"));
780            }
781        }
782
783        // Check for conditional expression [[ ... ]]
784        if self.at(TokenKind::DoubleLeftBracket) {
785            return self.parse_compound_with_redirects(|s| s.parse_conditional());
786        }
787
788        // Check for arithmetic command ((expression))
789        if self.at(TokenKind::DoubleLeftParen) {
790            if self.looks_like_command_style_double_paren() {
791                self.split_current_double_left_paren();
792                return self.parse_compound_with_redirects(|s| s.parse_subshell());
793            }
794
795            let checkpoint = self.checkpoint();
796            if let Ok(compound) = self.parse_arithmetic_command() {
797                let redirects = self.parse_trailing_redirects();
798                return Ok(Some(Command::Compound(Box::new(compound), redirects)));
799            }
800            self.restore(checkpoint);
801
802            self.split_current_double_left_paren();
803            return self.parse_compound_with_redirects(|s| s.parse_subshell());
804        }
805
806        if self.dialect == ShellDialect::Zsh && self.at(TokenKind::LeftParen) {
807            let checkpoint = self.checkpoint();
808            self.advance();
809            let is_right_paren = self.at(TokenKind::RightParen);
810            self.restore(checkpoint);
811            if is_right_paren {
812                return self.parse_anonymous_paren_function().map(Some);
813            }
814        }
815
816        // Check for subshell
817        if self.at(TokenKind::LeftParen) {
818            return self.parse_compound_with_redirects(|s| s.parse_subshell());
819        }
820
821        // Check for brace group
822        if self.at(TokenKind::LeftBrace) {
823            return self.parse_compound_with_redirects(|s| {
824                s.parse_brace_group(BraceBodyContext::Ordinary)
825            });
826        }
827
828        // Default to simple command
829        match self.parse_simple_command()? {
830            Some(cmd) => Ok(Some(self.classify_simple_command(cmd))),
831            None => Ok(None),
832        }
833    }
834
835    /// Parse an if statement
836    fn parse_if(&mut self) -> Result<CompoundCommand> {
837        let start_span = self.current_span;
838        self.push_depth()?;
839        self.advance(); // consume 'if'
840        self.skip_newlines()?;
841
842        // Parse condition
843        let condition_start = self.current_span.start;
844        let allow_brace_syntax = self.zsh_brace_if_enabled();
845        let condition = self.parse_if_condition_until_body_start(allow_brace_syntax)?;
846        let condition_span = Span::from_positions(condition_start, self.current_span.start);
847        let condition = Self::stmt_seq_with_span(condition_span, condition);
848
849        let (mut syntax, then_branch, brace_style) = if allow_brace_syntax
850            && self.at(TokenKind::LeftBrace)
851        {
852            let (then_branch, left_brace_span, right_brace_span) = self
853                .parse_brace_enclosed_stmt_seq(
854                    "syntax error: empty then clause",
855                    BraceBodyContext::IfClause,
856                )?;
857            self.record_zsh_brace_if_span(left_brace_span);
858            (
859                IfSyntax::Brace {
860                    left_brace_span,
861                    right_brace_span,
862                },
863                then_branch,
864                true,
865            )
866        } else if let Some((then_branch, left_brace_span, right_brace_span)) = allow_brace_syntax
867            .then(|| self.try_parse_compact_zsh_brace_body(BraceBodyContext::IfClause))
868            .transpose()?
869            .flatten()
870        {
871            self.record_zsh_brace_if_span(left_brace_span);
872            (
873                IfSyntax::Brace {
874                    left_brace_span,
875                    right_brace_span,
876                },
877                then_branch,
878                true,
879            )
880        } else {
881            let then_span = self.current_span;
882            self.expect_keyword(Keyword::Then)?;
883            self.skip_newlines()?;
884
885            let then_start = self.current_span.start;
886            let then_branch = self.parse_compound_list_until(IF_BODY_TERMINATORS)?;
887            let then_branch_span = Span::from_positions(then_start, self.current_span.start);
888
889            let then_branch = if then_branch.is_empty() {
890                if self.dialect == ShellDialect::Zsh && self.is_keyword(Keyword::Elif) {
891                    Self::stmt_seq_with_span(then_branch_span, Vec::new())
892                } else {
893                    self.pop_depth();
894                    return Err(self.error("syntax error: empty then clause"));
895                }
896            } else {
897                Self::stmt_seq_with_span(then_branch_span, then_branch)
898            };
899
900            (
901                IfSyntax::ThenFi {
902                    then_span,
903                    fi_span: Span::new(),
904                },
905                then_branch,
906                false,
907            )
908        };
909
910        // Parse elif branches
911        let mut elif_branches = Vec::new();
912        while self.is_keyword(Keyword::Elif) {
913            self.advance(); // consume 'elif'
914            self.skip_newlines()?;
915
916            let elif_condition_start = self.current_span.start;
917            let elif_condition = self.parse_if_condition_until_body_start(brace_style)?;
918            let elif_condition_span =
919                Span::from_positions(elif_condition_start, self.current_span.start);
920            let elif_condition = Self::stmt_seq_with_span(elif_condition_span, elif_condition);
921
922            let elif_body = if brace_style {
923                if self.at(TokenKind::LeftBrace) {
924                    self.parse_brace_enclosed_stmt_seq(
925                        "syntax error: empty elif clause",
926                        BraceBodyContext::IfClause,
927                    )?
928                    .0
929                } else if let Some((body, _, _)) =
930                    self.try_parse_compact_zsh_brace_body(BraceBodyContext::IfClause)?
931                {
932                    body
933                } else {
934                    self.pop_depth();
935                    return Err(self.error("expected '{' to start elif clause"));
936                }
937            } else {
938                self.expect_keyword(Keyword::Then)?;
939                let elif_body_region_start = self.current_span.start;
940                self.skip_newlines()?;
941
942                let elif_body_start = self.current_span.start;
943                let elif_body = self.parse_compound_list_until(IF_BODY_TERMINATORS)?;
944                let elif_body_span = Span::from_positions(elif_body_start, self.current_span.start);
945
946                if elif_body.is_empty() {
947                    if self.dialect == ShellDialect::Zsh
948                        && self.has_recorded_comment_between(
949                            elif_body_region_start.offset,
950                            self.current_span.start.offset,
951                        )
952                    {
953                        Self::stmt_seq_with_span(
954                            Span::from_positions(elif_body_region_start, self.current_span.start),
955                            Vec::new(),
956                        )
957                    } else {
958                        self.pop_depth();
959                        return Err(self.error("syntax error: empty elif clause"));
960                    }
961                } else {
962                    Self::stmt_seq_with_span(elif_body_span, elif_body)
963                }
964            };
965
966            elif_branches.push((elif_condition, elif_body));
967        }
968
969        // Parse else branch
970        let else_branch = if self.is_keyword(Keyword::Else) {
971            self.advance(); // consume 'else'
972            let else_region_start = self.current_span.start;
973            self.skip_newlines()?;
974            if brace_style {
975                if self.at(TokenKind::LeftBrace) {
976                    Some(
977                        self.parse_brace_enclosed_stmt_seq(
978                            "syntax error: empty else clause",
979                            BraceBodyContext::IfClause,
980                        )?
981                        .0,
982                    )
983                } else if let Some((body, _, _)) =
984                    self.try_parse_compact_zsh_brace_body(BraceBodyContext::IfClause)?
985                {
986                    Some(body)
987                } else {
988                    self.pop_depth();
989                    return Err(self.error("expected '{' to start else clause"));
990                }
991            } else {
992                let else_start = self.current_span.start;
993                let branch = self.parse_compound_list(Keyword::Fi)?;
994                let else_span = Span::from_positions(else_start, self.current_span.start);
995
996                if branch.is_empty() {
997                    if self.dialect == ShellDialect::Zsh
998                        && self.has_recorded_comment_between(
999                            else_region_start.offset,
1000                            self.current_span.start.offset,
1001                        )
1002                    {
1003                        Some(Self::stmt_seq_with_span(
1004                            Span::from_positions(else_region_start, self.current_span.start),
1005                            Vec::new(),
1006                        ))
1007                    } else {
1008                        self.pop_depth();
1009                        return Err(self.error("syntax error: empty else clause"));
1010                    }
1011                } else {
1012                    Some(Self::stmt_seq_with_span(else_span, branch))
1013                }
1014            }
1015        } else {
1016            None
1017        };
1018
1019        if !brace_style {
1020            self.expect_keyword(Keyword::Fi)?;
1021            if let IfSyntax::ThenFi { then_span, .. } = syntax {
1022                syntax = IfSyntax::ThenFi {
1023                    then_span,
1024                    fi_span: self.current_span,
1025                };
1026            }
1027        }
1028
1029        self.pop_depth();
1030        Ok(CompoundCommand::If(IfCommand {
1031            condition,
1032            then_branch,
1033            elif_branches,
1034            else_branch,
1035            syntax,
1036            span: start_span.merge(self.current_span),
1037        }))
1038    }
1039
1040    /// Parse a for loop
1041    fn parse_for(&mut self) -> Result<CompoundCommand> {
1042        let start_span = self.current_span;
1043        self.push_depth()?;
1044        self.advance(); // consume 'for'
1045        self.skip_newlines()?;
1046
1047        // Check for C-style for loop: for ((init; cond; step))
1048        if self.at(TokenKind::DoubleLeftParen) {
1049            let result = self.parse_arithmetic_for_inner(start_span);
1050            self.pop_depth();
1051            return result;
1052        }
1053
1054        let allow_zsh_targets = self.dialect == ShellDialect::Zsh;
1055        let targets = match self.parse_for_targets(allow_zsh_targets) {
1056            Ok(targets) => targets,
1057            Err(error) => {
1058                self.pop_depth();
1059                return Err(error);
1060            }
1061        };
1062
1063        if allow_zsh_targets {
1064            self.skip_newlines()?;
1065        }
1066
1067        let (words, header) = if allow_zsh_targets && self.at(TokenKind::LeftParen) {
1068            let left_paren_span = self.current_span;
1069            self.advance();
1070
1071            let mut words = SmallVec::<[Word; 2]>::new();
1072            while !self.at(TokenKind::RightParen) {
1073                if self.at(TokenKind::Newline) {
1074                    self.skip_newlines()?;
1075                    continue;
1076                }
1077                match self.current_token_kind {
1078                    Some(kind)
1079                        if kind.is_word_like()
1080                            || (self.dialect == ShellDialect::Zsh
1081                                && matches!(kind, TokenKind::LeftParen)) =>
1082                    {
1083                        if self.dialect == ShellDialect::Zsh
1084                            && self
1085                                .current_token
1086                                .as_ref()
1087                                .is_some_and(|token| !token.flags.is_synthetic())
1088                        {
1089                            let start = self.current_span.start;
1090                            if let Some((text, end)) = self.scan_source_word(start) {
1091                                let span = Span::from_positions(start, end);
1092                                let word = self.parse_word_with_context(&text, span, start, true);
1093                                self.advance_past_word(&word);
1094                                words.push(word);
1095                                continue;
1096                            }
1097                        }
1098
1099                        let word = self
1100                            .take_current_word_and_advance()
1101                            .ok_or_else(|| self.error("expected for word"))?;
1102                        words.push(word);
1103                    }
1104                    Some(_) | None => {
1105                        self.pop_depth();
1106                        return Err(self.error("expected ')' after for word list"));
1107                    }
1108                }
1109            }
1110
1111            let right_paren_span = self.current_span;
1112            self.advance();
1113            if self.at(TokenKind::Semicolon) {
1114                self.advance();
1115            }
1116            self.skip_newlines()?;
1117
1118            (
1119                Some(words),
1120                ForHeaderSurface::Paren {
1121                    left_paren_span,
1122                    right_paren_span,
1123                },
1124            )
1125        } else if self.is_keyword(Keyword::In) {
1126            let in_span = self.current_span;
1127            self.advance();
1128
1129            let (words, saw_separator) = self.parse_for_word_list_until_body_separator()?;
1130            if !saw_separator {
1131                self.pop_depth();
1132                return Err(self.error("expected ';' or newline before for loop body"));
1133            }
1134            (
1135                Some(words),
1136                ForHeaderSurface::In {
1137                    in_span: Some(in_span),
1138                },
1139            )
1140        } else {
1141            if self.at(TokenKind::Semicolon) {
1142                self.advance();
1143            }
1144            self.skip_newlines()?;
1145            (None, ForHeaderSurface::In { in_span: None })
1146        };
1147
1148        let (body, syntax, end_span) = match header {
1149            ForHeaderSurface::In { in_span }
1150                if allow_zsh_targets && self.at(TokenKind::LeftBrace) =>
1151            {
1152                let (body, left_brace_span, right_brace_span) = self
1153                    .parse_brace_enclosed_stmt_seq(
1154                        "syntax error: empty for loop body",
1155                        BraceBodyContext::Ordinary,
1156                    )?;
1157                (
1158                    body,
1159                    ForSyntax::InBrace {
1160                        in_span,
1161                        left_brace_span,
1162                        right_brace_span,
1163                    },
1164                    right_brace_span,
1165                )
1166            }
1167            ForHeaderSurface::Paren {
1168                left_paren_span,
1169                right_paren_span,
1170            } if allow_zsh_targets && self.at(TokenKind::LeftBrace) => {
1171                let (body, left_brace_span, right_brace_span) = self
1172                    .parse_brace_enclosed_stmt_seq(
1173                        "syntax error: empty for loop body",
1174                        BraceBodyContext::Ordinary,
1175                    )?;
1176                (
1177                    body,
1178                    ForSyntax::ParenBrace {
1179                        left_paren_span,
1180                        right_paren_span,
1181                        left_brace_span,
1182                        right_brace_span,
1183                    },
1184                    right_brace_span,
1185                )
1186            }
1187            ForHeaderSurface::In { in_span }
1188                if allow_zsh_targets && !self.is_keyword(Keyword::Do) =>
1189            {
1190                let stmt = self.parse_single_stmt_command()?;
1191                let span = stmt.span;
1192                (
1193                    Self::stmt_seq_with_span(span, vec![stmt]),
1194                    ForSyntax::InDirect { in_span },
1195                    span,
1196                )
1197            }
1198            ForHeaderSurface::In { in_span } => {
1199                let do_span = if self.is_keyword(Keyword::Do) {
1200                    self.current_span
1201                } else {
1202                    self.pop_depth();
1203                    return Err(self.error("expected 'do'"));
1204                };
1205                self.advance();
1206                self.skip_newlines()?;
1207
1208                let body_start = self.current_span.start;
1209                let body = self.parse_compound_list(Keyword::Done)?;
1210                let body_span = Span::from_positions(body_start, self.current_span.start);
1211                if body.is_empty() && self.dialect != ShellDialect::Zsh {
1212                    self.pop_depth();
1213                    return Err(self.error("syntax error: empty for loop body"));
1214                }
1215                if !self.is_keyword(Keyword::Done) {
1216                    self.pop_depth();
1217                    return Err(self.error("expected 'done'"));
1218                }
1219                let done_span = self.current_span;
1220                self.advance();
1221                let body = if body.is_empty() {
1222                    Self::stmt_seq_with_span(body_span, Vec::new())
1223                } else {
1224                    Self::stmt_seq_with_span(body_span, body)
1225                };
1226                (
1227                    body,
1228                    ForSyntax::InDoDone {
1229                        in_span,
1230                        do_span,
1231                        done_span,
1232                    },
1233                    done_span,
1234                )
1235            }
1236            ForHeaderSurface::Paren {
1237                left_paren_span,
1238                right_paren_span,
1239            } if allow_zsh_targets && !self.is_keyword(Keyword::Do) => {
1240                let stmt = self.parse_single_stmt_command()?;
1241                let span = stmt.span;
1242                (
1243                    Self::stmt_seq_with_span(span, vec![stmt]),
1244                    ForSyntax::ParenDirect {
1245                        left_paren_span,
1246                        right_paren_span,
1247                    },
1248                    span,
1249                )
1250            }
1251            ForHeaderSurface::Paren {
1252                left_paren_span,
1253                right_paren_span,
1254            } => {
1255                let do_span = if self.is_keyword(Keyword::Do) {
1256                    self.current_span
1257                } else {
1258                    self.pop_depth();
1259                    return Err(self.error("expected 'do'"));
1260                };
1261                self.advance();
1262                self.skip_newlines()?;
1263
1264                let body_start = self.current_span.start;
1265                let body = self.parse_compound_list(Keyword::Done)?;
1266                let body_span = Span::from_positions(body_start, self.current_span.start);
1267                if body.is_empty() && self.dialect != ShellDialect::Zsh {
1268                    self.pop_depth();
1269                    return Err(self.error("syntax error: empty for loop body"));
1270                }
1271                if !self.is_keyword(Keyword::Done) {
1272                    self.pop_depth();
1273                    return Err(self.error("expected 'done'"));
1274                }
1275                let done_span = self.current_span;
1276                self.advance();
1277                let body = if body.is_empty() {
1278                    Self::stmt_seq_with_span(body_span, Vec::new())
1279                } else {
1280                    Self::stmt_seq_with_span(body_span, body)
1281                };
1282                (
1283                    body,
1284                    ForSyntax::ParenDoDone {
1285                        left_paren_span,
1286                        right_paren_span,
1287                        do_span,
1288                        done_span,
1289                    },
1290                    done_span,
1291                )
1292            }
1293        };
1294
1295        self.pop_depth();
1296        Ok(CompoundCommand::For(ForCommand {
1297            targets: targets.into_vec(),
1298            words: words.map(SmallVec::into_vec),
1299            body,
1300            syntax,
1301            span: start_span.merge(end_span),
1302        }))
1303    }
1304
1305    fn parse_for_targets(&mut self, allow_zsh_targets: bool) -> Result<SmallVec<[ForTarget; 1]>> {
1306        let allow_digits = allow_zsh_targets;
1307        let first_target = self
1308            .current_for_target(allow_digits)
1309            .ok_or_else(|| Error::parse("expected variable name in for loop".to_string()))?;
1310        let first_word = first_target.word.clone();
1311        self.advance_past_word(&first_word);
1312
1313        let mut targets = SmallVec::from_vec(vec![first_target]);
1314        if !allow_zsh_targets {
1315            return Ok(targets);
1316        }
1317
1318        loop {
1319            if self.current_keyword() == Some(Keyword::In)
1320                || matches!(
1321                    self.current_token_kind,
1322                    Some(TokenKind::LeftParen | TokenKind::Semicolon | TokenKind::Newline)
1323                )
1324                || self.at(TokenKind::LeftBrace)
1325                || self.is_keyword(Keyword::Do)
1326            {
1327                break;
1328            }
1329
1330            let target = self
1331                .current_for_target(true)
1332                .ok_or_else(|| Error::parse("expected variable name in for loop".to_string()))?;
1333            let word = target.word.clone();
1334            self.advance_past_word(&word);
1335            targets.push(target);
1336        }
1337
1338        Ok(targets)
1339    }
1340
1341    fn current_for_target(&mut self, allow_digits: bool) -> Option<ForTarget> {
1342        let name = self.current_word_str().and_then(|name| {
1343            (Self::is_valid_identifier(name)
1344                || (allow_digits && name.bytes().all(|byte| byte.is_ascii_digit())))
1345            .then(|| Name::from(name))
1346        });
1347        let word = self.current_word()?;
1348        Some(ForTarget {
1349            span: word.span,
1350            word,
1351            name,
1352        })
1353    }
1354
1355    fn parse_for_word_list_until_body_separator(&mut self) -> Result<(SmallVec<[Word; 2]>, bool)> {
1356        let mut words = SmallVec::<[Word; 2]>::new();
1357        loop {
1358            match self.current_token_kind {
1359                Some(kind)
1360                    if kind.is_word_like()
1361                        || (self.dialect == ShellDialect::Zsh
1362                            && matches!(kind, TokenKind::LeftParen)) =>
1363                {
1364                    if self.dialect == ShellDialect::Zsh
1365                        && self
1366                            .current_token
1367                            .as_ref()
1368                            .is_some_and(|token| !token.flags.is_synthetic())
1369                    {
1370                        let start = self.current_span.start;
1371                        if let Some((text, end)) = self.scan_source_word(start) {
1372                            let span = Span::from_positions(start, end);
1373                            let word = self.parse_word_with_context(&text, span, start, true);
1374                            self.advance_past_word(&word);
1375                            words.push(word);
1376                            continue;
1377                        }
1378                    }
1379
1380                    let word = self
1381                        .take_current_word_and_advance()
1382                        .ok_or_else(|| self.error("expected for word"))?;
1383                    words.push(word);
1384                }
1385                Some(TokenKind::Semicolon) => {
1386                    self.advance();
1387                    self.skip_newlines()?;
1388                    return Ok((words, true));
1389                }
1390                Some(TokenKind::Newline) => {
1391                    self.skip_newlines()?;
1392                    return Ok((words, true));
1393                }
1394                _ => return Ok((words, false)),
1395            }
1396        }
1397    }
1398
1399    /// Parse a zsh repeat loop.
1400    fn parse_repeat(&mut self) -> Result<CompoundCommand> {
1401        self.ensure_repeat_loop()?;
1402        let start_span = self.current_span;
1403        self.push_depth()?;
1404        self.advance(); // consume 'repeat'
1405
1406        let count = match self.current_token_kind {
1407            Some(kind) if kind.is_word_like() => self.expect_word()?,
1408            _ => {
1409                self.pop_depth();
1410                return Err(self.error("expected loop count in repeat"));
1411            }
1412        };
1413
1414        let (syntax, body, end_span) = match self.current_token_kind {
1415            _ if self.is_keyword(Keyword::Do) => {
1416                let do_span = self.current_span;
1417                self.advance();
1418                self.skip_newlines()?;
1419
1420                let body_start = self.current_span.start;
1421                let body = self.parse_compound_list(Keyword::Done)?;
1422                let body_span = Span::from_positions(body_start, self.current_span.start);
1423                if body.is_empty() {
1424                    self.pop_depth();
1425                    return Err(self.error("syntax error: empty repeat loop body"));
1426                }
1427                if !self.is_keyword(Keyword::Done) {
1428                    self.pop_depth();
1429                    return Err(self.error("expected 'done'"));
1430                }
1431                let done_span = self.current_span;
1432                self.advance();
1433                (
1434                    RepeatSyntax::DoDone { do_span, done_span },
1435                    Self::stmt_seq_with_span(body_span, body),
1436                    done_span,
1437                )
1438            }
1439            Some(TokenKind::LeftBrace) => {
1440                let (body, left_brace_span, right_brace_span) = self
1441                    .parse_brace_enclosed_stmt_seq(
1442                        "syntax error: empty repeat loop body",
1443                        BraceBodyContext::Ordinary,
1444                    )?;
1445                (
1446                    RepeatSyntax::Brace {
1447                        left_brace_span,
1448                        right_brace_span,
1449                    },
1450                    body,
1451                    right_brace_span,
1452                )
1453            }
1454            Some(TokenKind::Semicolon) => {
1455                self.advance();
1456                self.skip_newlines()?;
1457                if !self.is_keyword(Keyword::Do) {
1458                    self.pop_depth();
1459                    return Err(self.error("expected 'do' after repeat count"));
1460                }
1461                let do_span = self.current_span;
1462                self.advance();
1463                self.skip_newlines()?;
1464
1465                let body_start = self.current_span.start;
1466                let body = self.parse_compound_list(Keyword::Done)?;
1467                let body_span = Span::from_positions(body_start, self.current_span.start);
1468                if body.is_empty() {
1469                    self.pop_depth();
1470                    return Err(self.error("syntax error: empty repeat loop body"));
1471                }
1472                if !self.is_keyword(Keyword::Done) {
1473                    self.pop_depth();
1474                    return Err(self.error("expected 'done'"));
1475                }
1476                let done_span = self.current_span;
1477                self.advance();
1478                (
1479                    RepeatSyntax::DoDone { do_span, done_span },
1480                    Self::stmt_seq_with_span(body_span, body),
1481                    done_span,
1482                )
1483            }
1484            Some(TokenKind::Newline) => {
1485                self.skip_newlines()?;
1486                if !self.is_keyword(Keyword::Do) {
1487                    self.pop_depth();
1488                    return Err(self.error("expected 'do' after repeat count"));
1489                }
1490                let do_span = self.current_span;
1491                self.advance();
1492                self.skip_newlines()?;
1493
1494                let body_start = self.current_span.start;
1495                let body = self.parse_compound_list(Keyword::Done)?;
1496                let body_span = Span::from_positions(body_start, self.current_span.start);
1497                if body.is_empty() {
1498                    self.pop_depth();
1499                    return Err(self.error("syntax error: empty repeat loop body"));
1500                }
1501                if !self.is_keyword(Keyword::Done) {
1502                    self.pop_depth();
1503                    return Err(self.error("expected 'done'"));
1504                }
1505                let done_span = self.current_span;
1506                self.advance();
1507                (
1508                    RepeatSyntax::DoDone { do_span, done_span },
1509                    Self::stmt_seq_with_span(body_span, body),
1510                    done_span,
1511                )
1512            }
1513            _ => {
1514                let stmt = self.parse_single_stmt_command()?;
1515                let span = stmt.span;
1516                (
1517                    RepeatSyntax::Direct,
1518                    Self::stmt_seq_with_span(span, vec![stmt]),
1519                    span,
1520                )
1521            }
1522        };
1523
1524        self.pop_depth();
1525        Ok(CompoundCommand::Repeat(RepeatCommand {
1526            count,
1527            body,
1528            syntax,
1529            span: start_span.merge(end_span),
1530        }))
1531    }
1532
1533    /// Parse a zsh foreach loop.
1534    fn parse_foreach(&mut self) -> Result<CompoundCommand> {
1535        self.ensure_foreach_loop()?;
1536        let start_span = self.current_span;
1537        self.push_depth()?;
1538        self.advance(); // consume 'foreach'
1539
1540        let (variable, variable_span) = match self.current_name_token() {
1541            Some(pair) => pair,
1542            _ => {
1543                self.pop_depth();
1544                return Err(self.error("expected variable name in foreach"));
1545            }
1546        };
1547        self.advance();
1548
1549        let (words, body, syntax, end_span) = if self.at(TokenKind::LeftParen) {
1550            let left_paren_span = self.current_span;
1551            self.advance();
1552
1553            let mut words = SmallVec::<[Word; 2]>::new();
1554            while !self.at(TokenKind::RightParen) {
1555                match self.current_token_kind {
1556                    Some(kind) if kind.is_word_like() => {
1557                        let word = self
1558                            .take_current_word_and_advance()
1559                            .ok_or_else(|| self.error("expected foreach word"))?;
1560                        words.push(word);
1561                    }
1562                    Some(_) | None => {
1563                        self.pop_depth();
1564                        return Err(self.error("expected ')' after foreach word list"));
1565                    }
1566                }
1567            }
1568            if words.is_empty() {
1569                self.pop_depth();
1570                return Err(self.error("expected word list in foreach"));
1571            }
1572
1573            let right_paren_span = self.current_span;
1574            self.advance();
1575            if !self.at(TokenKind::LeftBrace) {
1576                self.pop_depth();
1577                return Err(self.error("expected '{' after foreach word list"));
1578            }
1579
1580            let (body, left_brace_span, right_brace_span) = self.parse_brace_enclosed_stmt_seq(
1581                "syntax error: empty foreach loop body",
1582                BraceBodyContext::Ordinary,
1583            )?;
1584            (
1585                words,
1586                body,
1587                ForeachSyntax::ParenBrace {
1588                    left_paren_span,
1589                    right_paren_span,
1590                    left_brace_span,
1591                    right_brace_span,
1592                },
1593                right_brace_span,
1594            )
1595        } else if self.is_keyword(Keyword::In) {
1596            let in_span = self.current_span;
1597            self.advance();
1598
1599            let mut words = SmallVec::<[Word; 2]>::new();
1600            let saw_separator = loop {
1601                match self.current_token_kind {
1602                    _ if self.current_keyword() == Some(Keyword::Do) => break false,
1603                    Some(kind) if kind.is_word_like() => {
1604                        let word = self
1605                            .take_current_word_and_advance()
1606                            .ok_or_else(|| self.error("expected foreach word"))?;
1607                        words.push(word);
1608                    }
1609                    Some(TokenKind::Semicolon) => {
1610                        self.advance();
1611                        break true;
1612                    }
1613                    Some(TokenKind::Newline) => {
1614                        self.skip_newlines()?;
1615                        break true;
1616                    }
1617                    _ => break false,
1618                }
1619            };
1620            if words.is_empty() {
1621                self.pop_depth();
1622                return Err(self.error("expected word list in foreach"));
1623            }
1624            if !saw_separator {
1625                self.pop_depth();
1626                return Err(self.error("expected ';' or newline before 'do' in foreach"));
1627            }
1628            if !self.is_keyword(Keyword::Do) {
1629                self.pop_depth();
1630                return Err(self.error("expected 'do' in foreach"));
1631            }
1632            let do_span = self.current_span;
1633            self.advance();
1634            self.skip_newlines()?;
1635
1636            let body_start = self.current_span.start;
1637            let body = self.parse_compound_list(Keyword::Done)?;
1638            let body_span = Span::from_positions(body_start, self.current_span.start);
1639            if body.is_empty() {
1640                self.pop_depth();
1641                return Err(self.error("syntax error: empty foreach loop body"));
1642            }
1643            if !self.is_keyword(Keyword::Done) {
1644                self.pop_depth();
1645                return Err(self.error("expected 'done'"));
1646            }
1647            let done_span = self.current_span;
1648            self.advance();
1649            (
1650                words,
1651                Self::stmt_seq_with_span(body_span, body),
1652                ForeachSyntax::InDoDone {
1653                    in_span,
1654                    do_span,
1655                    done_span,
1656                },
1657                done_span,
1658            )
1659        } else {
1660            self.pop_depth();
1661            return Err(self.error("expected '(' or 'in' after foreach variable"));
1662        };
1663
1664        self.pop_depth();
1665        Ok(CompoundCommand::Foreach(ForeachCommand {
1666            variable,
1667            variable_span,
1668            words: words.into_vec(),
1669            body,
1670            syntax,
1671            span: start_span.merge(end_span),
1672        }))
1673    }
1674
1675    /// Parse select loop: select var in list; do body; done
1676    fn parse_select(&mut self) -> Result<CompoundCommand> {
1677        self.ensure_select_loop()?;
1678        let start_span = self.current_span;
1679        self.push_depth()?;
1680        self.advance(); // consume 'select'
1681        self.skip_newlines()?;
1682
1683        // Expect variable name
1684        let (variable, variable_span) = match self.current_name_token() {
1685            Some(pair) => pair,
1686            _ => {
1687                self.pop_depth();
1688                return Err(Error::parse("expected variable name in select".to_string()));
1689            }
1690        };
1691        self.advance();
1692
1693        // Expect 'in' keyword
1694        if !self.is_keyword(Keyword::In) {
1695            self.pop_depth();
1696            return Err(Error::parse("expected 'in' in select".to_string()));
1697        }
1698        self.advance(); // consume 'in'
1699
1700        // Parse word list until do/newline/;
1701        let mut words = SmallVec::<[Word; 2]>::new();
1702        loop {
1703            match self.current_token_kind {
1704                _ if self.current_keyword() == Some(Keyword::Do) => break,
1705                Some(kind) if kind.is_word_like() => {
1706                    if let Some(word) = self.take_current_word_and_advance() {
1707                        words.push(word);
1708                    }
1709                }
1710                Some(TokenKind::Newline | TokenKind::Semicolon) => {
1711                    self.advance();
1712                    break;
1713                }
1714                _ => break,
1715            }
1716        }
1717
1718        self.skip_newlines()?;
1719
1720        // Expect 'do'
1721        self.expect_keyword(Keyword::Do)?;
1722        self.skip_newlines()?;
1723
1724        // Parse body
1725        let body_start = self.current_span.start;
1726        let body = self.parse_compound_list(Keyword::Done)?;
1727        let body_span = Span::from_positions(body_start, self.current_span.start);
1728
1729        // Bash requires at least one command in loop body
1730        if body.is_empty() {
1731            self.pop_depth();
1732            return Err(self.error("syntax error: empty select loop body"));
1733        }
1734        let body = Self::stmt_seq_with_span(body_span, body);
1735
1736        // Expect 'done'
1737        self.expect_keyword(Keyword::Done)?;
1738
1739        self.pop_depth();
1740        Ok(CompoundCommand::Select(SelectCommand {
1741            variable,
1742            variable_span,
1743            words: words.into_vec(),
1744            body,
1745            span: start_span.merge(self.current_span),
1746        }))
1747    }
1748
1749    /// Parse C-style arithmetic for loop inner: for ((init; cond; step)); do body; done
1750    /// Note: depth tracking is done by parse_for which calls this
1751    fn parse_arithmetic_for_inner(&mut self, start_span: Span) -> Result<CompoundCommand> {
1752        self.ensure_arithmetic_for()?;
1753        let left_paren_span = self.current_span;
1754        self.advance(); // consume '(('
1755
1756        let mut paren_depth = 0_i32;
1757        let mut segment_start = left_paren_span.end;
1758        let mut init_span = None;
1759        let mut first_semicolon_span = None;
1760        let mut condition_span = None;
1761        let mut second_semicolon_span = None;
1762
1763        let right_paren_span = loop {
1764            match self.current_token_kind {
1765                Some(TokenKind::DoubleLeftParen) => {
1766                    paren_depth += 2;
1767                    self.advance();
1768                }
1769                Some(TokenKind::LeftParen) => {
1770                    paren_depth += 1;
1771                    self.advance();
1772                }
1773                Some(TokenKind::ProcessSubIn) | Some(TokenKind::ProcessSubOut) => {
1774                    paren_depth += 1;
1775                    self.advance();
1776                }
1777                Some(TokenKind::DoubleRightParen) => {
1778                    if paren_depth == 0 {
1779                        let right_paren_span = self.current_span;
1780                        self.advance();
1781                        break right_paren_span;
1782                    }
1783                    if paren_depth == 1 {
1784                        break self.split_nested_arithmetic_close("arithmetic for header")?;
1785                    }
1786                    paren_depth -= 2;
1787                    self.advance();
1788                }
1789                Some(TokenKind::RightParen) => {
1790                    if paren_depth > 0 {
1791                        paren_depth -= 1;
1792                    }
1793                    self.advance();
1794                }
1795                Some(TokenKind::DoubleSemicolon) if paren_depth == 0 => {
1796                    let (first_span, second_span) = Self::split_double_semicolon(self.current_span);
1797                    Self::record_arithmetic_for_separator(
1798                        first_span,
1799                        &mut segment_start,
1800                        &mut init_span,
1801                        &mut first_semicolon_span,
1802                        &mut condition_span,
1803                        &mut second_semicolon_span,
1804                    )?;
1805                    Self::record_arithmetic_for_separator(
1806                        second_span,
1807                        &mut segment_start,
1808                        &mut init_span,
1809                        &mut first_semicolon_span,
1810                        &mut condition_span,
1811                        &mut second_semicolon_span,
1812                    )?;
1813                    self.advance();
1814                }
1815                Some(TokenKind::Semicolon) if paren_depth == 0 => {
1816                    Self::record_arithmetic_for_separator(
1817                        self.current_span,
1818                        &mut segment_start,
1819                        &mut init_span,
1820                        &mut first_semicolon_span,
1821                        &mut condition_span,
1822                        &mut second_semicolon_span,
1823                    )?;
1824                    self.advance();
1825                }
1826                Some(_) => {
1827                    self.advance();
1828                }
1829                None => {
1830                    return Err(Error::parse(
1831                        "unexpected end of input in for loop".to_string(),
1832                    ));
1833                }
1834            }
1835        };
1836
1837        let first_semicolon_span = first_semicolon_span
1838            .ok_or_else(|| Error::parse("expected ';' in arithmetic for header".to_string()))?;
1839        let second_semicolon_span = second_semicolon_span.ok_or_else(|| {
1840            Error::parse("expected second ';' in arithmetic for header".to_string())
1841        })?;
1842        let step_span = Self::optional_span(segment_start, right_paren_span.start);
1843        let init_ast =
1844            self.parse_explicit_arithmetic_span(init_span, "invalid arithmetic for init")?;
1845        let condition_ast = self
1846            .parse_explicit_arithmetic_span(condition_span, "invalid arithmetic for condition")?;
1847        let step_ast =
1848            self.parse_explicit_arithmetic_span(step_span, "invalid arithmetic for step")?;
1849
1850        self.skip_newlines()?;
1851
1852        // Skip optional semicolon after ))
1853        if self.at(TokenKind::Semicolon) {
1854            self.advance();
1855        }
1856        self.skip_newlines()?;
1857
1858        let (body, end_span) = if self.at(TokenKind::LeftBrace) {
1859            let body = self.parse_brace_group(BraceBodyContext::Ordinary)?;
1860            let span = Self::compound_span(&body);
1861            (
1862                Self::stmt_seq_with_span(
1863                    span,
1864                    vec![Self::lower_non_sequence_command_to_stmt(Command::Compound(
1865                        Box::new(body),
1866                        SmallVec::<[Redirect; 1]>::new(),
1867                    ))],
1868                ),
1869                self.current_span,
1870            )
1871        } else {
1872            // Expect 'do'
1873            self.expect_keyword(Keyword::Do)?;
1874            self.skip_newlines()?;
1875
1876            // Parse body
1877            let body_start = self.current_span.start;
1878            let body = self.parse_compound_list(Keyword::Done)?;
1879            let body_span = Span::from_positions(body_start, self.current_span.start);
1880
1881            // Bash requires at least one command in loop body
1882            if body.is_empty() {
1883                return Err(self.error("syntax error: empty for loop body"));
1884            }
1885
1886            // Expect 'done'
1887            if !self.is_keyword(Keyword::Done) {
1888                return Err(self.error("expected 'done'"));
1889            }
1890            let done_span = self.current_span;
1891            self.advance();
1892            (Self::stmt_seq_with_span(body_span, body), done_span)
1893        };
1894
1895        Ok(CompoundCommand::ArithmeticFor(Box::new(
1896            ArithmeticForCommand {
1897                left_paren_span,
1898                init_span,
1899                init_ast,
1900                first_semicolon_span,
1901                condition_span,
1902                condition_ast,
1903                second_semicolon_span,
1904                step_span,
1905                step_ast,
1906                right_paren_span,
1907                body,
1908                span: start_span.merge(end_span),
1909            },
1910        )))
1911    }
1912
1913    /// Parse a while loop
1914    fn parse_while(&mut self) -> Result<CompoundCommand> {
1915        let start_span = self.current_span;
1916        self.push_depth()?;
1917        self.advance(); // consume 'while'
1918        self.skip_newlines()?;
1919
1920        // Parse condition
1921        let condition_start = self.current_span.start;
1922        let allow_brace_body = self.dialect == ShellDialect::Zsh && self.zsh_brace_bodies_enabled();
1923        let condition = self.parse_loop_condition_until_body_start(allow_brace_body)?;
1924        let condition_span = Span::from_positions(condition_start, self.current_span.start);
1925        let condition = Self::stmt_seq_with_span(condition_span, condition);
1926
1927        let (body, end_span) = if allow_brace_body && self.at(TokenKind::LeftBrace) {
1928            let body = self.parse_brace_group(BraceBodyContext::Ordinary)?;
1929            let span = Self::compound_span(&body);
1930            (
1931                Self::stmt_seq_with_span(
1932                    span,
1933                    vec![Self::lower_non_sequence_command_to_stmt(Command::Compound(
1934                        Box::new(body),
1935                        SmallVec::<[Redirect; 1]>::new(),
1936                    ))],
1937                ),
1938                self.current_span,
1939            )
1940        } else if let Some((body, left_brace_span, right_brace_span)) = allow_brace_body
1941            .then(|| self.try_parse_compact_zsh_brace_body(BraceBodyContext::Ordinary))
1942            .transpose()?
1943            .flatten()
1944        {
1945            let brace_group = CompoundCommand::BraceGroup(body);
1946            let span = left_brace_span.merge(right_brace_span);
1947            (
1948                Self::stmt_seq_with_span(
1949                    span,
1950                    vec![Self::lower_non_sequence_command_to_stmt(Command::Compound(
1951                        Box::new(brace_group),
1952                        SmallVec::<[Redirect; 1]>::new(),
1953                    ))],
1954                ),
1955                right_brace_span,
1956            )
1957        } else {
1958            self.expect_keyword(Keyword::Do)?;
1959            self.skip_newlines()?;
1960
1961            let body_start = self.current_span.start;
1962            let body = self.parse_compound_list(Keyword::Done)?;
1963            let body_span = Span::from_positions(body_start, self.current_span.start);
1964
1965            if body.is_empty() && self.dialect != ShellDialect::Zsh {
1966                self.pop_depth();
1967                return Err(self.error("syntax error: empty while loop body"));
1968            }
1969            let body = Self::stmt_seq_with_span(body_span, body);
1970
1971            self.expect_keyword(Keyword::Done)?;
1972            (body, self.current_span)
1973        };
1974
1975        self.pop_depth();
1976        Ok(CompoundCommand::While(WhileCommand {
1977            condition,
1978            body,
1979            span: start_span.merge(end_span),
1980        }))
1981    }
1982
1983    /// Parse an until loop
1984    fn parse_until(&mut self) -> Result<CompoundCommand> {
1985        let start_span = self.current_span;
1986        self.push_depth()?;
1987        self.advance(); // consume 'until'
1988        self.skip_newlines()?;
1989
1990        // Parse condition
1991        let condition_start = self.current_span.start;
1992        let allow_brace_body = self.dialect == ShellDialect::Zsh && self.zsh_brace_bodies_enabled();
1993        let condition = self.parse_loop_condition_until_body_start(allow_brace_body)?;
1994        let condition_span = Span::from_positions(condition_start, self.current_span.start);
1995        let condition = Self::stmt_seq_with_span(condition_span, condition);
1996
1997        let (body, end_span) = if allow_brace_body && self.at(TokenKind::LeftBrace) {
1998            let body = self.parse_brace_group(BraceBodyContext::Ordinary)?;
1999            let span = Self::compound_span(&body);
2000            (
2001                Self::stmt_seq_with_span(
2002                    span,
2003                    vec![Self::lower_non_sequence_command_to_stmt(Command::Compound(
2004                        Box::new(body),
2005                        SmallVec::<[Redirect; 1]>::new(),
2006                    ))],
2007                ),
2008                self.current_span,
2009            )
2010        } else if let Some((body, left_brace_span, right_brace_span)) = allow_brace_body
2011            .then(|| self.try_parse_compact_zsh_brace_body(BraceBodyContext::Ordinary))
2012            .transpose()?
2013            .flatten()
2014        {
2015            let brace_group = CompoundCommand::BraceGroup(body);
2016            let span = left_brace_span.merge(right_brace_span);
2017            (
2018                Self::stmt_seq_with_span(
2019                    span,
2020                    vec![Self::lower_non_sequence_command_to_stmt(Command::Compound(
2021                        Box::new(brace_group),
2022                        SmallVec::<[Redirect; 1]>::new(),
2023                    ))],
2024                ),
2025                right_brace_span,
2026            )
2027        } else {
2028            self.expect_keyword(Keyword::Do)?;
2029            self.skip_newlines()?;
2030
2031            let body_start = self.current_span.start;
2032            let body = self.parse_compound_list(Keyword::Done)?;
2033            let body_span = Span::from_positions(body_start, self.current_span.start);
2034
2035            if body.is_empty() && self.dialect != ShellDialect::Zsh {
2036                self.pop_depth();
2037                return Err(self.error("syntax error: empty until loop body"));
2038            }
2039            let body = Self::stmt_seq_with_span(body_span, body);
2040
2041            self.expect_keyword(Keyword::Done)?;
2042            (body, self.current_span)
2043        };
2044
2045        self.pop_depth();
2046        Ok(CompoundCommand::Until(UntilCommand {
2047            condition,
2048            body,
2049            span: start_span.merge(end_span),
2050        }))
2051    }
2052
2053    /// Parse a case statement: case WORD in pattern) commands ;; ... esac
2054    fn parse_case(&mut self) -> Result<CompoundCommand> {
2055        let start_span = self.current_span;
2056        self.push_depth()?;
2057        self.advance(); // consume 'case'
2058        self.skip_newlines()?;
2059
2060        // Get the word to match against
2061        let word = self.expect_word()?;
2062        self.skip_newlines()?;
2063
2064        // Expect 'in'
2065        self.expect_keyword(Keyword::In)?;
2066        self.skip_newlines()?;
2067
2068        // Parse case items
2069        let mut cases = Vec::new();
2070        while !self.is_keyword(Keyword::Esac) && self.current_token.is_some() {
2071            self.skip_newlines()?;
2072            if self.is_keyword(Keyword::Esac) {
2073                break;
2074            }
2075
2076            let patterns = match self.parse_case_patterns() {
2077                Ok(patterns) => patterns,
2078                Err(err) => {
2079                    self.pop_depth();
2080                    return Err(err);
2081                }
2082            };
2083            self.skip_newlines()?;
2084
2085            // Parse commands until ;; or esac
2086            let body_start = self.current_span.start;
2087            let mut commands = Vec::new();
2088            while !self.is_case_terminator()
2089                && !self.is_keyword(Keyword::Esac)
2090                && self.current_token.is_some()
2091            {
2092                commands.extend(self.parse_command_list_required()?);
2093                self.skip_newlines()?;
2094            }
2095
2096            let (terminator, terminator_span) = self.parse_case_terminator();
2097            let body_span = Span::from_positions(body_start, self.current_span.start);
2098            cases.push(CaseItem {
2099                patterns,
2100                body: Self::stmt_seq_with_span(body_span, commands),
2101                terminator,
2102                terminator_span,
2103            });
2104            self.skip_newlines()?;
2105        }
2106
2107        // Expect 'esac'
2108        self.expect_keyword(Keyword::Esac)?;
2109
2110        self.pop_depth();
2111        Ok(CompoundCommand::Case(CaseCommand {
2112            word,
2113            cases,
2114            span: start_span.merge(self.current_span),
2115        }))
2116    }
2117
2118    fn parse_case_patterns(&mut self) -> Result<Vec<Pattern>> {
2119        self.record_zsh_case_group_parts_from_current_case_header();
2120        if self.dialect == ShellDialect::Zsh {
2121            self.parse_zsh_case_patterns()
2122        } else {
2123            self.parse_posix_case_patterns()
2124        }
2125    }
2126
2127    fn record_zsh_case_group_parts_from_current_case_header(&mut self) {
2128        let Ok((pattern_spans, _)) = self.scan_zsh_case_pattern_spans() else {
2129            return;
2130        };
2131
2132        for span in pattern_spans {
2133            let pattern = self.pattern_from_zsh_case_span(span);
2134            for (index, part) in pattern.parts.iter().enumerate() {
2135                if matches!(
2136                    &part.kind,
2137                    PatternPart::Group {
2138                        kind: PatternGroupKind::ExactlyOne,
2139                        ..
2140                    }
2141                ) && part.span.slice(self.input).starts_with('(')
2142                {
2143                    self.record_zsh_case_group_part(index, part.span);
2144                }
2145            }
2146        }
2147    }
2148
2149    fn parse_posix_case_patterns(&mut self) -> Result<Vec<Pattern>> {
2150        if self.at(TokenKind::LeftParen) {
2151            self.advance();
2152        }
2153
2154        let mut patterns = Vec::new();
2155        while self.at_word_like() {
2156            if let Some(word) = self.take_current_word_and_advance() {
2157                patterns.push(self.pattern_from_word(&word));
2158            }
2159
2160            if self.at(TokenKind::Pipe) {
2161                self.advance();
2162            } else {
2163                break;
2164            }
2165        }
2166
2167        if !self.at(TokenKind::RightParen) {
2168            return Err(self.error("expected ')' after case pattern"));
2169        }
2170        self.advance();
2171
2172        Ok(patterns)
2173    }
2174
2175    fn parse_zsh_case_patterns(&mut self) -> Result<Vec<Pattern>> {
2176        let (pattern_spans, delimiter_span) = self.scan_zsh_case_pattern_spans()?;
2177        let patterns = pattern_spans
2178            .into_iter()
2179            .map(|span| self.pattern_from_zsh_case_span(span))
2180            .collect::<Vec<_>>();
2181
2182        while self.current_token.is_some()
2183            && self.current_span.start.offset < delimiter_span.end.offset
2184        {
2185            self.advance();
2186        }
2187
2188        Ok(patterns)
2189    }
2190
2191    fn scan_zsh_case_pattern_spans(&self) -> Result<(Vec<Span>, Span)> {
2192        let start = self.current_span.start;
2193        let Some((spans, delimiter_span)) = self.try_scan_zsh_case_pattern_spans(start) else {
2194            return Err(self.error("expected ')' after case pattern"));
2195        };
2196        if spans.is_empty() {
2197            return Err(self.error("expected ')' after case pattern"));
2198        }
2199        Ok((spans, delimiter_span))
2200    }
2201
2202    fn try_scan_zsh_case_pattern_spans(&self, start: Position) -> Option<(Vec<Span>, Span)> {
2203        if self.input[start.offset..].starts_with('(')
2204            && let Some(wrapper_close) = self.scan_zsh_case_group_close(start)
2205            && self.case_wrapper_close_is_arm_delimiter(wrapper_close)
2206        {
2207            let inner_start = start.advanced_by("(");
2208            let inner_span = Span::from_positions(inner_start, wrapper_close.start);
2209            let patterns = self.split_zsh_case_pattern_alternatives(inner_span)?;
2210            return Some((patterns, wrapper_close));
2211        }
2212
2213        let delimiter_span = self.scan_zsh_case_arm_delimiter(start)?;
2214        let header_span = Span::from_positions(start, delimiter_span.start);
2215        let patterns = self.split_zsh_case_pattern_alternatives(header_span)?;
2216        Some((patterns, delimiter_span))
2217    }
2218
2219    fn case_wrapper_close_is_arm_delimiter(&self, close_span: Span) -> bool {
2220        self.input[close_span.end.offset..]
2221            .chars()
2222            .next()
2223            .is_none_or(char::is_whitespace)
2224    }
2225
2226    fn split_zsh_case_pattern_alternatives(&self, span: Span) -> Option<Vec<Span>> {
2227        let mut state = ZshCaseScanState::new(span.start);
2228        let mut chars = self.input[span.start.offset..span.end.offset]
2229            .chars()
2230            .peekable();
2231        let mut part_start = span.start;
2232        let mut parts = Vec::new();
2233
2234        while let Some(ch) = chars.peek().copied() {
2235            if state.escaped {
2236                state.escaped = false;
2237                Self::next_word_char_unwrap(&mut chars, &mut state.position);
2238                continue;
2239            }
2240
2241            match ch {
2242                '\\' if !state.in_single => {
2243                    state.escaped = true;
2244                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2245                }
2246                '\'' if !state.in_double && !state.in_backtick => {
2247                    state.in_single = !state.in_single;
2248                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2249                }
2250                '"' if !state.in_single && !state.in_backtick => {
2251                    state.in_double = !state.in_double;
2252                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2253                }
2254                '`' if !state.in_single && !state.in_double => {
2255                    state.in_backtick = !state.in_backtick;
2256                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2257                }
2258                '[' if !state.in_single
2259                    && !state.in_double
2260                    && !state.in_backtick
2261                    && state.bracket_depth == 0 =>
2262                {
2263                    state.bracket_depth += 1;
2264                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2265                }
2266                '[' if !state.in_single && !state.in_double && !state.in_backtick => {
2267                    state.bracket_depth += 1;
2268                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2269                }
2270                ']' if !state.in_single
2271                    && !state.in_double
2272                    && !state.in_backtick
2273                    && state.bracket_depth > 0 =>
2274                {
2275                    state.bracket_depth -= 1;
2276                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2277                }
2278                '{' if !state.in_single && !state.in_double && !state.in_backtick => {
2279                    state.brace_depth += 1;
2280                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2281                }
2282                '}' if !state.in_single
2283                    && !state.in_double
2284                    && !state.in_backtick
2285                    && state.brace_depth > 0 =>
2286                {
2287                    state.brace_depth -= 1;
2288                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2289                }
2290                '(' if !state.in_single
2291                    && !state.in_double
2292                    && !state.in_backtick
2293                    && state.bracket_depth == 0
2294                    && state.brace_depth == 0 =>
2295                {
2296                    state.paren_depth += 1;
2297                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2298                }
2299                ')' if !state.in_single
2300                    && !state.in_double
2301                    && !state.in_backtick
2302                    && state.bracket_depth == 0
2303                    && state.brace_depth == 0
2304                    && state.paren_depth > 0 =>
2305                {
2306                    state.paren_depth -= 1;
2307                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2308                }
2309                '|' if !state.in_single
2310                    && !state.in_double
2311                    && !state.in_backtick
2312                    && state.bracket_depth == 0
2313                    && state.brace_depth == 0
2314                    && state.paren_depth == 0 =>
2315                {
2316                    let end = state.position;
2317                    let _ = Self::next_word_char_unwrap(&mut chars, &mut state.position);
2318                    parts.push(
2319                        self.trim_zsh_case_pattern_span(Span::from_positions(part_start, end))?,
2320                    );
2321                    part_start = state.position;
2322                }
2323                _ => {
2324                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2325                }
2326            }
2327        }
2328
2329        parts.push(
2330            self.trim_zsh_case_pattern_span(Span::from_positions(part_start, state.position))?,
2331        );
2332        Some(parts)
2333    }
2334
2335    fn trim_zsh_case_pattern_span(&self, span: Span) -> Option<Span> {
2336        let text = span.slice(self.input);
2337        let trimmed_start = text.len() - text.trim_start_matches(char::is_whitespace).len();
2338        let trimmed_end = text.trim_end_matches(char::is_whitespace).len();
2339        let start = span.start.advanced_by(&text[..trimmed_start]);
2340        let end = span.start.advanced_by(&text[..trimmed_end]);
2341        Some(Span::from_positions(start, end))
2342    }
2343
2344    fn scan_zsh_case_group_close(&self, start: Position) -> Option<Span> {
2345        let mut state = ZshCaseScanState::new(start);
2346        let mut chars = self.input[start.offset..].chars().peekable();
2347
2348        if Self::next_word_char_unwrap(&mut chars, &mut state.position) != '(' {
2349            return None;
2350        }
2351        state.paren_depth = 1;
2352
2353        while let Some(ch) = chars.peek().copied() {
2354            let ch_start = state.position;
2355
2356            if state.escaped {
2357                state.escaped = false;
2358                Self::next_word_char_unwrap(&mut chars, &mut state.position);
2359                continue;
2360            }
2361
2362            match ch {
2363                '\\' if !state.in_single => {
2364                    state.escaped = true;
2365                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2366                }
2367                '\'' if !state.in_double && !state.in_backtick => {
2368                    state.in_single = !state.in_single;
2369                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2370                }
2371                '"' if !state.in_single && !state.in_backtick => {
2372                    state.in_double = !state.in_double;
2373                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2374                }
2375                '`' if !state.in_single && !state.in_double => {
2376                    state.in_backtick = !state.in_backtick;
2377                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2378                }
2379                '[' if !state.in_single && !state.in_double && !state.in_backtick => {
2380                    state.bracket_depth += 1;
2381                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2382                }
2383                ']' if !state.in_single
2384                    && !state.in_double
2385                    && !state.in_backtick
2386                    && state.bracket_depth > 0 =>
2387                {
2388                    state.bracket_depth -= 1;
2389                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2390                }
2391                '{' if !state.in_single && !state.in_double && !state.in_backtick => {
2392                    state.brace_depth += 1;
2393                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2394                }
2395                '}' if !state.in_single
2396                    && !state.in_double
2397                    && !state.in_backtick
2398                    && state.brace_depth > 0 =>
2399                {
2400                    state.brace_depth -= 1;
2401                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2402                }
2403                '(' if !state.in_single
2404                    && !state.in_double
2405                    && !state.in_backtick
2406                    && state.bracket_depth == 0
2407                    && state.brace_depth == 0 =>
2408                {
2409                    state.paren_depth += 1;
2410                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2411                }
2412                ')' if !state.in_single
2413                    && !state.in_double
2414                    && !state.in_backtick
2415                    && state.bracket_depth == 0
2416                    && state.brace_depth == 0
2417                    && state.paren_depth > 0 =>
2418                {
2419                    let _ = Self::next_word_char_unwrap(&mut chars, &mut state.position);
2420                    state.paren_depth -= 1;
2421                    if state.paren_depth == 0 {
2422                        return Some(Span::from_positions(ch_start, state.position));
2423                    }
2424                }
2425                _ => {
2426                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2427                }
2428            }
2429        }
2430
2431        None
2432    }
2433
2434    fn scan_zsh_case_arm_delimiter(&self, start: Position) -> Option<Span> {
2435        let mut state = ZshCaseScanState::new(start);
2436        let mut chars = self.input[start.offset..].chars().peekable();
2437
2438        while let Some(ch) = chars.peek().copied() {
2439            let ch_start = state.position;
2440
2441            if state.escaped {
2442                state.escaped = false;
2443                Self::next_word_char_unwrap(&mut chars, &mut state.position);
2444                continue;
2445            }
2446
2447            match ch {
2448                '\\' if !state.in_single => {
2449                    state.escaped = true;
2450                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2451                }
2452                '\'' if !state.in_double && !state.in_backtick => {
2453                    state.in_single = !state.in_single;
2454                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2455                }
2456                '"' if !state.in_single && !state.in_backtick => {
2457                    state.in_double = !state.in_double;
2458                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2459                }
2460                '`' if !state.in_single && !state.in_double => {
2461                    state.in_backtick = !state.in_backtick;
2462                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2463                }
2464                '[' if !state.in_single && !state.in_double && !state.in_backtick => {
2465                    state.bracket_depth += 1;
2466                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2467                }
2468                ']' if !state.in_single
2469                    && !state.in_double
2470                    && !state.in_backtick
2471                    && state.bracket_depth > 0 =>
2472                {
2473                    state.bracket_depth -= 1;
2474                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2475                }
2476                '{' if !state.in_single && !state.in_double && !state.in_backtick => {
2477                    state.brace_depth += 1;
2478                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2479                }
2480                '}' if !state.in_single
2481                    && !state.in_double
2482                    && !state.in_backtick
2483                    && state.brace_depth > 0 =>
2484                {
2485                    state.brace_depth -= 1;
2486                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2487                }
2488                '(' if !state.in_single
2489                    && !state.in_double
2490                    && !state.in_backtick
2491                    && state.bracket_depth == 0
2492                    && state.brace_depth == 0 =>
2493                {
2494                    state.paren_depth += 1;
2495                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2496                }
2497                ')' if !state.in_single
2498                    && !state.in_double
2499                    && !state.in_backtick
2500                    && state.bracket_depth == 0
2501                    && state.brace_depth == 0 =>
2502                {
2503                    let _ = Self::next_word_char_unwrap(&mut chars, &mut state.position);
2504                    if state.paren_depth == 0 {
2505                        return Some(Span::from_positions(ch_start, state.position));
2506                    }
2507                    state.paren_depth -= 1;
2508                }
2509                _ => {
2510                    Self::next_word_char_unwrap(&mut chars, &mut state.position);
2511                }
2512            }
2513        }
2514
2515        None
2516    }
2517
2518    /// Parse a time command: time [-p] [command]
2519    ///
2520    /// The time keyword measures execution time of the following command.
2521    /// Note: Shuck only tracks wall-clock time, not CPU user/sys time.
2522    fn parse_time(&mut self) -> Result<CompoundCommand> {
2523        let start_span = self.current_span;
2524        self.advance(); // consume 'time'
2525        self.skip_newlines()?;
2526
2527        // Check for -p flag (POSIX format)
2528        let posix_format = if self.at(TokenKind::Word) && self.current_word_str() == Some("-p") {
2529            self.advance();
2530            self.skip_newlines()?;
2531            true
2532        } else {
2533            false
2534        };
2535
2536        // Parse the command to time (if any)
2537        // time with no command is valid in bash (just outputs timing header)
2538        let command = self.parse_pipeline()?.map(Box::new);
2539
2540        Ok(CompoundCommand::Time(TimeCommand {
2541            posix_format,
2542            command,
2543            span: start_span.merge(self.current_span),
2544        }))
2545    }
2546
2547    /// Parse a coproc command: `coproc [NAME] command`
2548    ///
2549    /// If the token after `coproc` is a simple word followed by a compound
2550    /// command (`{`, `(`, `while`, `for`, etc.), it is treated as the coproc
2551    /// name. Otherwise the command starts immediately and the default name
2552    /// "COPROC" is used.
2553    fn parse_coproc(&mut self) -> Result<CompoundCommand> {
2554        self.ensure_coproc()?;
2555        let start_span = self.current_span;
2556        self.advance(); // consume 'coproc'
2557        self.skip_newlines()?;
2558
2559        // Determine if next token is a NAME (simple word that is NOT a compound-
2560        // command keyword and is followed by a compound command start).
2561        let (name, name_span) = if self.at(TokenKind::Word) {
2562            if let Some(word) = self.current_word_str() {
2563                let word = word.to_string();
2564                let word_span = self.current_span;
2565                let is_compound_keyword = matches!(
2566                    word.as_str(),
2567                    "if" | "for" | "while" | "until" | "case" | "select" | "time" | "coproc"
2568                );
2569                let next_is_compound_start = matches!(
2570                    self.peek_next_kind(),
2571                    Some(TokenKind::LeftBrace | TokenKind::LeftParen)
2572                );
2573                if !is_compound_keyword && next_is_compound_start {
2574                    self.advance(); // consume the NAME
2575                    self.skip_newlines()?;
2576                    (Name::from(word), Some(word_span))
2577                } else {
2578                    (Name::new_static("COPROC"), None)
2579                }
2580            } else {
2581                (Name::new_static("COPROC"), None)
2582            }
2583        } else {
2584            (Name::new_static("COPROC"), None)
2585        };
2586
2587        // Parse the command body (could be simple, compound, or pipeline)
2588        let body = self.parse_pipeline()?;
2589        let body = body.ok_or_else(|| self.error("coproc: missing command"))?;
2590
2591        Ok(CompoundCommand::Coproc(CoprocCommand {
2592            name,
2593            name_span,
2594            body: Box::new(body),
2595            span: start_span.merge(self.current_span),
2596        }))
2597    }
2598
2599    /// Check if current token is ;; (case terminator)
2600    fn is_case_terminator(&self) -> bool {
2601        matches!(
2602            self.current_token_kind,
2603            Some(TokenKind::DoubleSemicolon | TokenKind::SemiAmp | TokenKind::DoubleSemiAmp)
2604        ) || (self.dialect == ShellDialect::Zsh
2605            && self.current_token_kind == Some(TokenKind::SemiPipe))
2606    }
2607
2608    /// Parse case terminator: `;;` (break), `;&` (fallthrough),
2609    /// `;;&` / `;|` (continue matching)
2610    fn parse_case_terminator(&mut self) -> (CaseTerminator, Option<Span>) {
2611        match self.current_token_kind {
2612            Some(TokenKind::SemiAmp) => {
2613                let span = self.current_span;
2614                self.advance();
2615                (CaseTerminator::FallThrough, Some(span))
2616            }
2617            Some(TokenKind::SemiPipe) => {
2618                let span = self.current_span;
2619                self.advance();
2620                (CaseTerminator::ContinueMatching, Some(span))
2621            }
2622            Some(TokenKind::DoubleSemiAmp) => {
2623                let span = self.current_span;
2624                self.advance();
2625                (CaseTerminator::Continue, Some(span))
2626            }
2627            Some(TokenKind::DoubleSemicolon) => {
2628                let span = self.current_span;
2629                self.advance();
2630                (CaseTerminator::Break, Some(span))
2631            }
2632            _ => (CaseTerminator::Break, None),
2633        }
2634    }
2635
2636    /// Parse a subshell (commands in parentheses)
2637    fn parse_subshell(&mut self) -> Result<CompoundCommand> {
2638        self.push_depth()?;
2639        self.advance(); // consume '('
2640        self.skip_newlines()?;
2641
2642        let body_start = self.current_span.start;
2643        let mut commands = Vec::new();
2644        while !matches!(
2645            self.current_token_kind,
2646            Some(TokenKind::RightParen | TokenKind::DoubleRightParen) | None
2647        ) {
2648            self.skip_newlines()?;
2649            if matches!(
2650                self.current_token_kind,
2651                Some(TokenKind::RightParen | TokenKind::DoubleRightParen)
2652            ) {
2653                break;
2654            }
2655            commands.extend(self.parse_command_list_required()?);
2656        }
2657
2658        if self.at(TokenKind::DoubleRightParen) {
2659            // `))` at end of nested subshells: consume as single `)`, leave `)` for parent
2660            self.set_current_kind(TokenKind::RightParen, self.current_span);
2661        } else if !self.at(TokenKind::RightParen) {
2662            self.pop_depth();
2663            return Err(Error::parse("expected ')' to close subshell".to_string()));
2664        } else {
2665            self.advance(); // consume ')'
2666        }
2667
2668        self.pop_depth();
2669        Ok(CompoundCommand::Subshell(Self::stmt_seq_with_span(
2670            Span::from_positions(body_start, self.current_span.start),
2671            commands,
2672        )))
2673    }
2674
2675    /// Parse a brace group
2676    fn parse_brace_group(&mut self, context: BraceBodyContext) -> Result<CompoundCommand> {
2677        self.push_depth()?;
2678        let (body, left_brace_span, right_brace_span) =
2679            self.parse_brace_enclosed_stmt_seq("syntax error: empty brace group", context)?;
2680
2681        let always_span = self.peek_zsh_always_span();
2682        if let Some(span) = always_span {
2683            self.record_zsh_always_span(span);
2684        }
2685
2686        let compound = if self.dialect.features().zsh_always && self.is_keyword(Keyword::Always) {
2687            self.record_zsh_always_span(self.current_span);
2688            self.advance();
2689            self.skip_newlines()?;
2690            if !self.at(TokenKind::LeftBrace) {
2691                self.pop_depth();
2692                return Err(self.error("expected '{' after always"));
2693            }
2694            let (always_body, _, always_right_brace_span) = self.parse_brace_enclosed_stmt_seq(
2695                "syntax error: empty always clause",
2696                BraceBodyContext::Ordinary,
2697            )?;
2698            CompoundCommand::Always(AlwaysCommand {
2699                body,
2700                always_body,
2701                span: left_brace_span.merge(always_right_brace_span),
2702            })
2703        } else {
2704            let _ = right_brace_span;
2705            CompoundCommand::BraceGroup(body)
2706        };
2707
2708        self.pop_depth();
2709        Ok(compound)
2710    }
2711
2712    fn parse_brace_enclosed_stmt_seq(
2713        &mut self,
2714        empty_error: &str,
2715        context: BraceBodyContext,
2716    ) -> Result<(StmtSeq, Span, Span)> {
2717        let left_brace_span = self.current_span;
2718        self.advance();
2719        self.brace_group_depth += 1;
2720        self.brace_body_stack.push(context);
2721        self.skip_command_separators()?;
2722
2723        let body_start = self.current_span.start;
2724        let mut commands = Vec::new();
2725        while !matches!(self.current_token_kind, Some(TokenKind::RightBrace) | None) {
2726            self.skip_command_separators()?;
2727            if self.at(TokenKind::RightBrace) {
2728                break;
2729            }
2730            commands.extend(self.parse_command_list_required()?);
2731        }
2732
2733        if !self.at(TokenKind::RightBrace) {
2734            self.brace_body_stack.pop();
2735            self.brace_group_depth -= 1;
2736            return Err(Error::parse(
2737                "expected '}' to close brace group".to_string(),
2738            ));
2739        }
2740
2741        if commands.is_empty()
2742            && !(self.dialect == ShellDialect::Zsh && matches!(context, BraceBodyContext::Function))
2743        {
2744            self.brace_body_stack.pop();
2745            self.brace_group_depth -= 1;
2746            return Err(self.error(empty_error));
2747        }
2748
2749        let right_brace_span = self.current_span;
2750        self.advance();
2751        self.brace_body_stack.pop();
2752        self.brace_group_depth -= 1;
2753        Ok((
2754            Self::stmt_seq_with_span(
2755                Span::from_positions(body_start, right_brace_span.start),
2756                commands,
2757            ),
2758            left_brace_span,
2759            right_brace_span,
2760        ))
2761    }
2762
2763    fn parse_if_condition_until_body_start(&mut self, allow_brace_body: bool) -> Result<Vec<Stmt>> {
2764        let mut stmts = Vec::with_capacity(2);
2765
2766        loop {
2767            self.skip_newlines()?;
2768
2769            if !allow_brace_body
2770                && !stmts.is_empty()
2771                && self.current_brace_starts_zsh_if_body_fact()
2772            {
2773                self.record_zsh_brace_if_span(self.current_span);
2774            }
2775
2776            if self.at(TokenKind::Semicolon) {
2777                let checkpoint = self.checkpoint();
2778                self.advance();
2779                if let Err(error) = self.skip_newlines() {
2780                    self.restore(checkpoint);
2781                    return Err(error);
2782                }
2783                let brace_if_span = (!allow_brace_body
2784                    && !stmts.is_empty()
2785                    && self.current_brace_starts_zsh_if_body_fact())
2786                .then_some(self.current_span);
2787                if self.is_keyword(Keyword::Then)
2788                    || (allow_brace_body && !stmts.is_empty() && self.at(TokenKind::LeftBrace))
2789                {
2790                    if let Some(span) = brace_if_span {
2791                        self.record_zsh_brace_if_span(span);
2792                    }
2793                    break;
2794                }
2795                self.restore(checkpoint);
2796                if let Some(span) = brace_if_span {
2797                    self.record_zsh_brace_if_span(span);
2798                }
2799            }
2800
2801            if self.is_keyword(Keyword::Then)
2802                || (allow_brace_body
2803                    && !stmts.is_empty()
2804                    && (self.at(TokenKind::LeftBrace)
2805                        || self.current_token_is_compact_zsh_brace_body()))
2806            {
2807                break;
2808            }
2809
2810            if self.current_token.is_none() {
2811                break;
2812            }
2813
2814            let command_stmts = self.parse_command_list_required()?;
2815            self.apply_stmt_list_effects(&command_stmts);
2816            stmts.extend(command_stmts);
2817        }
2818
2819        Ok(stmts)
2820    }
2821
2822    fn current_brace_starts_zsh_if_body_fact(&mut self) -> bool {
2823        if !self.at(TokenKind::LeftBrace) {
2824            return false;
2825        }
2826
2827        let checkpoint = self.checkpoint();
2828        let reaches_then = loop {
2829            if self.skip_newlines().is_err() {
2830                break false;
2831            }
2832            if self.is_keyword(Keyword::Then) {
2833                break true;
2834            }
2835            if self.current_token.is_none() {
2836                break false;
2837            }
2838            if self.parse_command_list_required().is_err() {
2839                break false;
2840            }
2841        };
2842        self.restore(checkpoint);
2843
2844        !reaches_then
2845    }
2846
2847    fn parse_loop_condition_until_body_start(
2848        &mut self,
2849        allow_brace_body: bool,
2850    ) -> Result<Vec<Stmt>> {
2851        let mut stmts = Vec::with_capacity(2);
2852
2853        loop {
2854            self.skip_newlines()?;
2855
2856            if self.is_keyword(Keyword::Do)
2857                || (allow_brace_body
2858                    && !stmts.is_empty()
2859                    && (self.at(TokenKind::LeftBrace)
2860                        || self.current_token_is_compact_zsh_brace_body())
2861                    && self.current_brace_starts_zsh_loop_body_fact())
2862            {
2863                break;
2864            }
2865
2866            if self.current_token.is_none() {
2867                break;
2868            }
2869
2870            let command_stmts = self.parse_command_list_required()?;
2871            self.apply_stmt_list_effects(&command_stmts);
2872            stmts.extend(command_stmts);
2873        }
2874
2875        Ok(stmts)
2876    }
2877
2878    fn current_brace_starts_zsh_loop_body_fact(&mut self) -> bool {
2879        let checkpoint = self.checkpoint();
2880        let reaches_do = loop {
2881            if self.skip_newlines().is_err() {
2882                break false;
2883            }
2884            if self.is_keyword(Keyword::Do) {
2885                break true;
2886            }
2887            if self.current_token.is_none() {
2888                break false;
2889            }
2890            if self.parse_command_list_required().is_err() {
2891                break false;
2892            }
2893        };
2894        self.restore(checkpoint);
2895
2896        !reaches_do
2897    }
2898
2899    fn current_token_is_compact_zsh_brace_body(&mut self) -> bool {
2900        self.current_source_like_word_text()
2901            .is_some_and(|text| text.starts_with('{') && text.ends_with('}'))
2902    }
2903
2904    fn peek_zsh_always_span(&mut self) -> Option<Span> {
2905        if !self.is_keyword(Keyword::Always) {
2906            return None;
2907        }
2908
2909        let always_span = self.current_span;
2910        let checkpoint = self.checkpoint();
2911        self.advance();
2912        let result = match self.skip_newlines() {
2913            Ok(()) if self.at(TokenKind::LeftBrace) => Some(always_span),
2914            Ok(()) => None,
2915            Err(_) => None,
2916        };
2917        self.restore(checkpoint);
2918        result
2919    }
2920
2921    fn has_recorded_comment_between(&self, start_offset: usize, end_offset: usize) -> bool {
2922        self.comments.iter().any(|comment| {
2923            let comment_start = usize::from(comment.range.start());
2924            comment_start >= start_offset && comment_start < end_offset
2925        })
2926    }
2927
2928    fn rebase_nested_parse_error(&self, error: Error, base: Position) -> Error {
2929        let Error::Parse {
2930            message,
2931            line,
2932            column,
2933        } = error;
2934
2935        if line == 0 {
2936            return Error::parse(message);
2937        }
2938
2939        let rebased_line = base.line + line.saturating_sub(1);
2940        let rebased_column = if line == 1 {
2941            base.column + column.saturating_sub(1)
2942        } else {
2943            column
2944        };
2945
2946        Error::parse_at(message, rebased_line, rebased_column)
2947    }
2948
2949    fn try_parse_compact_function_brace_body(&mut self) -> Result<Option<CompoundCommand>> {
2950        if self.dialect != ShellDialect::Zsh
2951            || !self.zsh_short_loops_enabled()
2952            || !self.at_word_like()
2953        {
2954            return Ok(None);
2955        }
2956
2957        let Some(body_text) = self.current_source_like_word_text() else {
2958            return Ok(None);
2959        };
2960        let body_text = body_text.into_owned();
2961        let Some(inner) = body_text
2962            .strip_prefix('{')
2963            .and_then(|body| body.strip_suffix('}'))
2964        else {
2965            return Ok(None);
2966        };
2967
2968        if !inner.is_empty()
2969            && !inner.chars().any(|ch| {
2970                matches!(
2971                    ch,
2972                    ' ' | '\t' | '\n' | ';' | '&' | '|' | '<' | '>' | '$' | '"' | '\'' | '(' | ')'
2973                )
2974            })
2975        {
2976            return Ok(None);
2977        }
2978
2979        let nested_profile = self
2980            .current_zsh_options()
2981            .cloned()
2982            .map(|options| ShellProfile::with_zsh_options(self.dialect, options))
2983            .unwrap_or_else(|| self.shell_profile.clone());
2984        let mut nested =
2985            Parser::with_limits_and_profile(inner, self.max_depth, self.max_fuel, nested_profile);
2986        nested.aliases = self.aliases.clone();
2987        nested.expand_aliases = self.expand_aliases;
2988        nested.expand_next_word = self.expand_next_word;
2989
2990        let inner_start = self.current_span.start.advanced_by("{");
2991        let mut output = nested.parse();
2992        if output.is_err() {
2993            return Err(self.rebase_nested_parse_error(output.strict_error(), inner_start));
2994        }
2995        Self::rebase_stmt_seq(&mut output.file.body, inner_start);
2996        self.advance();
2997        Ok(Some(CompoundCommand::BraceGroup(output.file.body)))
2998    }
2999
3000    fn try_parse_compact_zsh_brace_body(
3001        &mut self,
3002        context: BraceBodyContext,
3003    ) -> Result<Option<(StmtSeq, Span, Span)>> {
3004        if self.dialect != ShellDialect::Zsh
3005            || !self.zsh_brace_bodies_enabled()
3006            || !self.at_word_like()
3007        {
3008            return Ok(None);
3009        }
3010
3011        let Some(body_text) = self.current_source_like_word_text() else {
3012            return Ok(None);
3013        };
3014        let body_text = body_text.into_owned();
3015        let Some(inner) = body_text
3016            .strip_prefix('{')
3017            .and_then(|body| body.strip_suffix('}'))
3018        else {
3019            return Ok(None);
3020        };
3021
3022        if !inner.is_empty()
3023            && !inner.chars().any(|ch| {
3024                matches!(
3025                    ch,
3026                    ' ' | '\t' | '\n' | ';' | '&' | '|' | '<' | '>' | '$' | '"' | '\'' | '(' | ')'
3027                )
3028            })
3029        {
3030            return Ok(None);
3031        }
3032
3033        let nested_profile = self
3034            .current_zsh_options()
3035            .cloned()
3036            .map(|options| ShellProfile::with_zsh_options(self.dialect, options))
3037            .unwrap_or_else(|| self.shell_profile.clone());
3038        let mut nested =
3039            Parser::with_limits_and_profile(inner, self.max_depth, self.max_fuel, nested_profile);
3040        nested.aliases = self.aliases.clone();
3041        nested.expand_aliases = self.expand_aliases;
3042        nested.expand_next_word = self.expand_next_word;
3043
3044        let word_span = self.current_span;
3045        let inner_start = word_span.start.advanced_by("{");
3046        let mut output = nested.parse();
3047        if output.is_err() {
3048            return Err(self.rebase_nested_parse_error(output.strict_error(), inner_start));
3049        }
3050        Self::rebase_stmt_seq(&mut output.file.body, inner_start);
3051
3052        let left_brace_span = Span::from_positions(word_span.start, inner_start);
3053        let right_brace_start = word_span
3054            .start
3055            .advanced_by(&body_text[..body_text.len() - 1]);
3056        let right_brace_span = Span::from_positions(right_brace_start, word_span.end);
3057        let body = Self::stmt_seq_with_span(
3058            Span::from_positions(inner_start, right_brace_start),
3059            output.file.body.stmts,
3060        );
3061
3062        if body.is_empty()
3063            && !(self.dialect == ShellDialect::Zsh && matches!(context, BraceBodyContext::Function))
3064        {
3065            let message = match context {
3066                BraceBodyContext::Function => "syntax error: empty brace group",
3067                BraceBodyContext::IfClause => "syntax error: empty then clause",
3068                BraceBodyContext::Ordinary => "syntax error: empty brace group",
3069            };
3070            return Err(self.error(message));
3071        }
3072
3073        self.advance();
3074        Ok(Some((body, left_brace_span, right_brace_span)))
3075    }
3076
3077    fn should_consume_right_brace_as_literal_argument(
3078        &mut self,
3079        next_kind_after_right_brace: Option<TokenKind>,
3080    ) -> bool {
3081        if !self.current_token_has_leading_whitespace()
3082            || next_kind_after_right_brace.is_some_and(Self::is_redirect_kind)
3083        {
3084            return false;
3085        }
3086
3087        if self.brace_group_depth == 0 {
3088            return true;
3089        }
3090
3091        if self.dialect != ShellDialect::Zsh {
3092            return true;
3093        }
3094
3095        next_kind_after_right_brace == Some(TokenKind::Semicolon)
3096            && self.current_token_is_tight_to_next_token()
3097            && self.next_token_after_tight_semicolon_is(TokenKind::RightBrace)
3098    }
3099
3100    fn next_token_after_tight_semicolon_is(&mut self, expected: TokenKind) -> bool {
3101        let checkpoint = self.checkpoint();
3102        self.advance();
3103        if !self.at(TokenKind::Semicolon) {
3104            self.restore(checkpoint);
3105            return false;
3106        }
3107        self.advance();
3108        let result = self.at(expected);
3109        self.restore(checkpoint);
3110        result
3111    }
3112
3113    /// Parse arithmetic command ((expression))
3114    /// Parse [[ conditional expression ]]
3115    fn parse_conditional(&mut self) -> Result<CompoundCommand> {
3116        self.ensure_double_bracket()?;
3117        let left_bracket_span = self.current_span;
3118        self.advance(); // consume '[['
3119        self.skip_conditional_newlines();
3120
3121        let expression = self.parse_conditional_or(false)?;
3122        self.skip_conditional_newlines();
3123
3124        let right_bracket_span = match self.current_token_kind {
3125            Some(TokenKind::DoubleRightBracket) => {
3126                let span = self.current_span;
3127                self.advance(); // consume ']]'
3128                span
3129            }
3130            None => {
3131                return Err(crate::error::Error::parse(
3132                    "unexpected end of input in [[ ]]".to_string(),
3133                ));
3134            }
3135            _ => return Err(self.error("expected ']]' to close conditional expression")),
3136        };
3137
3138        Ok(CompoundCommand::Conditional(ConditionalCommand {
3139            expression,
3140            span: left_bracket_span.merge(right_bracket_span),
3141            left_bracket_span,
3142            right_bracket_span,
3143        }))
3144    }
3145
3146    fn skip_conditional_newlines(&mut self) {
3147        while self.at(TokenKind::Newline) {
3148            self.advance();
3149        }
3150    }
3151
3152    fn parse_conditional_or(&mut self, stop_at_right_paren: bool) -> Result<ConditionalExpr> {
3153        let mut expr = self.parse_conditional_and(stop_at_right_paren)?;
3154
3155        loop {
3156            self.skip_conditional_newlines();
3157            if !self.at(TokenKind::Or) {
3158                break;
3159            }
3160
3161            let op_span = self.current_span;
3162            self.advance();
3163            let right = self.parse_conditional_and(stop_at_right_paren)?;
3164            expr = ConditionalExpr::Binary(ConditionalBinaryExpr {
3165                left: Box::new(expr),
3166                op: ConditionalBinaryOp::Or,
3167                op_span,
3168                right: Box::new(right),
3169            });
3170        }
3171
3172        Ok(expr)
3173    }
3174
3175    fn parse_conditional_and(&mut self, stop_at_right_paren: bool) -> Result<ConditionalExpr> {
3176        let mut expr = self.parse_conditional_term(stop_at_right_paren)?;
3177
3178        loop {
3179            self.skip_conditional_newlines();
3180            if !self.at(TokenKind::And) {
3181                break;
3182            }
3183
3184            let op_span = self.current_span;
3185            self.advance();
3186            let right = self.parse_conditional_term(stop_at_right_paren)?;
3187            expr = ConditionalExpr::Binary(ConditionalBinaryExpr {
3188                left: Box::new(expr),
3189                op: ConditionalBinaryOp::And,
3190                op_span,
3191                right: Box::new(right),
3192            });
3193        }
3194
3195        Ok(expr)
3196    }
3197
3198    fn parse_conditional_term(&mut self, stop_at_right_paren: bool) -> Result<ConditionalExpr> {
3199        self.skip_conditional_newlines();
3200
3201        if let Some(op) = self.current_conditional_unary_op() {
3202            let op_span = self.current_span;
3203            self.advance();
3204            self.skip_conditional_newlines();
3205
3206            let expr = if matches!(op, ConditionalUnaryOp::Not) {
3207                self.parse_conditional_term(stop_at_right_paren)?
3208            } else {
3209                if matches!(
3210                    op,
3211                    ConditionalUnaryOp::VariableSet | ConditionalUnaryOp::ReferenceVariable
3212                ) {
3213                    let word = self.collect_conditional_context_word(stop_at_right_paren)?;
3214                    self.conditional_var_ref_expr(word)
3215                } else {
3216                    let word = self.parse_conditional_operand_word()?;
3217                    ConditionalExpr::Word(word)
3218                }
3219            };
3220
3221            return Ok(ConditionalExpr::Unary(ConditionalUnaryExpr {
3222                op,
3223                op_span,
3224                expr: Box::new(expr),
3225            }));
3226        }
3227
3228        if self.at(TokenKind::DoubleLeftParen) {
3229            if self.dialect == ShellDialect::Zsh {
3230                let left_paren_span = self.current_span;
3231                if let Some(right_paren_span) = self.scan_arithmetic_command_close(left_paren_span)
3232                {
3233                    let span = left_paren_span.merge(right_paren_span);
3234                    let text = span.slice(self.input).to_string();
3235                    while self.current_token.is_some()
3236                        && self.current_span.start.offset < right_paren_span.end.offset
3237                    {
3238                        self.advance();
3239                    }
3240                    return Ok(ConditionalExpr::Word(
3241                        self.parse_word_with_context(&text, span, span.start, true),
3242                    ));
3243                }
3244            }
3245            self.split_current_double_left_paren();
3246        }
3247
3248        let left = if self.at(TokenKind::LeftParen) {
3249            let left_paren_span = self.current_span;
3250            self.advance();
3251            let expr = self.parse_conditional_or(true)?;
3252            self.skip_conditional_newlines();
3253            if self.at(TokenKind::DoubleRightParen) {
3254                self.split_current_double_right_paren();
3255            }
3256            if !self.at(TokenKind::RightParen) {
3257                return Err(self.error("expected ')' in conditional expression"));
3258            }
3259            let right_paren_span = self.current_span;
3260            self.advance();
3261            ConditionalExpr::Parenthesized(ConditionalParenExpr {
3262                left_paren_span,
3263                expr: Box::new(expr),
3264                right_paren_span,
3265            })
3266        } else {
3267            ConditionalExpr::Word(self.parse_conditional_operand_word()?)
3268        };
3269
3270        self.skip_conditional_newlines();
3271
3272        let Some(op) = self.current_conditional_comparison_op() else {
3273            return Ok(left);
3274        };
3275
3276        let op_span = self.current_span;
3277        self.advance();
3278        self.skip_conditional_newlines();
3279
3280        let right = match op {
3281            ConditionalBinaryOp::RegexMatch => {
3282                if self.at(TokenKind::LeftBrace) {
3283                    return Err(self.error("expected conditional operand"));
3284                }
3285                ConditionalExpr::Regex(self.collect_conditional_context_word(stop_at_right_paren)?)
3286            }
3287            ConditionalBinaryOp::PatternEqShort
3288            | ConditionalBinaryOp::PatternEq
3289            | ConditionalBinaryOp::PatternNe => {
3290                let word = self.collect_conditional_context_word(stop_at_right_paren)?;
3291                ConditionalExpr::Pattern(self.pattern_from_conditional_word(&word))
3292            }
3293            _ => ConditionalExpr::Word(self.parse_conditional_operand_word()?),
3294        };
3295
3296        Ok(ConditionalExpr::Binary(ConditionalBinaryExpr {
3297            left: Box::new(left),
3298            op,
3299            op_span,
3300            right: Box::new(right),
3301        }))
3302    }
3303
3304    fn parse_conditional_operand_word(&mut self) -> Result<Word> {
3305        self.skip_conditional_newlines();
3306
3307        if let Some(word) = self.current_conditional_source_word(false) {
3308            self.advance_past_word(&word);
3309            self.restore_conditional_source_delimiter(word.span.end, false);
3310            return Ok(word);
3311        }
3312
3313        if let Some(word) = self.take_current_word_and_advance() {
3314            return Ok(word);
3315        }
3316
3317        let Some(word) = self.current_conditional_literal_word() else {
3318            return Err(self.error("expected conditional operand"));
3319        };
3320        self.advance_past_word(&word);
3321        Ok(word)
3322    }
3323
3324    fn conditional_var_ref_expr(&self, word: Word) -> ConditionalExpr {
3325        self.parse_var_ref_from_word(&word, SubscriptInterpretation::Contextual)
3326            .map(Box::new)
3327            .map(ConditionalExpr::VarRef)
3328            .unwrap_or(ConditionalExpr::Word(word))
3329    }
3330
3331    fn current_conditional_source_word(&mut self, stop_at_right_paren: bool) -> Option<Word> {
3332        let token = self.current_token.as_ref()?;
3333        if token.flags.is_synthetic() {
3334            return None;
3335        }
3336
3337        if matches!(
3338            self.current_token_kind,
3339            Some(TokenKind::QuotedWord | TokenKind::LiteralWord)
3340        ) {
3341            return None;
3342        }
3343
3344        let starts_with_paren = matches!(self.current_token_kind, Some(TokenKind::LeftParen));
3345        let starts_with_zsh_pattern_punct = matches!(
3346            self.current_token_kind,
3347            Some(TokenKind::RedirectIn | TokenKind::RedirectOut | TokenKind::RedirectReadWrite)
3348        ) && self.dialect == ShellDialect::Zsh;
3349
3350        if !starts_with_paren
3351            && !starts_with_zsh_pattern_punct
3352            && !self.current_token_kind.is_some_and(TokenKind::is_word_like)
3353        {
3354            return None;
3355        }
3356
3357        let start = self.current_span.start;
3358        let (text, end) =
3359            self.scan_conditional_source_word(start, stop_at_right_paren, starts_with_paren)?;
3360        let span = Span::from_positions(start, end);
3361        Some(self.parse_word_with_context(&text, span, start, true))
3362    }
3363
3364    fn scan_conditional_source_word(
3365        &self,
3366        start: Position,
3367        stop_at_right_paren: bool,
3368        starts_with_paren: bool,
3369    ) -> Option<(String, Position)> {
3370        if start.offset >= self.input.len() {
3371            return None;
3372        }
3373
3374        let mut cursor = start;
3375        let mut text = String::new();
3376        let mut paren_depth = 0_i32;
3377        let mut brace_depth = 0_i32;
3378        let mut bracket_depth = 0_i32;
3379        let mut in_single = false;
3380        let mut in_double = false;
3381        let mut in_backtick = false;
3382        let mut escaped = false;
3383        let mut prev_char = None;
3384
3385        while cursor.offset < self.input.len() {
3386            let rest = &self.input[cursor.offset..];
3387            if !in_single
3388                && !in_double
3389                && !in_backtick
3390                && !escaped
3391                && paren_depth == 0
3392                && brace_depth == 0
3393                && bracket_depth == 0
3394            {
3395                if rest.starts_with("]]")
3396                    || rest.starts_with("&&")
3397                    || rest.starts_with("||")
3398                    || (!starts_with_paren && rest.starts_with(')'))
3399                    || (stop_at_right_paren && rest.starts_with(')'))
3400                {
3401                    break;
3402                }
3403
3404                let ch = rest.chars().next()?;
3405                if matches!(ch, ' ' | '\t' | '\n' | ';') {
3406                    break;
3407                }
3408            }
3409
3410            let ch = self.input[cursor.offset..].chars().next()?;
3411            cursor.advance(ch);
3412            text.push(ch);
3413
3414            if escaped {
3415                escaped = false;
3416                prev_char = Some(ch);
3417                continue;
3418            }
3419
3420            match ch {
3421                '\\' if !in_single => escaped = true,
3422                '\'' if !in_double => in_single = !in_single,
3423                '"' if !in_single => in_double = !in_double,
3424                '`' if !in_single => in_backtick = !in_backtick,
3425                '(' if !in_single && !in_double && brace_depth == 0 => paren_depth += 1,
3426                ')' if !in_single && !in_double && brace_depth == 0 && paren_depth > 0 => {
3427                    paren_depth -= 1
3428                }
3429                '{' if !in_single && !in_double && (brace_depth > 0 || prev_char == Some('$')) => {
3430                    brace_depth += 1
3431                }
3432                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
3433                '[' if !in_single && !in_double => bracket_depth += 1,
3434                ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
3435                _ => {}
3436            }
3437
3438            prev_char = Some(ch);
3439        }
3440
3441        (!text.is_empty()).then_some((text, cursor))
3442    }
3443
3444    fn conditional_source_delimiter_after(
3445        &self,
3446        end: Position,
3447        stop_at_right_paren: bool,
3448    ) -> Option<(TokenKind, Span)> {
3449        let mut cursor = end;
3450        while cursor.offset < self.input.len() {
3451            let rest = &self.input[cursor.offset..];
3452            let ch = rest.chars().next()?;
3453            if matches!(ch, ' ' | '\t') {
3454                cursor.advance(ch);
3455                continue;
3456            }
3457            break;
3458        }
3459
3460        let rest = self.input.get(cursor.offset..)?;
3461        let (kind, text) = if rest.starts_with("]]") {
3462            (TokenKind::DoubleRightBracket, "]]")
3463        } else if rest.starts_with("&&") {
3464            (TokenKind::And, "&&")
3465        } else if rest.starts_with("||") {
3466            (TokenKind::Or, "||")
3467        } else if stop_at_right_paren && rest.starts_with(')') {
3468            (TokenKind::RightParen, ")")
3469        } else {
3470            return None;
3471        };
3472
3473        Some((kind, Span::from_positions(cursor, cursor.advanced_by(text))))
3474    }
3475
3476    fn restore_conditional_source_delimiter(&mut self, end: Position, stop_at_right_paren: bool) {
3477        let Some((kind, span)) = self.conditional_source_delimiter_after(end, stop_at_right_paren)
3478        else {
3479            return;
3480        };
3481
3482        if self.current_token_kind == Some(kind) && self.current_span == span {
3483            return;
3484        }
3485
3486        if let Some(current_kind) = self.current_token_kind
3487            && matches!(
3488                current_kind,
3489                TokenKind::Newline
3490                    | TokenKind::Semicolon
3491                    | TokenKind::And
3492                    | TokenKind::Or
3493                    | TokenKind::RightParen
3494                    | TokenKind::DoubleRightBracket
3495            )
3496        {
3497            self.synthetic_tokens
3498                .push_front(SyntheticToken::punctuation(current_kind, self.current_span));
3499        }
3500
3501        self.set_current_kind(kind, span);
3502    }
3503
3504    fn current_conditional_unary_op(&self) -> Option<ConditionalUnaryOp> {
3505        if !self.at(TokenKind::Word) {
3506            return None;
3507        }
3508        let word = self.current_word_str()?;
3509
3510        Some(match word {
3511            "!" => ConditionalUnaryOp::Not,
3512            "-e" | "-a" => ConditionalUnaryOp::Exists,
3513            "-f" => ConditionalUnaryOp::RegularFile,
3514            "-d" => ConditionalUnaryOp::Directory,
3515            "-c" => ConditionalUnaryOp::CharacterSpecial,
3516            "-b" => ConditionalUnaryOp::BlockSpecial,
3517            "-p" => ConditionalUnaryOp::NamedPipe,
3518            "-S" => ConditionalUnaryOp::Socket,
3519            "-L" | "-h" => ConditionalUnaryOp::Symlink,
3520            "-k" => ConditionalUnaryOp::Sticky,
3521            "-g" => ConditionalUnaryOp::SetGroupId,
3522            "-u" => ConditionalUnaryOp::SetUserId,
3523            "-G" => ConditionalUnaryOp::GroupOwned,
3524            "-O" => ConditionalUnaryOp::UserOwned,
3525            "-N" => ConditionalUnaryOp::Modified,
3526            "-r" => ConditionalUnaryOp::Readable,
3527            "-w" => ConditionalUnaryOp::Writable,
3528            "-x" => ConditionalUnaryOp::Executable,
3529            "-s" => ConditionalUnaryOp::NonEmptyFile,
3530            "-t" => ConditionalUnaryOp::FdTerminal,
3531            "-z" => ConditionalUnaryOp::EmptyString,
3532            "-n" => ConditionalUnaryOp::NonEmptyString,
3533            "-o" => ConditionalUnaryOp::OptionSet,
3534            "-v" => ConditionalUnaryOp::VariableSet,
3535            "-R" => ConditionalUnaryOp::ReferenceVariable,
3536            _ => return None,
3537        })
3538    }
3539
3540    fn current_conditional_comparison_op(&self) -> Option<ConditionalBinaryOp> {
3541        match self.current_token_kind? {
3542            TokenKind::Word => Some(match self.current_word_str()? {
3543                "=" => ConditionalBinaryOp::PatternEqShort,
3544                "==" => ConditionalBinaryOp::PatternEq,
3545                "!=" => ConditionalBinaryOp::PatternNe,
3546                "=~" => ConditionalBinaryOp::RegexMatch,
3547                "-nt" => ConditionalBinaryOp::NewerThan,
3548                "-ot" => ConditionalBinaryOp::OlderThan,
3549                "-ef" => ConditionalBinaryOp::SameFile,
3550                "-eq" => ConditionalBinaryOp::ArithmeticEq,
3551                "-ne" => ConditionalBinaryOp::ArithmeticNe,
3552                "-le" => ConditionalBinaryOp::ArithmeticLe,
3553                "-ge" => ConditionalBinaryOp::ArithmeticGe,
3554                "-lt" => ConditionalBinaryOp::ArithmeticLt,
3555                "-gt" => ConditionalBinaryOp::ArithmeticGt,
3556                _ => return None,
3557            }),
3558            TokenKind::RedirectIn => Some(ConditionalBinaryOp::LexicalBefore),
3559            TokenKind::RedirectOut => Some(ConditionalBinaryOp::LexicalAfter),
3560            _ => None,
3561        }
3562    }
3563
3564    fn collect_conditional_context_word(&mut self, stop_at_right_paren: bool) -> Result<Word> {
3565        self.skip_conditional_newlines();
3566
3567        if let Some(word) = self.current_conditional_source_word(stop_at_right_paren) {
3568            self.advance_past_word(&word);
3569            self.restore_conditional_source_delimiter(word.span.end, stop_at_right_paren);
3570            return Ok(word);
3571        }
3572
3573        let mut first_word: Option<Word> = None;
3574        let mut parts = Vec::new();
3575        let mut start = None;
3576        let mut end = None;
3577        let mut previous_end: Option<Position> = None;
3578        let mut composite = false;
3579        let mut paren_depth = 0usize;
3580
3581        loop {
3582            self.skip_conditional_newlines();
3583
3584            match self.current_token_kind {
3585                Some(TokenKind::DoubleRightBracket) => break,
3586                Some(TokenKind::And) | Some(TokenKind::Or) if paren_depth == 0 => break,
3587                Some(TokenKind::RightParen) if stop_at_right_paren && paren_depth == 0 => break,
3588                None => break,
3589                _ => {}
3590            }
3591
3592            if let Some(prev_end) = previous_end
3593                && prev_end.offset < self.current_span.start.offset
3594            {
3595                let gap_span = Span::from_positions(prev_end, self.current_span.start);
3596                let gap_text = gap_span.slice(self.input);
3597                if let Some(word) = first_word.take() {
3598                    parts.extend(word.parts);
3599                }
3600                if Self::source_text_needs_quote_preserving_decode(gap_text) {
3601                    let gap_word = self.decode_word_text_preserving_quotes_if_needed(
3602                        gap_text,
3603                        gap_span,
3604                        gap_span.start,
3605                        true,
3606                    );
3607                    parts.extend(gap_word.parts);
3608                } else {
3609                    parts.push(WordPartNode::new(
3610                        WordPart::Literal(LiteralText::source()),
3611                        gap_span,
3612                    ));
3613                }
3614                composite = true;
3615            }
3616
3617            match self.current_token_kind {
3618                Some(TokenKind::Word | TokenKind::LiteralWord | TokenKind::QuotedWord) => {
3619                    let word = self
3620                        .take_current_word()
3621                        .ok_or_else(|| self.error("expected conditional operand"))?;
3622                    if start.is_none() {
3623                        start = Some(word.span.start);
3624                    } else {
3625                        if let Some(first) = first_word.take() {
3626                            parts.extend(first.parts);
3627                        }
3628                        composite = true;
3629                    }
3630                    end = Some(word.span.end);
3631                    if first_word.is_none() && !composite {
3632                        first_word = Some(word);
3633                    } else {
3634                        parts.extend(word.parts);
3635                    }
3636                    previous_end = Some(self.current_span.end);
3637                    self.advance();
3638                }
3639                Some(TokenKind::LeftParen) => {
3640                    if start.is_none() {
3641                        start = Some(self.current_span.start);
3642                    }
3643                    end = Some(self.current_span.end);
3644                    if let Some(word) = first_word.take() {
3645                        parts.extend(word.parts);
3646                    }
3647                    parts.push(WordPartNode::new(
3648                        WordPart::Literal(LiteralText::owned("(")),
3649                        self.current_span,
3650                    ));
3651                    previous_end = Some(self.current_span.end);
3652                    paren_depth += 1;
3653                    composite = true;
3654                    self.advance();
3655                }
3656                Some(TokenKind::DoubleLeftParen) => {
3657                    if start.is_none() {
3658                        start = Some(self.current_span.start);
3659                    }
3660                    end = Some(self.current_span.end);
3661                    if let Some(word) = first_word.take() {
3662                        parts.extend(word.parts);
3663                    }
3664                    parts.push(WordPartNode::new(
3665                        WordPart::Literal(LiteralText::owned("((")),
3666                        self.current_span,
3667                    ));
3668                    previous_end = Some(self.current_span.end);
3669                    paren_depth += 2;
3670                    composite = true;
3671                    self.advance();
3672                }
3673                Some(TokenKind::RightParen) => {
3674                    if paren_depth == 0 {
3675                        break;
3676                    }
3677                    if start.is_none() {
3678                        start = Some(self.current_span.start);
3679                    }
3680                    end = Some(self.current_span.end);
3681                    if let Some(word) = first_word.take() {
3682                        parts.extend(word.parts);
3683                    }
3684                    parts.push(WordPartNode::new(
3685                        WordPart::Literal(LiteralText::owned(")")),
3686                        self.current_span,
3687                    ));
3688                    previous_end = Some(self.current_span.end);
3689                    paren_depth = paren_depth.saturating_sub(1);
3690                    composite = true;
3691                    self.advance();
3692                }
3693                Some(TokenKind::DoubleRightParen) => {
3694                    if start.is_none() {
3695                        start = Some(self.current_span.start);
3696                    }
3697                    end = Some(self.current_span.end);
3698                    if let Some(word) = first_word.take() {
3699                        parts.extend(word.parts);
3700                    }
3701                    parts.push(WordPartNode::new(
3702                        WordPart::Literal(LiteralText::owned("))")),
3703                        self.current_span,
3704                    ));
3705                    previous_end = Some(self.current_span.end);
3706                    paren_depth = paren_depth.saturating_sub(2);
3707                    composite = true;
3708                    self.advance();
3709                }
3710                Some(TokenKind::Pipe) => {
3711                    if start.is_none() {
3712                        start = Some(self.current_span.start);
3713                    }
3714                    end = Some(self.current_span.end);
3715                    if let Some(word) = first_word.take() {
3716                        parts.extend(word.parts);
3717                    }
3718                    parts.push(WordPartNode::new(
3719                        WordPart::Literal(LiteralText::owned("|")),
3720                        self.current_span,
3721                    ));
3722                    previous_end = Some(self.current_span.end);
3723                    composite = true;
3724                    self.advance();
3725                }
3726                Some(TokenKind::And) => {
3727                    if paren_depth == 0 {
3728                        break;
3729                    }
3730                    if start.is_none() {
3731                        start = Some(self.current_span.start);
3732                    }
3733                    end = Some(self.current_span.end);
3734                    if let Some(word) = first_word.take() {
3735                        parts.extend(word.parts);
3736                    }
3737                    parts.push(WordPartNode::new(
3738                        WordPart::Literal(LiteralText::owned("&&")),
3739                        self.current_span,
3740                    ));
3741                    previous_end = Some(self.current_span.end);
3742                    composite = true;
3743                    self.advance();
3744                }
3745                Some(TokenKind::Or) => {
3746                    if paren_depth == 0 {
3747                        break;
3748                    }
3749                    if start.is_none() {
3750                        start = Some(self.current_span.start);
3751                    }
3752                    end = Some(self.current_span.end);
3753                    if let Some(word) = first_word.take() {
3754                        parts.extend(word.parts);
3755                    }
3756                    parts.push(WordPartNode::new(
3757                        WordPart::Literal(LiteralText::owned("||")),
3758                        self.current_span,
3759                    ));
3760                    previous_end = Some(self.current_span.end);
3761                    composite = true;
3762                    self.advance();
3763                }
3764                Some(TokenKind::RedirectIn)
3765                | Some(TokenKind::RedirectOut)
3766                | Some(TokenKind::RedirectReadWrite) => {
3767                    let literal = self.input
3768                        [self.current_span.start.offset..self.current_span.end.offset]
3769                        .to_string();
3770                    if start.is_none() {
3771                        start = Some(self.current_span.start);
3772                    }
3773                    end = Some(self.current_span.end);
3774                    if let Some(word) = first_word.take() {
3775                        parts.extend(word.parts);
3776                    }
3777                    parts.push(WordPartNode::new(
3778                        WordPart::Literal(self.literal_text(
3779                            literal,
3780                            self.current_span.start,
3781                            self.current_span.end,
3782                            true,
3783                        )),
3784                        self.current_span,
3785                    ));
3786                    previous_end = Some(self.current_span.end);
3787                    composite = true;
3788                    self.advance();
3789                }
3790                _ => {
3791                    let literal = self.input
3792                        [self.current_span.start.offset..self.current_span.end.offset]
3793                        .to_string();
3794                    if literal.is_empty() {
3795                        break;
3796                    }
3797                    if start.is_none() {
3798                        start = Some(self.current_span.start);
3799                    }
3800                    end = Some(self.current_span.end);
3801                    if let Some(word) = first_word.take() {
3802                        parts.extend(word.parts);
3803                    }
3804                    parts.push(WordPartNode::new(
3805                        WordPart::Literal(self.literal_text(
3806                            literal,
3807                            self.current_span.start,
3808                            self.current_span.end,
3809                            true,
3810                        )),
3811                        self.current_span,
3812                    ));
3813                    previous_end = Some(self.current_span.end);
3814                    composite = true;
3815                    self.advance();
3816                }
3817            }
3818        }
3819
3820        if !composite && let Some(word) = first_word {
3821            return Ok(word);
3822        }
3823
3824        let (start, end) = match (start, end) {
3825            (Some(start), Some(end)) => (start, end),
3826            _ => return Err(self.error("expected conditional operand")),
3827        };
3828
3829        Ok(self.word_with_parts(parts, Span::from_positions(start, end)))
3830    }
3831
3832    fn parse_arithmetic_command(&mut self) -> Result<CompoundCommand> {
3833        self.ensure_arithmetic_command()?;
3834        let left_paren_span = self.current_span;
3835        let Some(right_paren_span) = self.scan_arithmetic_command_close(left_paren_span) else {
3836            return Err(Error::parse(
3837                "unexpected end of input in arithmetic command".to_string(),
3838            ));
3839        };
3840        while self.current_token.is_some()
3841            && self.current_span.start.offset < right_paren_span.end.offset
3842        {
3843            self.advance();
3844        }
3845
3846        let expr_span = Self::optional_span(left_paren_span.end, right_paren_span.start);
3847        let expr_ast =
3848            self.parse_explicit_arithmetic_span(expr_span, "invalid arithmetic command")?;
3849        Ok(CompoundCommand::Arithmetic(ArithmeticCommand {
3850            span: left_paren_span.merge(right_paren_span),
3851            left_paren_span,
3852            expr_span,
3853            expr_ast,
3854            right_paren_span,
3855        }))
3856    }
3857
3858    fn scan_arithmetic_command_close(&self, left_paren_span: Span) -> Option<Span> {
3859        let mut cursor = left_paren_span.end;
3860        let mut depth = 0_i32;
3861        let mut in_single = false;
3862        let mut in_double = false;
3863        let mut in_backtick = false;
3864        let mut escaped = false;
3865
3866        while cursor.offset < self.input.len() {
3867            let rest = &self.input[cursor.offset..];
3868
3869            if !in_single && !in_double && !in_backtick {
3870                if rest.starts_with("((") {
3871                    depth += 2;
3872                    cursor = cursor.advanced_by("((");
3873                    continue;
3874                }
3875
3876                if rest.starts_with("))") {
3877                    if depth == 0 {
3878                        return Some(Span::from_positions(cursor, cursor.advanced_by("))")));
3879                    }
3880                    if depth == 1 {
3881                        cursor.advance(')');
3882                        return Some(Span::from_positions(cursor, cursor.advanced_by("))")));
3883                    }
3884                    depth -= 2;
3885                    cursor = cursor.advanced_by("))");
3886                    continue;
3887                }
3888            }
3889
3890            let ch = rest.chars().next()?;
3891            cursor.advance(ch);
3892
3893            if escaped {
3894                escaped = false;
3895                continue;
3896            }
3897
3898            match ch {
3899                '\\' if !in_single => escaped = true,
3900                '\'' if !in_double && !in_backtick => in_single = !in_single,
3901                '"' if !in_single && !in_backtick => in_double = !in_double,
3902                '`' if !in_single && !in_double => in_backtick = !in_backtick,
3903                '(' if !in_single && !in_double && !in_backtick => depth += 1,
3904                ')' if !in_single && !in_double && !in_backtick && depth > 0 => depth -= 1,
3905                _ => {}
3906            }
3907        }
3908
3909        None
3910    }
3911
3912    fn parse_function_body_command(&mut self, allow_bare_compound: bool) -> Result<Stmt> {
3913        if let Some(compound) = self.try_parse_compact_function_brace_body()? {
3914            let redirects = self.parse_trailing_redirects();
3915            return Ok(Self::lower_non_sequence_command_to_stmt(Command::Compound(
3916                Box::new(compound),
3917                redirects,
3918            )));
3919        }
3920
3921        let compound = match self.current_keyword() {
3922            Some(Keyword::If) if allow_bare_compound => self.parse_if()?,
3923            Some(Keyword::For) if allow_bare_compound => self.parse_for()?,
3924            Some(Keyword::Repeat) if allow_bare_compound && self.zsh_short_repeat_enabled() => {
3925                self.parse_repeat()?
3926            }
3927            Some(Keyword::Foreach) if allow_bare_compound && self.zsh_short_loops_enabled() => {
3928                self.parse_foreach()?
3929            }
3930            Some(Keyword::While) if allow_bare_compound => self.parse_while()?,
3931            Some(Keyword::Until) if allow_bare_compound => self.parse_until()?,
3932            Some(Keyword::Case) if allow_bare_compound => self.parse_case()?,
3933            Some(Keyword::Select) if allow_bare_compound => self.parse_select()?,
3934            _ => match self.current_token_kind {
3935                Some(TokenKind::LeftBrace) => self.parse_brace_group(BraceBodyContext::Function)?,
3936                Some(TokenKind::LeftParen) => self.parse_subshell()?,
3937                Some(TokenKind::DoubleLeftBracket) if allow_bare_compound => {
3938                    self.parse_conditional()?
3939                }
3940                Some(TokenKind::DoubleLeftParen) if allow_bare_compound => {
3941                    if self.looks_like_command_style_double_paren() {
3942                        self.split_current_double_left_paren();
3943                        self.parse_subshell()?
3944                    } else {
3945                        let checkpoint = self.checkpoint();
3946                        if let Ok(compound) = self.parse_arithmetic_command() {
3947                            compound
3948                        } else {
3949                            self.restore(checkpoint);
3950                            self.split_current_double_left_paren();
3951                            self.parse_subshell()?
3952                        }
3953                    }
3954                }
3955                _ => {
3956                    return Err(Error::parse(
3957                        "expected compound command for function body".to_string(),
3958                    ));
3959                }
3960            },
3961        };
3962        let redirects = self.parse_trailing_redirects();
3963        Ok(Self::lower_non_sequence_command_to_stmt(Command::Compound(
3964            Box::new(compound),
3965            redirects,
3966        )))
3967    }
3968
3969    fn parse_function_header_entry(&mut self) -> Result<FunctionHeaderEntry> {
3970        let word = self
3971            .take_current_word_and_advance()
3972            .ok_or_else(|| self.error("expected function name"))?;
3973        Ok(self.function_header_entry_from_word(word))
3974    }
3975
3976    fn parse_function_keyword_header_entry(&mut self) -> Result<FunctionHeaderEntry> {
3977        let word = self
3978            .take_current_word_and_advance()
3979            .or_else(|| self.take_current_function_keyword_name_and_advance())
3980            .ok_or_else(|| self.error("expected function name"))?;
3981        Ok(self.function_header_entry_from_word(word))
3982    }
3983
3984    fn take_current_function_keyword_name_and_advance(&mut self) -> Option<Word> {
3985        let text = match self.current_token_kind? {
3986            TokenKind::DoubleLeftBracket => "[[",
3987            TokenKind::DoubleRightBracket => "]]",
3988            TokenKind::LeftBrace => "{",
3989            TokenKind::RightBrace => "}",
3990            _ => return None,
3991        };
3992        let word = Word::literal_with_span(text, self.current_span);
3993        self.advance();
3994        Some(word)
3995    }
3996
3997    fn function_header_entry_from_word(&self, word: Word) -> FunctionHeaderEntry {
3998        let static_name = self.literal_word_text(&word).map(Name::from);
3999        FunctionHeaderEntry { word, static_name }
4000    }
4001
4002    fn parse_function_parens_span(&mut self) -> Result<Span> {
4003        if !self.at(TokenKind::LeftParen) {
4004            return Err(self.error("expected '(' in function definition"));
4005        }
4006        let left_paren_span = self.current_span;
4007        self.advance();
4008
4009        if !self.at(TokenKind::RightParen) {
4010            return Err(Error::parse(
4011                "expected ')' in function definition".to_string(),
4012            ));
4013        }
4014        let right_paren_span = self.current_span;
4015        self.advance();
4016        Ok(left_paren_span.merge(right_paren_span))
4017    }
4018
4019    fn parse_zsh_function_body_stmt(&mut self) -> Result<Stmt> {
4020        self.skip_newlines()?;
4021
4022        if let Some(compound) = self.try_parse_compact_function_brace_body()? {
4023            let redirects = self.parse_trailing_redirects();
4024            return Ok(Self::lower_non_sequence_command_to_stmt(Command::Compound(
4025                Box::new(compound),
4026                redirects,
4027            )));
4028        }
4029
4030        if self.at(TokenKind::LeftBrace) {
4031            let compound = self.parse_brace_group(BraceBodyContext::Function)?;
4032            let redirects = self.parse_trailing_redirects();
4033            return Ok(Self::lower_non_sequence_command_to_stmt(Command::Compound(
4034                Box::new(compound),
4035                redirects,
4036            )));
4037        }
4038
4039        self.parse_single_stmt_command()
4040    }
4041
4042    fn parse_single_stmt_command(&mut self) -> Result<Stmt> {
4043        let mut stmt = self
4044            .parse_pipeline()?
4045            .ok_or_else(|| self.error("expected command"))?;
4046
4047        let Some(kind) = self.current_token_kind else {
4048            return Ok(stmt);
4049        };
4050        let operator = match kind {
4051            TokenKind::And => Some((Some(BinaryOp::And), None, false)),
4052            TokenKind::Or => Some((Some(BinaryOp::Or), None, false)),
4053            TokenKind::Semicolon => Some((None, Some(StmtTerminator::Semicolon), true)),
4054            TokenKind::Background => Some((
4055                None,
4056                Some(StmtTerminator::Background(BackgroundOperator::Plain)),
4057                true,
4058            )),
4059            TokenKind::BackgroundPipe => Some((
4060                None,
4061                Some(StmtTerminator::Background(BackgroundOperator::Pipe)),
4062                true,
4063            )),
4064            TokenKind::BackgroundBang => Some((
4065                None,
4066                Some(StmtTerminator::Background(BackgroundOperator::Bang)),
4067                true,
4068            )),
4069            _ => None,
4070        };
4071        let Some((binary_op, terminator, allow_empty_tail)) = operator else {
4072            return Ok(stmt);
4073        };
4074        let operator_span = self.current_span;
4075        self.advance();
4076
4077        if let Some(binary_op) = binary_op {
4078            self.skip_newlines()?;
4079            if let Some(right) = self.parse_pipeline()? {
4080                stmt = Self::binary_stmt(stmt, binary_op, operator_span, right);
4081            }
4082            return Ok(stmt);
4083        }
4084
4085        if allow_empty_tail
4086            && matches!(
4087                self.current_token_kind,
4088                Some(TokenKind::Semicolon | TokenKind::Newline)
4089            )
4090        {
4091            self.advance();
4092        }
4093
4094        stmt.terminator = terminator;
4095        stmt.terminator_span = Some(operator_span);
4096        Ok(stmt)
4097    }
4098
4099    fn parse_anonymous_function_args(&mut self) -> Result<SmallVec<[Word; 2]>> {
4100        let mut args = SmallVec::<[Word; 2]>::new();
4101        while self.current_token_kind.is_some_and(TokenKind::is_word_like) {
4102            let word = self
4103                .take_current_word_and_advance()
4104                .ok_or_else(|| self.error("expected anonymous function argument"))?;
4105            args.push(word);
4106        }
4107        Ok(args)
4108    }
4109
4110    /// Parse function definition with 'function' keyword: function name { body }
4111    fn parse_function_keyword(&mut self) -> Result<Command> {
4112        self.ensure_function_keyword()?;
4113        let start_span = self.current_span;
4114        self.advance(); // consume 'function'
4115        self.skip_newlines()?;
4116
4117        if self.dialect == ShellDialect::Zsh {
4118            let mut entries = Vec::new();
4119            while self.current_token_kind.is_some_and(TokenKind::is_word_like) {
4120                entries.push(self.parse_function_header_entry()?);
4121                if self.at(TokenKind::LeftParen) {
4122                    break;
4123                }
4124            }
4125
4126            let trailing_parens_span = if !entries.is_empty() && self.at(TokenKind::LeftParen) {
4127                Some(self.parse_function_parens_span()?)
4128            } else {
4129                None
4130            };
4131
4132            if entries.is_empty() {
4133                let body = self.parse_zsh_function_body_stmt()?;
4134                let args = self.parse_anonymous_function_args()?;
4135                let redirects = self.parse_trailing_redirects();
4136                let span = start_span.merge(self.current_span);
4137                return Ok(Command::AnonymousFunction(
4138                    AnonymousFunctionCommand {
4139                        surface: AnonymousFunctionSurface::FunctionKeyword {
4140                            function_keyword_span: start_span,
4141                        },
4142                        body: Box::new(body),
4143                        args: args.into_vec(),
4144                        span,
4145                    },
4146                    redirects,
4147                ));
4148            }
4149
4150            let body = self.parse_zsh_function_body_stmt()?;
4151            let span = start_span.merge(self.current_span);
4152            return Ok(Command::Function(FunctionDef {
4153                header: FunctionHeader {
4154                    function_keyword_span: Some(start_span),
4155                    entries,
4156                    trailing_parens_span,
4157                },
4158                body: Box::new(body),
4159                span,
4160            }));
4161        }
4162
4163        let entry = self.parse_function_keyword_header_entry()?;
4164        let saw_newline_after_name = self.skip_newlines_with_flag()?;
4165        let (trailing_parens_span, allow_bare_compound) = if self.at(TokenKind::LeftParen) {
4166            let parens_span = self.parse_function_parens_span()?;
4167            (Some(parens_span), self.skip_newlines_with_flag()?)
4168        } else {
4169            (None, saw_newline_after_name)
4170        };
4171
4172        let body = self.parse_function_body_command(allow_bare_compound)?;
4173        let span = start_span.merge(self.current_span);
4174
4175        Ok(Command::Function(FunctionDef {
4176            header: FunctionHeader {
4177                function_keyword_span: Some(start_span),
4178                entries: vec![entry],
4179                trailing_parens_span,
4180            },
4181            body: Box::new(body),
4182            span,
4183        }))
4184    }
4185
4186    /// Parse POSIX-style function definition: name() { body }
4187    fn parse_function_posix(&mut self) -> Result<Command> {
4188        let start_span = self.current_span;
4189        let entry = self.parse_function_header_entry()?;
4190        let trailing_parens_span = self.parse_function_parens_span()?;
4191
4192        self.finish_parse_function_posix(start_span, entry, trailing_parens_span)
4193    }
4194
4195    fn finish_parse_function_posix(
4196        &mut self,
4197        start_span: Span,
4198        entry: FunctionHeaderEntry,
4199        trailing_parens_span: Span,
4200    ) -> Result<Command> {
4201        let body = if self.dialect == ShellDialect::Zsh {
4202            self.parse_zsh_function_body_stmt()?
4203        } else {
4204            self.skip_newlines()?;
4205            self.parse_function_body_command(true)?
4206        };
4207
4208        Ok(Command::Function(FunctionDef {
4209            header: FunctionHeader {
4210                function_keyword_span: None,
4211                entries: vec![entry],
4212                trailing_parens_span: Some(trailing_parens_span),
4213            },
4214            body: Box::new(body),
4215            span: start_span.merge(self.current_span),
4216        }))
4217    }
4218
4219    fn try_parse_zsh_attached_parens_function(&mut self) -> Result<Option<Command>> {
4220        if self.dialect != ShellDialect::Zsh || !self.at_word_like() {
4221            return Ok(None);
4222        }
4223
4224        let Some(word_text) = self.current_source_like_word_text() else {
4225            return Ok(None);
4226        };
4227        let Some(header_text) = word_text.as_ref().strip_suffix("()") else {
4228            return Ok(None);
4229        };
4230        if header_text.is_empty() || header_text.contains('=') {
4231            return Ok(None);
4232        }
4233
4234        let checkpoint = self.checkpoint();
4235        self.advance();
4236        if let Err(error) = self.skip_newlines() {
4237            self.restore(checkpoint);
4238            return Err(error);
4239        }
4240        if !self.at(TokenKind::LeftBrace) {
4241            self.restore(checkpoint);
4242            return Ok(None);
4243        }
4244        self.restore(checkpoint);
4245
4246        let start_span = self.current_span;
4247        let header_span =
4248            Span::from_positions(start_span.start, start_span.start.advanced_by(header_text));
4249        let parens_span = Span::from_positions(header_span.end, start_span.end);
4250        let header_word =
4251            self.parse_word_with_context(header_text, header_span, header_span.start, true);
4252        let entry = self.function_header_entry_from_word(header_word);
4253        self.advance();
4254
4255        self.finish_parse_function_posix(start_span, entry, parens_span)
4256            .map(Some)
4257    }
4258
4259    fn parse_anonymous_paren_function(&mut self) -> Result<Command> {
4260        let start_span = self.current_span;
4261        let parens_span = self.parse_function_parens_span()?;
4262        let body = self.parse_zsh_function_body_stmt()?;
4263        let args = self.parse_anonymous_function_args()?;
4264        let redirects = self.parse_trailing_redirects();
4265        let span = start_span.merge(self.current_span);
4266        Ok(Command::AnonymousFunction(
4267            AnonymousFunctionCommand {
4268                surface: AnonymousFunctionSurface::Parens { parens_span },
4269                body: Box::new(body),
4270                args: args.into_vec(),
4271                span,
4272            },
4273            redirects,
4274        ))
4275    }
4276
4277    /// Parse commands until a terminating keyword
4278    fn parse_compound_list(&mut self, terminator: Keyword) -> Result<Vec<Stmt>> {
4279        self.parse_compound_list_until(KeywordSet::single(terminator))
4280    }
4281
4282    /// Parse commands until one of the terminating keywords
4283    fn parse_compound_list_until(&mut self, terminators: KeywordSet) -> Result<Vec<Stmt>> {
4284        let mut stmts = Vec::with_capacity(4);
4285
4286        loop {
4287            self.skip_command_separators()?;
4288
4289            // Check for terminators
4290            if self
4291                .current_keyword()
4292                .is_some_and(|keyword| terminators.contains(keyword))
4293            {
4294                break;
4295            }
4296
4297            if self.current_token.is_none() {
4298                break;
4299            }
4300
4301            let command_stmts = self.parse_command_list_required()?;
4302            self.apply_stmt_list_effects(&command_stmts);
4303            stmts.extend(command_stmts);
4304        }
4305
4306        Ok(stmts)
4307    }
4308
4309    /// Reserved words that cannot start a simple command.
4310    /// These words are only special in command position, not as arguments.
4311    /// Check if a word cannot start a command
4312    fn is_non_command_keyword(keyword: Keyword) -> bool {
4313        NON_COMMAND_KEYWORDS.contains(keyword)
4314    }
4315
4316    /// Check if current token is a specific keyword
4317    fn is_keyword(&self, keyword: Keyword) -> bool {
4318        self.current_keyword() == Some(keyword)
4319    }
4320
4321    /// Expect a specific keyword
4322    fn expect_keyword(&mut self, keyword: Keyword) -> Result<()> {
4323        if self.is_keyword(keyword) {
4324            self.advance();
4325            Ok(())
4326        } else {
4327            Err(self.error(format!("expected '{}'", keyword)))
4328        }
4329    }
4330    fn parse_simple_command(&mut self) -> Result<Option<SimpleCommand>> {
4331        self.tick()?;
4332        self.skip_newlines()?;
4333        self.check_error_token()?;
4334        let start_span = self.current_span;
4335
4336        let mut assignments = SmallVec::<[Assignment; 1]>::new();
4337        let mut words = SmallVec::<[Word; 2]>::new();
4338        let mut redirects = SmallVec::<[Redirect; 1]>::new();
4339
4340        loop {
4341            self.check_error_token()?;
4342            let next_kind_after_right_brace = if self.at(TokenKind::RightBrace) {
4343                self.peek_next_kind()
4344            } else {
4345                None
4346            };
4347            let right_brace_is_literal_argument = self.at(TokenKind::RightBrace)
4348                && !words.is_empty()
4349                && self.should_consume_right_brace_as_literal_argument(next_kind_after_right_brace);
4350            match self.current_token_kind {
4351                Some(kind) if kind.is_word_like() => {
4352                    // Bail out before touching word text when the token is a
4353                    // reserved word that cannot begin a simple command.
4354                    if words.is_empty()
4355                        && self
4356                            .current_keyword()
4357                            .is_some_and(Self::is_non_command_keyword)
4358                    {
4359                        break;
4360                    }
4361
4362                    let is_literal = kind == TokenKind::LiteralWord;
4363                    let word_text =
4364                        self.current_source_like_word_text_or_error("simple command word")?;
4365                    let assignment_shape = (!is_literal && words.is_empty())
4366                        .then(|| Self::is_assignment(word_text.as_ref()));
4367                    let assignment_shape = assignment_shape.flatten();
4368
4369                    // Check for assignment (only before the command name, not for literal words)
4370                    if words.is_empty()
4371                        && !is_literal
4372                        && let Some((assignment, needs_advance)) = self
4373                            .try_parse_assignment_with_shape(word_text.as_ref(), assignment_shape)
4374                    {
4375                        if needs_advance {
4376                            self.advance();
4377                        }
4378                        assignments.push(assignment);
4379                        continue;
4380                    }
4381
4382                    if words.is_empty()
4383                        && !is_literal
4384                        && assignment_shape.is_none()
4385                        && word_text.contains('[')
4386                        && let Some(assignment) =
4387                            self.try_parse_split_indexed_assignment_from_text()
4388                    {
4389                        assignments.push(assignment);
4390                        continue;
4391                    }
4392
4393                    // Handle compound array assignment in arg position:
4394                    // declare -a arr=(x y z) → arr=(x y z) as single arg
4395                    if word_text.ends_with('=') && !words.is_empty() {
4396                        let original_word = self.current_word_ref().cloned();
4397                        let saved_span = self.current_span;
4398                        self.advance();
4399                        if let Some(word) =
4400                            self.try_parse_compound_array_arg(word_text.as_ref(), saved_span)?
4401                        {
4402                            words.push(word);
4403                            continue;
4404                        }
4405                        // Not a compound assignment — treat as regular word
4406                        if let Some(word) = original_word {
4407                            words.push(word);
4408                        }
4409                        continue;
4410                    }
4411
4412                    if let Some(word) = self.take_current_word_and_advance() {
4413                        words.push(word);
4414                    }
4415                }
4416                Some(TokenKind::LeftParen) if !words.is_empty() => {
4417                    let Some(word) = self.take_current_word_and_advance() else {
4418                        break;
4419                    };
4420                    words.push(word);
4421                }
4422                Some(TokenKind::Newline) => {
4423                    let next_kind = self.peek_next_kind();
4424                    let supports_fd_var = next_kind.is_some_and(|kind| {
4425                        matches!(kind, TokenKind::HereDoc | TokenKind::HereDocStrip)
4426                            || Self::redirect_supports_fd_var(kind)
4427                    });
4428                    if supports_fd_var {
4429                        let (fd_var, fd_var_span) = self.pop_line_continuation_fd_var(&mut words);
4430                        if let Some(fd_var) = fd_var {
4431                            self.advance();
4432                            if matches!(
4433                                self.current_token_kind,
4434                                Some(TokenKind::HereDoc | TokenKind::HereDocStrip)
4435                            ) {
4436                                self.parse_heredoc_redirect(
4437                                    self.current_token_kind == Some(TokenKind::HereDocStrip),
4438                                    &mut redirects,
4439                                    Some(fd_var),
4440                                    fd_var_span,
4441                                )?;
4442                                continue;
4443                            }
4444
4445                            if self.consume_non_heredoc_redirect(
4446                                &mut redirects,
4447                                Some(fd_var),
4448                                fd_var_span,
4449                                true,
4450                            )? {
4451                                continue;
4452                            }
4453                        }
4454                    }
4455                    break;
4456                }
4457                Some(kind) if Self::is_redirect_kind(kind) => {
4458                    if matches!(kind, TokenKind::HereDoc | TokenKind::HereDocStrip) {
4459                        let (fd_var, fd_var_span) = if words
4460                            .last()
4461                            .is_some_and(|word| self.word_is_attached_to_current_token(word))
4462                        {
4463                            self.pop_fd_var(&mut words)
4464                        } else {
4465                            (None, None)
4466                        };
4467                        self.parse_heredoc_redirect(
4468                            kind == TokenKind::HereDocStrip,
4469                            &mut redirects,
4470                            fd_var,
4471                            fd_var_span,
4472                        )?;
4473                        continue;
4474                    }
4475
4476                    let (fd_var, fd_var_span) = if Self::redirect_supports_fd_var(kind) {
4477                        if words
4478                            .last()
4479                            .is_some_and(|word| self.word_is_attached_to_current_token(word))
4480                        {
4481                            self.pop_fd_var(&mut words)
4482                        } else {
4483                            (None, None)
4484                        }
4485                    } else {
4486                        (None, None)
4487                    };
4488
4489                    if self.consume_non_heredoc_redirect(
4490                        &mut redirects,
4491                        fd_var,
4492                        fd_var_span,
4493                        true,
4494                    )? {
4495                        continue;
4496                    }
4497                    break;
4498                }
4499                Some(TokenKind::ProcessSubIn) | Some(TokenKind::ProcessSubOut) => {
4500                    let word = self.expect_word()?;
4501                    words.push(word);
4502                }
4503                // `{` can appear as a literal argument outside command position.
4504                Some(TokenKind::LeftBrace) if !words.is_empty() => {
4505                    words.push(Word::literal_with_span("{", self.current_span));
4506                    self.advance();
4507                }
4508                // Inside brace groups, a bare `}` can still be a literal
4509                // argument like `echo }`, but only when it's separated from the
4510                // preceding token by whitespace and isn't introducing an outer
4511                // redirect on the brace group itself.
4512                Some(TokenKind::RightBrace) if right_brace_is_literal_argument => {
4513                    words.push(Word::literal_with_span("}", self.current_span));
4514                    self.advance();
4515                }
4516                Some(TokenKind::Semicolon)
4517                | Some(TokenKind::Pipe)
4518                | Some(TokenKind::And)
4519                | Some(TokenKind::Or)
4520                | None => break,
4521                _ => break,
4522            }
4523        }
4524
4525        // Handle assignment-only or redirect-only commands with no command word.
4526        if words.is_empty() && (!assignments.is_empty() || !redirects.is_empty()) {
4527            return Ok(Some(SimpleCommand {
4528                name: Word::literal(""),
4529                args: SmallVec::new(),
4530                redirects,
4531                assignments,
4532                span: start_span.merge(self.current_span),
4533            }));
4534        }
4535
4536        if words.is_empty() {
4537            return Ok(None);
4538        }
4539
4540        let name = words.remove(0);
4541        let args = words;
4542
4543        Ok(Some(SimpleCommand {
4544            name,
4545            args,
4546            redirects,
4547            assignments,
4548            span: start_span.merge(self.current_span),
4549        }))
4550    }
4551
4552    /// Extract fd-variable name from `{varname}` pattern in the last word.
4553    /// If the last word is a single literal `{identifier}`, pop it and return the name.
4554    /// Used for `exec {var}>file` / `exec {var}>&-` syntax.
4555    fn pop_fd_var(&self, words: &mut SmallVec<[Word; 2]>) -> (Option<Name>, Option<Span>) {
4556        if let Some(last) = words.last()
4557            && last.parts.len() == 1
4558            && let WordPart::Literal(ref s) = last.parts[0].kind
4559            && let Some(span) = last.part_span(0)
4560            && let text = s.as_str(self.input, span)
4561            && text.starts_with('{')
4562            && text.ends_with('}')
4563            && text.len() > 2
4564            && text[1..text.len() - 1]
4565                .chars()
4566                .all(|c| c.is_alphanumeric() || c == '_')
4567        {
4568            let var_name = text[1..text.len() - 1].to_string();
4569            let start = last.span.start.advanced_by("{");
4570            let span = Span::from_positions(start, start.advanced_by(&var_name));
4571            words.pop();
4572            return (Some(Name::from(var_name)), Some(span));
4573        }
4574        (None, None)
4575    }
4576
4577    fn word_is_attached_to_current_token(&self, word: &Word) -> bool {
4578        let start = word.span.end.offset;
4579        let end = self.current_span.start.offset;
4580        let input_len = self.input.len();
4581        start <= end
4582            && end <= input_len
4583            && Self::fd_var_gap_allows_attachment(&self.input[start..end])
4584    }
4585
4586    fn pop_line_continuation_fd_var(
4587        &self,
4588        words: &mut SmallVec<[Word; 2]>,
4589    ) -> (Option<Name>, Option<Span>) {
4590        let Some(last) = words.last() else {
4591            return (None, None);
4592        };
4593        let Some(text) = self.single_literal_word_text(last) else {
4594            return (None, None);
4595        };
4596        let Some(fd_text) = text.strip_suffix('\\') else {
4597            return (None, None);
4598        };
4599        let Some((fd_var, fd_var_span)) = Self::fd_var_from_text(fd_text, last.span) else {
4600            return (None, None);
4601        };
4602        words.pop();
4603        (Some(fd_var), Some(fd_var_span))
4604    }
4605}