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