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