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    pub(super) 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            return false;
3085        }
3086
3087        if self.brace_group_depth == 0 {
3088            return true;
3089        }
3090
3091        if self.dialect != ShellDialect::Zsh {
3092            return true;
3093        }
3094
3095        next_kind_after_right_brace == Some(TokenKind::Semicolon)
3096            && self.current_token_is_tight_to_next_token()
3097            && self.next_token_after_tight_semicolon_is(TokenKind::RightBrace)
3098    }
3099
3100    fn next_token_after_tight_semicolon_is(&mut self, expected: TokenKind) -> bool {
3101        let checkpoint = self.checkpoint();
3102        self.advance();
3103        if !self.at(TokenKind::Semicolon) {
3104            self.restore(checkpoint);
3105            return false;
3106        }
3107        self.advance();
3108        let result = self.at(expected);
3109        self.restore(checkpoint);
3110        result
3111    }
3112
3113    /// Parse arithmetic command ((expression))
3114    /// Parse [[ conditional expression ]]
3115    fn parse_conditional(&mut self) -> Result<CompoundCommand> {
3116        self.ensure_double_bracket()?;
3117        let left_bracket_span = self.current_span;
3118        self.advance(); // consume '[['
3119        self.skip_conditional_newlines();
3120
3121        let expression = self.parse_conditional_or(false)?;
3122        self.skip_conditional_newlines();
3123
3124        let right_bracket_span = match self.current_token_kind {
3125            Some(TokenKind::DoubleRightBracket) => {
3126                let span = self.current_span;
3127                self.advance(); // consume ']]'
3128                span
3129            }
3130            None => {
3131                return Err(crate::error::Error::parse(
3132                    "unexpected end of input in [[ ]]".to_string(),
3133                ));
3134            }
3135            _ => return Err(self.error("expected ']]' to close conditional expression")),
3136        };
3137
3138        Ok(CompoundCommand::Conditional(ConditionalCommand {
3139            expression,
3140            span: left_bracket_span.merge(right_bracket_span),
3141            left_bracket_span,
3142            right_bracket_span,
3143        }))
3144    }
3145
3146    fn skip_conditional_newlines(&mut self) {
3147        while self.at(TokenKind::Newline) {
3148            self.advance();
3149        }
3150    }
3151
3152    fn parse_conditional_or(&mut self, stop_at_right_paren: bool) -> Result<ConditionalExpr> {
3153        let mut expr = self.parse_conditional_and(stop_at_right_paren)?;
3154
3155        loop {
3156            self.skip_conditional_newlines();
3157            if !self.at(TokenKind::Or) {
3158                break;
3159            }
3160
3161            let op_span = self.current_span;
3162            self.advance();
3163            let right = self.parse_conditional_and(stop_at_right_paren)?;
3164            expr = ConditionalExpr::Binary(ConditionalBinaryExpr {
3165                left: Box::new(expr),
3166                op: ConditionalBinaryOp::Or,
3167                op_span,
3168                right: Box::new(right),
3169            });
3170        }
3171
3172        Ok(expr)
3173    }
3174
3175    fn parse_conditional_and(&mut self, stop_at_right_paren: bool) -> Result<ConditionalExpr> {
3176        let mut expr = self.parse_conditional_term(stop_at_right_paren)?;
3177
3178        loop {
3179            self.skip_conditional_newlines();
3180            if !self.at(TokenKind::And) {
3181                break;
3182            }
3183
3184            let op_span = self.current_span;
3185            self.advance();
3186            let right = self.parse_conditional_term(stop_at_right_paren)?;
3187            expr = ConditionalExpr::Binary(ConditionalBinaryExpr {
3188                left: Box::new(expr),
3189                op: ConditionalBinaryOp::And,
3190                op_span,
3191                right: Box::new(right),
3192            });
3193        }
3194
3195        Ok(expr)
3196    }
3197
3198    fn parse_conditional_term(&mut self, stop_at_right_paren: bool) -> Result<ConditionalExpr> {
3199        self.skip_conditional_newlines();
3200
3201        if let Some(op) = self.current_conditional_unary_op() {
3202            let op_span = self.current_span;
3203            self.advance();
3204            self.skip_conditional_newlines();
3205
3206            let expr = if matches!(op, ConditionalUnaryOp::Not) {
3207                self.parse_conditional_term(stop_at_right_paren)?
3208            } else {
3209                if matches!(
3210                    op,
3211                    ConditionalUnaryOp::VariableSet | ConditionalUnaryOp::ReferenceVariable
3212                ) {
3213                    let word = self.collect_conditional_context_word(stop_at_right_paren)?;
3214                    self.conditional_var_ref_expr(word)
3215                } else {
3216                    let word = self.parse_conditional_operand_word()?;
3217                    ConditionalExpr::Word(word)
3218                }
3219            };
3220
3221            return Ok(ConditionalExpr::Unary(ConditionalUnaryExpr {
3222                op,
3223                op_span,
3224                expr: Box::new(expr),
3225            }));
3226        }
3227
3228        if self.at(TokenKind::DoubleLeftParen) {
3229            if self.dialect == ShellDialect::Zsh {
3230                let left_paren_span = self.current_span;
3231                if let Some(right_paren_span) = self.scan_arithmetic_command_close(left_paren_span)
3232                {
3233                    let span = left_paren_span.merge(right_paren_span);
3234                    let text = span.slice(self.input).to_string();
3235                    while self.current_token.is_some()
3236                        && self.current_span.start.offset < right_paren_span.end.offset
3237                    {
3238                        self.advance();
3239                    }
3240                    return Ok(ConditionalExpr::Word(
3241                        self.parse_word_with_context(&text, span, span.start, true),
3242                    ));
3243                }
3244            }
3245            self.split_current_double_left_paren();
3246        }
3247
3248        let left = if self.at(TokenKind::LeftParen) {
3249            let left_paren_span = self.current_span;
3250            self.advance();
3251            let expr = self.parse_conditional_or(true)?;
3252            self.skip_conditional_newlines();
3253            if self.at(TokenKind::DoubleRightParen) {
3254                self.split_current_double_right_paren();
3255            }
3256            if !self.at(TokenKind::RightParen) {
3257                return Err(self.error("expected ')' in conditional expression"));
3258            }
3259            let right_paren_span = self.current_span;
3260            self.advance();
3261            ConditionalExpr::Parenthesized(ConditionalParenExpr {
3262                left_paren_span,
3263                expr: Box::new(expr),
3264                right_paren_span,
3265            })
3266        } else {
3267            ConditionalExpr::Word(self.parse_conditional_operand_word()?)
3268        };
3269
3270        self.skip_conditional_newlines();
3271
3272        let Some(op) = self.current_conditional_comparison_op() else {
3273            return Ok(left);
3274        };
3275
3276        let op_span = self.current_span;
3277        self.advance();
3278        self.skip_conditional_newlines();
3279
3280        let right = match op {
3281            ConditionalBinaryOp::RegexMatch => {
3282                if self.at(TokenKind::LeftBrace) {
3283                    return Err(self.error("expected conditional operand"));
3284                }
3285                ConditionalExpr::Regex(self.collect_conditional_context_word(stop_at_right_paren)?)
3286            }
3287            ConditionalBinaryOp::PatternEqShort
3288            | ConditionalBinaryOp::PatternEq
3289            | ConditionalBinaryOp::PatternNe => {
3290                let word = self.collect_conditional_context_word(stop_at_right_paren)?;
3291                ConditionalExpr::Pattern(self.pattern_from_conditional_word(&word))
3292            }
3293            _ => ConditionalExpr::Word(self.parse_conditional_operand_word()?),
3294        };
3295
3296        Ok(ConditionalExpr::Binary(ConditionalBinaryExpr {
3297            left: Box::new(left),
3298            op,
3299            op_span,
3300            right: Box::new(right),
3301        }))
3302    }
3303
3304    fn parse_conditional_operand_word(&mut self) -> Result<Word> {
3305        self.skip_conditional_newlines();
3306
3307        if let Some(word) = self.current_conditional_source_word(false) {
3308            self.advance_past_word(&word);
3309            self.restore_conditional_source_delimiter(word.span.end, false);
3310            return Ok(word);
3311        }
3312
3313        if let Some(word) = self.take_current_word_and_advance() {
3314            return Ok(word);
3315        }
3316
3317        let Some(word) = self.current_conditional_literal_word() else {
3318            return Err(self.error("expected conditional operand"));
3319        };
3320        self.advance_past_word(&word);
3321        Ok(word)
3322    }
3323
3324    fn conditional_var_ref_expr(&self, word: Word) -> ConditionalExpr {
3325        self.parse_var_ref_from_word(&word, SubscriptInterpretation::Contextual)
3326            .map(Box::new)
3327            .map(ConditionalExpr::VarRef)
3328            .unwrap_or(ConditionalExpr::Word(word))
3329    }
3330
3331    fn current_conditional_source_word(&mut self, stop_at_right_paren: bool) -> Option<Word> {
3332        let token = self.current_token.as_ref()?;
3333        if token.flags.is_synthetic() {
3334            return None;
3335        }
3336
3337        if matches!(
3338            self.current_token_kind,
3339            Some(TokenKind::QuotedWord | TokenKind::LiteralWord)
3340        ) {
3341            return None;
3342        }
3343
3344        let starts_with_paren = matches!(self.current_token_kind, Some(TokenKind::LeftParen));
3345        let starts_with_zsh_pattern_punct = matches!(
3346            self.current_token_kind,
3347            Some(TokenKind::RedirectIn | TokenKind::RedirectOut | TokenKind::RedirectReadWrite)
3348        ) && self.dialect == ShellDialect::Zsh;
3349
3350        if !starts_with_paren
3351            && !starts_with_zsh_pattern_punct
3352            && !self.current_token_kind.is_some_and(TokenKind::is_word_like)
3353        {
3354            return None;
3355        }
3356
3357        let start = self.current_span.start;
3358        let (text, end) =
3359            self.scan_conditional_source_word(start, stop_at_right_paren, starts_with_paren)?;
3360        let span = Span::from_positions(start, end);
3361        Some(self.parse_word_with_context(&text, span, start, true))
3362    }
3363
3364    fn scan_conditional_source_word(
3365        &self,
3366        start: Position,
3367        stop_at_right_paren: bool,
3368        starts_with_paren: bool,
3369    ) -> Option<(String, Position)> {
3370        if start.offset >= self.input.len() {
3371            return None;
3372        }
3373
3374        let mut cursor = start;
3375        let mut text = String::new();
3376        let mut paren_depth = 0_i32;
3377        let mut brace_depth = 0_i32;
3378        let mut bracket_depth = 0_i32;
3379        let mut in_single = false;
3380        let mut in_double = false;
3381        let mut in_backtick = false;
3382        let mut escaped = false;
3383        let mut prev_char = None;
3384
3385        while cursor.offset < self.input.len() {
3386            let rest = &self.input[cursor.offset..];
3387            if !in_single
3388                && !in_double
3389                && !in_backtick
3390                && !escaped
3391                && paren_depth == 0
3392                && brace_depth == 0
3393                && bracket_depth == 0
3394            {
3395                if rest.starts_with("]]")
3396                    || rest.starts_with("&&")
3397                    || rest.starts_with("||")
3398                    || (!starts_with_paren && rest.starts_with(')'))
3399                    || (stop_at_right_paren && rest.starts_with(')'))
3400                {
3401                    break;
3402                }
3403
3404                let ch = rest.chars().next()?;
3405                if matches!(ch, ' ' | '\t' | '\n' | ';') {
3406                    break;
3407                }
3408            }
3409
3410            let ch = self.input[cursor.offset..].chars().next()?;
3411            cursor.advance(ch);
3412            text.push(ch);
3413
3414            if escaped {
3415                escaped = false;
3416                prev_char = Some(ch);
3417                continue;
3418            }
3419
3420            match ch {
3421                '\\' if !in_single => escaped = true,
3422                '\'' if !in_double => in_single = !in_single,
3423                '"' if !in_single => in_double = !in_double,
3424                '`' if !in_single => in_backtick = !in_backtick,
3425                '(' if !in_single && !in_double && brace_depth == 0 => paren_depth += 1,
3426                ')' if !in_single && !in_double && brace_depth == 0 && paren_depth > 0 => {
3427                    paren_depth -= 1
3428                }
3429                '{' if !in_single && !in_double && (brace_depth > 0 || prev_char == Some('$')) => {
3430                    brace_depth += 1
3431                }
3432                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
3433                '[' if !in_single && !in_double => bracket_depth += 1,
3434                ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
3435                _ => {}
3436            }
3437
3438            prev_char = Some(ch);
3439        }
3440
3441        (!text.is_empty()).then_some((text, cursor))
3442    }
3443
3444    fn conditional_source_delimiter_after(
3445        &self,
3446        end: Position,
3447        stop_at_right_paren: bool,
3448    ) -> Option<(TokenKind, Span)> {
3449        let mut cursor = end;
3450        while cursor.offset < self.input.len() {
3451            let rest = &self.input[cursor.offset..];
3452            let ch = rest.chars().next()?;
3453            if matches!(ch, ' ' | '\t') {
3454                cursor.advance(ch);
3455                continue;
3456            }
3457            break;
3458        }
3459
3460        let rest = self.input.get(cursor.offset..)?;
3461        let (kind, text) = if rest.starts_with("]]") {
3462            (TokenKind::DoubleRightBracket, "]]")
3463        } else if rest.starts_with("&&") {
3464            (TokenKind::And, "&&")
3465        } else if rest.starts_with("||") {
3466            (TokenKind::Or, "||")
3467        } else if stop_at_right_paren && rest.starts_with(')') {
3468            (TokenKind::RightParen, ")")
3469        } else {
3470            return None;
3471        };
3472
3473        Some((kind, Span::from_positions(cursor, cursor.advanced_by(text))))
3474    }
3475
3476    fn restore_conditional_source_delimiter(&mut self, end: Position, stop_at_right_paren: bool) {
3477        let Some((kind, span)) = self.conditional_source_delimiter_after(end, stop_at_right_paren)
3478        else {
3479            return;
3480        };
3481
3482        if self.current_token_kind == Some(kind) && self.current_span == span {
3483            return;
3484        }
3485
3486        if let Some(current_kind) = self.current_token_kind
3487            && matches!(
3488                current_kind,
3489                TokenKind::Newline
3490                    | TokenKind::Semicolon
3491                    | TokenKind::And
3492                    | TokenKind::Or
3493                    | TokenKind::RightParen
3494                    | TokenKind::DoubleRightBracket
3495            )
3496        {
3497            self.synthetic_tokens
3498                .push_front(SyntheticToken::punctuation(current_kind, self.current_span));
3499        }
3500
3501        self.set_current_kind(kind, span);
3502    }
3503
3504    fn current_conditional_unary_op(&self) -> Option<ConditionalUnaryOp> {
3505        if !self.at(TokenKind::Word) {
3506            return None;
3507        }
3508        let word = self.current_word_str()?;
3509
3510        Some(match word {
3511            "!" => ConditionalUnaryOp::Not,
3512            "-e" | "-a" => ConditionalUnaryOp::Exists,
3513            "-f" => ConditionalUnaryOp::RegularFile,
3514            "-d" => ConditionalUnaryOp::Directory,
3515            "-c" => ConditionalUnaryOp::CharacterSpecial,
3516            "-b" => ConditionalUnaryOp::BlockSpecial,
3517            "-p" => ConditionalUnaryOp::NamedPipe,
3518            "-S" => ConditionalUnaryOp::Socket,
3519            "-L" | "-h" => ConditionalUnaryOp::Symlink,
3520            "-k" => ConditionalUnaryOp::Sticky,
3521            "-g" => ConditionalUnaryOp::SetGroupId,
3522            "-u" => ConditionalUnaryOp::SetUserId,
3523            "-G" => ConditionalUnaryOp::GroupOwned,
3524            "-O" => ConditionalUnaryOp::UserOwned,
3525            "-N" => ConditionalUnaryOp::Modified,
3526            "-r" => ConditionalUnaryOp::Readable,
3527            "-w" => ConditionalUnaryOp::Writable,
3528            "-x" => ConditionalUnaryOp::Executable,
3529            "-s" => ConditionalUnaryOp::NonEmptyFile,
3530            "-t" => ConditionalUnaryOp::FdTerminal,
3531            "-z" => ConditionalUnaryOp::EmptyString,
3532            "-n" => ConditionalUnaryOp::NonEmptyString,
3533            "-o" => ConditionalUnaryOp::OptionSet,
3534            "-v" => ConditionalUnaryOp::VariableSet,
3535            "-R" => ConditionalUnaryOp::ReferenceVariable,
3536            _ => return None,
3537        })
3538    }
3539
3540    fn current_conditional_comparison_op(&self) -> Option<ConditionalBinaryOp> {
3541        match self.current_token_kind? {
3542            TokenKind::Word => Some(match self.current_word_str()? {
3543                "=" => ConditionalBinaryOp::PatternEqShort,
3544                "==" => ConditionalBinaryOp::PatternEq,
3545                "!=" => ConditionalBinaryOp::PatternNe,
3546                "=~" => ConditionalBinaryOp::RegexMatch,
3547                "-nt" => ConditionalBinaryOp::NewerThan,
3548                "-ot" => ConditionalBinaryOp::OlderThan,
3549                "-ef" => ConditionalBinaryOp::SameFile,
3550                "-eq" => ConditionalBinaryOp::ArithmeticEq,
3551                "-ne" => ConditionalBinaryOp::ArithmeticNe,
3552                "-le" => ConditionalBinaryOp::ArithmeticLe,
3553                "-ge" => ConditionalBinaryOp::ArithmeticGe,
3554                "-lt" => ConditionalBinaryOp::ArithmeticLt,
3555                "-gt" => ConditionalBinaryOp::ArithmeticGt,
3556                _ => return None,
3557            }),
3558            TokenKind::RedirectIn => Some(ConditionalBinaryOp::LexicalBefore),
3559            TokenKind::RedirectOut => Some(ConditionalBinaryOp::LexicalAfter),
3560            _ => None,
3561        }
3562    }
3563
3564    fn collect_conditional_context_word(&mut self, stop_at_right_paren: bool) -> Result<Word> {
3565        self.skip_conditional_newlines();
3566
3567        if let Some(word) = self.current_conditional_source_word(stop_at_right_paren) {
3568            self.advance_past_word(&word);
3569            self.restore_conditional_source_delimiter(word.span.end, stop_at_right_paren);
3570            return Ok(word);
3571        }
3572
3573        let mut first_word: Option<Word> = None;
3574        let mut parts = Vec::new();
3575        let mut start = None;
3576        let mut end = None;
3577        let mut previous_end: Option<Position> = None;
3578        let mut composite = false;
3579        let mut paren_depth = 0usize;
3580
3581        loop {
3582            self.skip_conditional_newlines();
3583
3584            match self.current_token_kind {
3585                Some(TokenKind::DoubleRightBracket) => break,
3586                Some(TokenKind::And) | Some(TokenKind::Or) if paren_depth == 0 => break,
3587                Some(TokenKind::RightParen) if stop_at_right_paren && paren_depth == 0 => break,
3588                None => break,
3589                _ => {}
3590            }
3591
3592            if let Some(prev_end) = previous_end
3593                && prev_end.offset < self.current_span.start.offset
3594            {
3595                let gap_span = Span::from_positions(prev_end, self.current_span.start);
3596                let gap_text = gap_span.slice(self.input);
3597                if let Some(word) = first_word.take() {
3598                    parts.extend(word.parts);
3599                }
3600                if Self::source_text_needs_quote_preserving_decode(gap_text) {
3601                    let gap_word = self.decode_word_text_preserving_quotes_if_needed(
3602                        gap_text,
3603                        gap_span,
3604                        gap_span.start,
3605                        true,
3606                    );
3607                    parts.extend(gap_word.parts);
3608                } else {
3609                    parts.push(WordPartNode::new(
3610                        WordPart::Literal(LiteralText::source()),
3611                        gap_span,
3612                    ));
3613                }
3614                composite = true;
3615            }
3616
3617            match self.current_token_kind {
3618                Some(TokenKind::Word | TokenKind::LiteralWord | TokenKind::QuotedWord) => {
3619                    let word = self
3620                        .take_current_word()
3621                        .ok_or_else(|| self.error("expected conditional operand"))?;
3622                    if start.is_none() {
3623                        start = Some(word.span.start);
3624                    } else {
3625                        if let Some(first) = first_word.take() {
3626                            parts.extend(first.parts);
3627                        }
3628                        composite = true;
3629                    }
3630                    end = Some(word.span.end);
3631                    if first_word.is_none() && !composite {
3632                        first_word = Some(word);
3633                    } else {
3634                        parts.extend(word.parts);
3635                    }
3636                    previous_end = Some(self.current_span.end);
3637                    self.advance();
3638                }
3639                Some(TokenKind::LeftParen) => {
3640                    if start.is_none() {
3641                        start = Some(self.current_span.start);
3642                    }
3643                    end = Some(self.current_span.end);
3644                    if let Some(word) = first_word.take() {
3645                        parts.extend(word.parts);
3646                    }
3647                    parts.push(WordPartNode::new(
3648                        WordPart::Literal(LiteralText::owned("(")),
3649                        self.current_span,
3650                    ));
3651                    previous_end = Some(self.current_span.end);
3652                    paren_depth += 1;
3653                    composite = true;
3654                    self.advance();
3655                }
3656                Some(TokenKind::DoubleLeftParen) => {
3657                    if start.is_none() {
3658                        start = Some(self.current_span.start);
3659                    }
3660                    end = Some(self.current_span.end);
3661                    if let Some(word) = first_word.take() {
3662                        parts.extend(word.parts);
3663                    }
3664                    parts.push(WordPartNode::new(
3665                        WordPart::Literal(LiteralText::owned("((")),
3666                        self.current_span,
3667                    ));
3668                    previous_end = Some(self.current_span.end);
3669                    paren_depth += 2;
3670                    composite = true;
3671                    self.advance();
3672                }
3673                Some(TokenKind::RightParen) => {
3674                    if paren_depth == 0 {
3675                        break;
3676                    }
3677                    if start.is_none() {
3678                        start = Some(self.current_span.start);
3679                    }
3680                    end = Some(self.current_span.end);
3681                    if let Some(word) = first_word.take() {
3682                        parts.extend(word.parts);
3683                    }
3684                    parts.push(WordPartNode::new(
3685                        WordPart::Literal(LiteralText::owned(")")),
3686                        self.current_span,
3687                    ));
3688                    previous_end = Some(self.current_span.end);
3689                    paren_depth = paren_depth.saturating_sub(1);
3690                    composite = true;
3691                    self.advance();
3692                }
3693                Some(TokenKind::DoubleRightParen) => {
3694                    if start.is_none() {
3695                        start = Some(self.current_span.start);
3696                    }
3697                    end = Some(self.current_span.end);
3698                    if let Some(word) = first_word.take() {
3699                        parts.extend(word.parts);
3700                    }
3701                    parts.push(WordPartNode::new(
3702                        WordPart::Literal(LiteralText::owned("))")),
3703                        self.current_span,
3704                    ));
3705                    previous_end = Some(self.current_span.end);
3706                    paren_depth = paren_depth.saturating_sub(2);
3707                    composite = true;
3708                    self.advance();
3709                }
3710                Some(TokenKind::Pipe) => {
3711                    if start.is_none() {
3712                        start = Some(self.current_span.start);
3713                    }
3714                    end = Some(self.current_span.end);
3715                    if let Some(word) = first_word.take() {
3716                        parts.extend(word.parts);
3717                    }
3718                    parts.push(WordPartNode::new(
3719                        WordPart::Literal(LiteralText::owned("|")),
3720                        self.current_span,
3721                    ));
3722                    previous_end = Some(self.current_span.end);
3723                    composite = true;
3724                    self.advance();
3725                }
3726                Some(TokenKind::And) => {
3727                    if paren_depth == 0 {
3728                        break;
3729                    }
3730                    if start.is_none() {
3731                        start = Some(self.current_span.start);
3732                    }
3733                    end = Some(self.current_span.end);
3734                    if let Some(word) = first_word.take() {
3735                        parts.extend(word.parts);
3736                    }
3737                    parts.push(WordPartNode::new(
3738                        WordPart::Literal(LiteralText::owned("&&")),
3739                        self.current_span,
3740                    ));
3741                    previous_end = Some(self.current_span.end);
3742                    composite = true;
3743                    self.advance();
3744                }
3745                Some(TokenKind::Or) => {
3746                    if paren_depth == 0 {
3747                        break;
3748                    }
3749                    if start.is_none() {
3750                        start = Some(self.current_span.start);
3751                    }
3752                    end = Some(self.current_span.end);
3753                    if let Some(word) = first_word.take() {
3754                        parts.extend(word.parts);
3755                    }
3756                    parts.push(WordPartNode::new(
3757                        WordPart::Literal(LiteralText::owned("||")),
3758                        self.current_span,
3759                    ));
3760                    previous_end = Some(self.current_span.end);
3761                    composite = true;
3762                    self.advance();
3763                }
3764                Some(TokenKind::RedirectIn)
3765                | Some(TokenKind::RedirectOut)
3766                | Some(TokenKind::RedirectReadWrite) => {
3767                    let literal = self.input
3768                        [self.current_span.start.offset..self.current_span.end.offset]
3769                        .to_string();
3770                    if start.is_none() {
3771                        start = Some(self.current_span.start);
3772                    }
3773                    end = Some(self.current_span.end);
3774                    if let Some(word) = first_word.take() {
3775                        parts.extend(word.parts);
3776                    }
3777                    parts.push(WordPartNode::new(
3778                        WordPart::Literal(self.literal_text(
3779                            literal,
3780                            self.current_span.start,
3781                            self.current_span.end,
3782                            true,
3783                        )),
3784                        self.current_span,
3785                    ));
3786                    previous_end = Some(self.current_span.end);
3787                    composite = true;
3788                    self.advance();
3789                }
3790                _ => {
3791                    let literal = self.input
3792                        [self.current_span.start.offset..self.current_span.end.offset]
3793                        .to_string();
3794                    if literal.is_empty() {
3795                        break;
3796                    }
3797                    if start.is_none() {
3798                        start = Some(self.current_span.start);
3799                    }
3800                    end = Some(self.current_span.end);
3801                    if let Some(word) = first_word.take() {
3802                        parts.extend(word.parts);
3803                    }
3804                    parts.push(WordPartNode::new(
3805                        WordPart::Literal(self.literal_text(
3806                            literal,
3807                            self.current_span.start,
3808                            self.current_span.end,
3809                            true,
3810                        )),
3811                        self.current_span,
3812                    ));
3813                    previous_end = Some(self.current_span.end);
3814                    composite = true;
3815                    self.advance();
3816                }
3817            }
3818        }
3819
3820        if !composite && let Some(word) = first_word {
3821            return Ok(word);
3822        }
3823
3824        let (start, end) = match (start, end) {
3825            (Some(start), Some(end)) => (start, end),
3826            _ => return Err(self.error("expected conditional operand")),
3827        };
3828
3829        Ok(self.word_with_parts(parts, Span::from_positions(start, end)))
3830    }
3831
3832    fn parse_arithmetic_command(&mut self) -> Result<CompoundCommand> {
3833        self.ensure_arithmetic_command()?;
3834        let left_paren_span = self.current_span;
3835        let Some(right_paren_span) = self.scan_arithmetic_command_close(left_paren_span) else {
3836            return Err(Error::parse(
3837                "unexpected end of input in arithmetic command".to_string(),
3838            ));
3839        };
3840        while self.current_token.is_some()
3841            && self.current_span.start.offset < right_paren_span.end.offset
3842        {
3843            self.advance();
3844        }
3845
3846        let expr_span = Self::optional_span(left_paren_span.end, right_paren_span.start);
3847        let expr_ast = self
3848            .parse_explicit_arithmetic_span(expr_span, "invalid arithmetic command")
3849            .ok()
3850            .flatten();
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_function_header_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_function_header_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_header_word_and_advance(&mut self) -> Option<Word> {
3987        let span = self.current_span;
3988        if let Some(token) = self.current_token.clone()
3989            && let Some(word) = self.simple_word_from_token(&token, span)
3990        {
3991            self.advance_past_word(&word);
3992            return Some(word);
3993        }
3994
3995        let token = self.current_token.take()?;
3996        let word = self.decode_word_from_token(&token, span);
3997        self.current_token = Some(token);
3998        if let Some(word) = word.as_ref() {
3999            self.advance_past_word(word);
4000        }
4001        word
4002    }
4003
4004    fn take_current_function_keyword_name_and_advance(&mut self) -> Option<Word> {
4005        let text = match self.current_token_kind? {
4006            TokenKind::DoubleLeftBracket => "[[",
4007            TokenKind::DoubleRightBracket => "]]",
4008            TokenKind::LeftBrace => "{",
4009            TokenKind::RightBrace => "}",
4010            _ => return None,
4011        };
4012        let word = Word::literal_with_span(text, self.current_span);
4013        self.advance();
4014        Some(word)
4015    }
4016
4017    fn function_header_entry_from_word(&self, word: Word) -> FunctionHeaderEntry {
4018        let static_name = self.literal_word_text(&word).map(Name::from);
4019        FunctionHeaderEntry { word, static_name }
4020    }
4021
4022    fn parse_function_parens_span(&mut self) -> Result<Span> {
4023        if !self.at(TokenKind::LeftParen) {
4024            return Err(self.error("expected '(' in function definition"));
4025        }
4026        let left_paren_span = self.current_span;
4027        self.advance();
4028
4029        if !self.at(TokenKind::RightParen) {
4030            return Err(Error::parse(
4031                "expected ')' in function definition".to_string(),
4032            ));
4033        }
4034        let right_paren_span = self.current_span;
4035        self.advance();
4036        Ok(left_paren_span.merge(right_paren_span))
4037    }
4038
4039    fn parse_zsh_function_body_stmt(&mut self) -> Result<Stmt> {
4040        self.skip_newlines()?;
4041
4042        if let Some(compound) = self.try_parse_compact_function_brace_body()? {
4043            let redirects = self.parse_trailing_redirects();
4044            return Ok(Self::lower_non_sequence_command_to_stmt(Command::Compound(
4045                Box::new(compound),
4046                redirects,
4047            )));
4048        }
4049
4050        if self.at(TokenKind::LeftBrace) {
4051            let compound = self.parse_brace_group(BraceBodyContext::Function)?;
4052            let redirects = self.parse_trailing_redirects();
4053            return Ok(Self::lower_non_sequence_command_to_stmt(Command::Compound(
4054                Box::new(compound),
4055                redirects,
4056            )));
4057        }
4058
4059        self.parse_single_stmt_command()
4060    }
4061
4062    fn parse_single_stmt_command(&mut self) -> Result<Stmt> {
4063        let mut stmt = self
4064            .parse_pipeline()?
4065            .ok_or_else(|| self.error("expected command"))?;
4066
4067        let Some(kind) = self.current_token_kind else {
4068            return Ok(stmt);
4069        };
4070        let operator = match kind {
4071            TokenKind::And => Some((Some(BinaryOp::And), None, false)),
4072            TokenKind::Or => Some((Some(BinaryOp::Or), None, false)),
4073            TokenKind::Semicolon => Some((None, Some(StmtTerminator::Semicolon), true)),
4074            TokenKind::Background => Some((
4075                None,
4076                Some(StmtTerminator::Background(BackgroundOperator::Plain)),
4077                true,
4078            )),
4079            TokenKind::BackgroundPipe => Some((
4080                None,
4081                Some(StmtTerminator::Background(BackgroundOperator::Pipe)),
4082                true,
4083            )),
4084            TokenKind::BackgroundBang => Some((
4085                None,
4086                Some(StmtTerminator::Background(BackgroundOperator::Bang)),
4087                true,
4088            )),
4089            _ => None,
4090        };
4091        let Some((binary_op, terminator, allow_empty_tail)) = operator else {
4092            return Ok(stmt);
4093        };
4094        let operator_span = self.current_span;
4095        self.advance();
4096
4097        if let Some(binary_op) = binary_op {
4098            self.skip_newlines()?;
4099            if let Some(right) = self.parse_pipeline()? {
4100                stmt = Self::binary_stmt(stmt, binary_op, operator_span, right);
4101            }
4102            return Ok(stmt);
4103        }
4104
4105        if allow_empty_tail
4106            && matches!(
4107                self.current_token_kind,
4108                Some(TokenKind::Semicolon | TokenKind::Newline)
4109            )
4110        {
4111            self.advance();
4112        }
4113
4114        stmt.terminator = terminator;
4115        stmt.terminator_span = Some(operator_span);
4116        Ok(stmt)
4117    }
4118
4119    fn parse_anonymous_function_args(&mut self) -> Result<SmallVec<[Word; 2]>> {
4120        let mut args = SmallVec::<[Word; 2]>::new();
4121        while self.current_token_kind.is_some_and(TokenKind::is_word_like) {
4122            let word = self
4123                .take_current_word_and_advance()
4124                .ok_or_else(|| self.error("expected anonymous function argument"))?;
4125            args.push(word);
4126        }
4127        Ok(args)
4128    }
4129
4130    /// Parse function definition with 'function' keyword: function name { body }
4131    fn parse_function_keyword(&mut self) -> Result<Command> {
4132        self.ensure_function_keyword()?;
4133        let start_span = self.current_span;
4134        self.advance(); // consume 'function'
4135        self.skip_newlines()?;
4136
4137        if self.dialect == ShellDialect::Zsh {
4138            let mut entries = Vec::new();
4139            while self.current_token_kind.is_some_and(TokenKind::is_word_like) {
4140                entries.push(self.parse_function_header_entry()?);
4141                if self.at(TokenKind::LeftParen) {
4142                    break;
4143                }
4144            }
4145
4146            let trailing_parens_span = if !entries.is_empty() && self.at(TokenKind::LeftParen) {
4147                Some(self.parse_function_parens_span()?)
4148            } else {
4149                None
4150            };
4151
4152            if entries.is_empty() {
4153                let body = self.parse_zsh_function_body_stmt()?;
4154                let args = self.parse_anonymous_function_args()?;
4155                let redirects = self.parse_trailing_redirects();
4156                let span = start_span.merge(self.current_span);
4157                return Ok(Command::AnonymousFunction(
4158                    AnonymousFunctionCommand {
4159                        surface: AnonymousFunctionSurface::FunctionKeyword {
4160                            function_keyword_span: start_span,
4161                        },
4162                        body: Box::new(body),
4163                        args: args.into_vec(),
4164                        span,
4165                    },
4166                    redirects,
4167                ));
4168            }
4169
4170            let body = self.parse_zsh_function_body_stmt()?;
4171            let span = start_span.merge(self.current_span);
4172            return Ok(Command::Function(FunctionDef {
4173                header: FunctionHeader {
4174                    function_keyword_span: Some(start_span),
4175                    entries,
4176                    trailing_parens_span,
4177                },
4178                body: Box::new(body),
4179                span,
4180            }));
4181        }
4182
4183        let entry = self.parse_function_keyword_header_entry()?;
4184        let saw_newline_after_name = self.skip_newlines_with_flag()?;
4185        let (trailing_parens_span, allow_bare_compound) = if self.at(TokenKind::LeftParen) {
4186            let parens_span = self.parse_function_parens_span()?;
4187            (Some(parens_span), self.skip_newlines_with_flag()?)
4188        } else {
4189            (None, saw_newline_after_name)
4190        };
4191
4192        let body = self.parse_function_body_command(allow_bare_compound)?;
4193        let span = start_span.merge(self.current_span);
4194
4195        Ok(Command::Function(FunctionDef {
4196            header: FunctionHeader {
4197                function_keyword_span: Some(start_span),
4198                entries: vec![entry],
4199                trailing_parens_span,
4200            },
4201            body: Box::new(body),
4202            span,
4203        }))
4204    }
4205
4206    /// Parse POSIX-style function definition: name() { body }
4207    fn parse_function_posix(&mut self) -> Result<Command> {
4208        let start_span = self.current_span;
4209        let entry = self.parse_function_header_entry()?;
4210        let trailing_parens_span = self.parse_function_parens_span()?;
4211
4212        self.finish_parse_function_posix(start_span, entry, trailing_parens_span)
4213    }
4214
4215    fn finish_parse_function_posix(
4216        &mut self,
4217        start_span: Span,
4218        entry: FunctionHeaderEntry,
4219        trailing_parens_span: Span,
4220    ) -> Result<Command> {
4221        let body = if self.dialect == ShellDialect::Zsh {
4222            self.parse_zsh_function_body_stmt()?
4223        } else {
4224            self.skip_newlines()?;
4225            self.parse_function_body_command(true)?
4226        };
4227
4228        Ok(Command::Function(FunctionDef {
4229            header: FunctionHeader {
4230                function_keyword_span: None,
4231                entries: vec![entry],
4232                trailing_parens_span: Some(trailing_parens_span),
4233            },
4234            body: Box::new(body),
4235            span: start_span.merge(self.current_span),
4236        }))
4237    }
4238
4239    fn try_parse_zsh_attached_parens_function(&mut self) -> Result<Option<Command>> {
4240        if self.dialect != ShellDialect::Zsh || !self.at_word_like() {
4241            return Ok(None);
4242        }
4243
4244        let Some(word_text) = self.current_source_like_word_text() else {
4245            return Ok(None);
4246        };
4247        let Some(header_text) = word_text.as_ref().strip_suffix("()") else {
4248            return Ok(None);
4249        };
4250        if header_text.is_empty() || header_text.contains('=') {
4251            return Ok(None);
4252        }
4253
4254        let checkpoint = self.checkpoint();
4255        self.advance();
4256        if let Err(error) = self.skip_newlines() {
4257            self.restore(checkpoint);
4258            return Err(error);
4259        }
4260        if !self.at(TokenKind::LeftBrace) {
4261            self.restore(checkpoint);
4262            return Ok(None);
4263        }
4264        self.restore(checkpoint);
4265
4266        let start_span = self.current_span;
4267        let header_span =
4268            Span::from_positions(start_span.start, start_span.start.advanced_by(header_text));
4269        let parens_span = Span::from_positions(header_span.end, start_span.end);
4270        let header_word =
4271            self.parse_word_with_context(header_text, header_span, header_span.start, true);
4272        let entry = self.function_header_entry_from_word(header_word);
4273        self.advance();
4274
4275        self.finish_parse_function_posix(start_span, entry, parens_span)
4276            .map(Some)
4277    }
4278
4279    fn parse_anonymous_paren_function(&mut self) -> Result<Command> {
4280        let start_span = self.current_span;
4281        let parens_span = self.parse_function_parens_span()?;
4282        let body = self.parse_zsh_function_body_stmt()?;
4283        let args = self.parse_anonymous_function_args()?;
4284        let redirects = self.parse_trailing_redirects();
4285        let span = start_span.merge(self.current_span);
4286        Ok(Command::AnonymousFunction(
4287            AnonymousFunctionCommand {
4288                surface: AnonymousFunctionSurface::Parens { parens_span },
4289                body: Box::new(body),
4290                args: args.into_vec(),
4291                span,
4292            },
4293            redirects,
4294        ))
4295    }
4296
4297    /// Parse commands until a terminating keyword
4298    fn parse_compound_list(&mut self, terminator: Keyword) -> Result<Vec<Stmt>> {
4299        self.parse_compound_list_until(KeywordSet::single(terminator))
4300    }
4301
4302    /// Parse commands until one of the terminating keywords
4303    fn parse_compound_list_until(&mut self, terminators: KeywordSet) -> Result<Vec<Stmt>> {
4304        let mut stmts = Vec::new();
4305
4306        loop {
4307            self.skip_command_separators()?;
4308
4309            // Check for terminators
4310            if self
4311                .current_keyword()
4312                .is_some_and(|keyword| terminators.contains(keyword))
4313            {
4314                break;
4315            }
4316
4317            if self.current_token.is_none() {
4318                break;
4319            }
4320
4321            let command_stmts = self.parse_command_list_required()?;
4322            self.apply_stmt_list_effects(&command_stmts);
4323            stmts.extend(command_stmts);
4324        }
4325
4326        Ok(stmts)
4327    }
4328
4329    /// Reserved words that cannot start a simple command.
4330    /// These words are only special in command position, not as arguments.
4331    /// Check if a word cannot start a command
4332    fn is_non_command_keyword(keyword: Keyword) -> bool {
4333        NON_COMMAND_KEYWORDS.contains(keyword)
4334    }
4335
4336    /// Check if current token is a specific keyword
4337    fn is_keyword(&self, keyword: Keyword) -> bool {
4338        self.current_keyword() == Some(keyword)
4339    }
4340
4341    /// Expect a specific keyword
4342    fn expect_keyword(&mut self, keyword: Keyword) -> Result<()> {
4343        if self.is_keyword(keyword) {
4344            self.advance();
4345            Ok(())
4346        } else {
4347            Err(self.error(format!("expected '{}'", keyword)))
4348        }
4349    }
4350    fn parse_simple_command(&mut self) -> Result<Option<SimpleCommand>> {
4351        self.tick()?;
4352        self.skip_newlines()?;
4353        self.check_error_token()?;
4354        let start_span = self.current_span;
4355
4356        let mut assignments = SmallVec::<[Assignment; 1]>::new();
4357        let mut words = SmallVec::<[Word; 2]>::new();
4358        let mut redirects = SmallVec::<[Redirect; 1]>::new();
4359
4360        loop {
4361            self.check_error_token()?;
4362            let next_kind_after_right_brace = if self.at(TokenKind::RightBrace) {
4363                self.peek_next_kind()
4364            } else {
4365                None
4366            };
4367            let right_brace_is_literal_argument = self.at(TokenKind::RightBrace)
4368                && !words.is_empty()
4369                && self.should_consume_right_brace_as_literal_argument(next_kind_after_right_brace);
4370            match self.current_token_kind {
4371                Some(kind) if kind.is_word_like() => {
4372                    // Bail out before touching word text when the token is a
4373                    // reserved word that cannot begin a simple command.
4374                    if words.is_empty()
4375                        && self
4376                            .current_keyword()
4377                            .is_some_and(Self::is_non_command_keyword)
4378                    {
4379                        break;
4380                    }
4381
4382                    let is_literal = kind == TokenKind::LiteralWord;
4383                    let word_text =
4384                        self.current_source_like_word_text_or_error("simple command word")?;
4385                    let assignment_shape = (!is_literal && words.is_empty())
4386                        .then(|| Self::is_assignment(word_text.as_ref()));
4387                    let assignment_shape = assignment_shape.flatten();
4388
4389                    // Check for assignment (only before the command name, not for literal words)
4390                    if words.is_empty()
4391                        && !is_literal
4392                        && let Some((assignment, needs_advance)) = self
4393                            .try_parse_assignment_with_shape(word_text.as_ref(), assignment_shape)
4394                    {
4395                        if needs_advance {
4396                            self.advance();
4397                        }
4398                        assignments.push(assignment);
4399                        continue;
4400                    }
4401
4402                    if words.is_empty()
4403                        && !is_literal
4404                        && assignment_shape.is_none()
4405                        && word_text.contains('[')
4406                        && let Some(assignment) =
4407                            self.try_parse_split_indexed_assignment_from_text()
4408                    {
4409                        assignments.push(assignment);
4410                        continue;
4411                    }
4412
4413                    // Handle compound array assignment in arg position:
4414                    // declare -a arr=(x y z) → arr=(x y z) as single arg
4415                    if word_text.ends_with('=') && !words.is_empty() {
4416                        let original_word = self.current_word_ref().cloned();
4417                        let saved_span = self.current_span;
4418                        self.advance();
4419                        if let Some(word) =
4420                            self.try_parse_compound_array_arg(word_text.as_ref(), saved_span)?
4421                        {
4422                            words.push(word);
4423                            continue;
4424                        }
4425                        // Not a compound assignment — treat as regular word
4426                        if let Some(word) = original_word {
4427                            words.push(word);
4428                        }
4429                        continue;
4430                    }
4431
4432                    if let Some(word) = self.take_current_word_and_advance() {
4433                        words.push(word);
4434                    }
4435                }
4436                Some(TokenKind::LeftParen) if !words.is_empty() => {
4437                    let Some(word) = self.take_current_word_and_advance() else {
4438                        break;
4439                    };
4440                    words.push(word);
4441                }
4442                Some(TokenKind::DoubleRightBracket)
4443                    if words.first().is_some_and(|word| {
4444                        matches!(
4445                            word.parts.as_slice(),
4446                            [WordPartNode {
4447                                kind: WordPart::ArithmeticExpansion {
4448                                    syntax: ArithmeticExpansionSyntax::DollarParenParen,
4449                                    ..
4450                                },
4451                                ..
4452                            }]
4453                        )
4454                    }) =>
4455                {
4456                    let span = self.current_span;
4457                    let word = self.word_from_raw_text(span.slice(self.input), span);
4458                    self.advance();
4459                    words.push(word);
4460                }
4461                Some(TokenKind::Newline) => {
4462                    let next_kind = self.peek_next_kind();
4463                    let supports_fd_var = next_kind.is_some_and(|kind| {
4464                        matches!(kind, TokenKind::HereDoc | TokenKind::HereDocStrip)
4465                            || Self::redirect_supports_fd_var(kind)
4466                    });
4467                    if supports_fd_var {
4468                        let (fd_var, fd_var_span) = self.pop_line_continuation_fd_var(&mut words);
4469                        if let Some(fd_var) = fd_var {
4470                            self.advance();
4471                            if matches!(
4472                                self.current_token_kind,
4473                                Some(TokenKind::HereDoc | TokenKind::HereDocStrip)
4474                            ) {
4475                                self.parse_heredoc_redirect(
4476                                    self.current_token_kind == Some(TokenKind::HereDocStrip),
4477                                    &mut redirects,
4478                                    Some(fd_var),
4479                                    fd_var_span,
4480                                )?;
4481                                continue;
4482                            }
4483
4484                            if self.consume_non_heredoc_redirect(
4485                                &mut redirects,
4486                                Some(fd_var),
4487                                fd_var_span,
4488                                true,
4489                            )? {
4490                                continue;
4491                            }
4492                        }
4493                    }
4494                    break;
4495                }
4496                Some(kind) if Self::is_redirect_kind(kind) => {
4497                    if matches!(kind, TokenKind::HereDoc | TokenKind::HereDocStrip) {
4498                        let (fd_var, fd_var_span) = if words
4499                            .last()
4500                            .is_some_and(|word| self.word_is_attached_to_current_token(word))
4501                        {
4502                            self.pop_fd_var(&mut words)
4503                        } else {
4504                            (None, None)
4505                        };
4506                        self.parse_heredoc_redirect(
4507                            kind == TokenKind::HereDocStrip,
4508                            &mut redirects,
4509                            fd_var,
4510                            fd_var_span,
4511                        )?;
4512                        continue;
4513                    }
4514
4515                    let (fd_var, fd_var_span) = if Self::redirect_supports_fd_var(kind) {
4516                        if words
4517                            .last()
4518                            .is_some_and(|word| self.word_is_attached_to_current_token(word))
4519                        {
4520                            self.pop_fd_var(&mut words)
4521                        } else {
4522                            (None, None)
4523                        }
4524                    } else {
4525                        (None, None)
4526                    };
4527
4528                    if self.consume_non_heredoc_redirect(
4529                        &mut redirects,
4530                        fd_var,
4531                        fd_var_span,
4532                        true,
4533                    )? {
4534                        continue;
4535                    }
4536                    break;
4537                }
4538                Some(TokenKind::ProcessSubIn) | Some(TokenKind::ProcessSubOut) => {
4539                    let word = self.expect_word()?;
4540                    words.push(word);
4541                }
4542                // `{` can appear as a literal argument outside command position.
4543                Some(TokenKind::LeftBrace) if !words.is_empty() => {
4544                    words.push(Word::literal_with_span("{", self.current_span));
4545                    self.advance();
4546                }
4547                // Inside brace groups, a bare `}` can still be a literal
4548                // argument like `echo }`, but only when it's separated from the
4549                // preceding token by whitespace. Closers are handled in command
4550                // position by the enclosing brace parser.
4551                Some(TokenKind::RightBrace) if right_brace_is_literal_argument => {
4552                    words.push(Word::literal_with_span("}", self.current_span));
4553                    self.advance();
4554                }
4555                Some(TokenKind::Semicolon)
4556                | Some(TokenKind::Pipe)
4557                | Some(TokenKind::And)
4558                | Some(TokenKind::Or)
4559                | None => break,
4560                _ => break,
4561            }
4562        }
4563
4564        // Handle assignment-only or redirect-only commands with no command word.
4565        if words.is_empty() && (!assignments.is_empty() || !redirects.is_empty()) {
4566            return Ok(Some(SimpleCommand {
4567                name: Word::literal(""),
4568                args: SmallVec::new(),
4569                redirects,
4570                assignments,
4571                span: start_span.merge(self.current_span),
4572            }));
4573        }
4574
4575        if words.is_empty() {
4576            return Ok(None);
4577        }
4578
4579        let name = words.remove(0);
4580        let args = words;
4581
4582        Ok(Some(SimpleCommand {
4583            name,
4584            args,
4585            redirects,
4586            assignments,
4587            span: start_span.merge(self.current_span),
4588        }))
4589    }
4590
4591    /// Extract fd-variable name from `{varname}` pattern in the last word.
4592    /// If the last word is a single literal `{identifier}`, pop it and return the name.
4593    /// Used for `exec {var}>file` / `exec {var}>&-` syntax.
4594    fn pop_fd_var(&self, words: &mut SmallVec<[Word; 2]>) -> (Option<Name>, Option<Span>) {
4595        if let Some(last) = words.last()
4596            && last.parts.len() == 1
4597            && let WordPart::Literal(ref s) = last.parts[0].kind
4598            && let Some(span) = last.part_span(0)
4599            && let text = s.as_str(self.input, span)
4600            && text.starts_with('{')
4601            && text.ends_with('}')
4602            && text.len() > 2
4603            && text[1..text.len() - 1]
4604                .chars()
4605                .all(|c| c.is_alphanumeric() || c == '_')
4606        {
4607            let var_name = text[1..text.len() - 1].to_string();
4608            let start = last.span.start.advanced_by("{");
4609            let span = Span::from_positions(start, start.advanced_by(&var_name));
4610            words.pop();
4611            return (Some(Name::from(var_name)), Some(span));
4612        }
4613        (None, None)
4614    }
4615
4616    fn word_is_attached_to_current_token(&self, word: &Word) -> bool {
4617        let start = word.span.end.offset;
4618        let end = self.current_span.start.offset;
4619        let input_len = self.input.len();
4620        start <= end
4621            && end <= input_len
4622            && Self::fd_var_gap_allows_attachment(&self.input[start..end])
4623    }
4624
4625    fn pop_line_continuation_fd_var(
4626        &self,
4627        words: &mut SmallVec<[Word; 2]>,
4628    ) -> (Option<Name>, Option<Span>) {
4629        let Some(last) = words.last() else {
4630            return (None, None);
4631        };
4632        let Some(text) = self.single_literal_word_text(last) else {
4633            return (None, None);
4634        };
4635        let Some(fd_text) = text.strip_suffix('\\') else {
4636            return (None, None);
4637        };
4638        let Some((fd_var, fd_var_span)) = Self::fd_var_from_text(fd_text, last.span) else {
4639            return (None, None);
4640        };
4641        words.pop();
4642        (Some(fd_var), Some(fd_var_span))
4643    }
4644}