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