Skip to main content

shuck_parser/parser/
commands.rs

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