Skip to main content

shuck_parser/parser/commands/
lists.rs

1use super::*;
2
3impl<'a> Parser<'a> {
4    pub(super) fn apply_word_command_effects(&mut self, name: &Word, args: &[Word]) {
5        let Some(name) = self.literal_word_text(name) else {
6            return;
7        };
8
9        match name.as_str() {
10            "shopt" => {
11                let mut toggle = None;
12                for arg in args {
13                    let Some(arg) = self.literal_word_text(arg) else {
14                        continue;
15                    };
16                    match arg.as_str() {
17                        "-s" => toggle = Some(true),
18                        "-u" => toggle = Some(false),
19                        "expand_aliases" => {
20                            if let Some(toggle) = toggle {
21                                self.expand_aliases = toggle;
22                            }
23                        }
24                        _ => {}
25                    }
26                }
27            }
28            "alias" => {
29                for arg in args {
30                    let Some(arg) = self.literal_word_text(arg) else {
31                        continue;
32                    };
33                    if arg == "--" {
34                        continue;
35                    }
36                    let Some((alias_name, value)) = arg.split_once('=') else {
37                        continue;
38                    };
39                    self.aliases
40                        .insert(alias_name.to_string(), self.compile_alias_definition(value));
41                }
42            }
43            "unalias" => {
44                for arg in args {
45                    let Some(arg) = self.literal_word_text(arg) else {
46                        continue;
47                    };
48                    match arg.as_str() {
49                        "--" => {}
50                        "-a" => self.aliases.clear(),
51                        _ => {
52                            self.aliases.remove(arg.as_str());
53                        }
54                    }
55                }
56            }
57            _ => {}
58        }
59    }
60
61    pub(super) fn apply_stmt_effects(&mut self, stmt: &Stmt) {
62        match &stmt.command {
63            AstCommand::Simple(simple) => {
64                self.apply_word_command_effects(&simple.name, &simple.args)
65            }
66            AstCommand::Binary(binary) if matches!(binary.op, BinaryOp::And | BinaryOp::Or) => {
67                self.apply_stmt_effects(&binary.left);
68                self.apply_stmt_effects(&binary.right);
69            }
70            _ => {}
71        }
72    }
73
74    pub(in crate::parser) fn apply_stmt_list_effects(&mut self, stmts: &[Stmt]) {
75        for stmt in stmts {
76            self.apply_stmt_effects(stmt);
77        }
78    }
79
80    pub(in crate::parser) fn parse_command_list_required(&mut self) -> Result<Vec<Stmt>> {
81        self.parse_command_list()?
82            .ok_or_else(|| self.error("expected command"))
83    }
84
85    pub(super) fn skip_command_separators(&mut self) -> Result<()> {
86        loop {
87            self.skip_newlines()?;
88            if self.at(TokenKind::Semicolon) {
89                self.advance();
90                continue;
91            }
92            break;
93        }
94        Ok(())
95    }
96
97    /// Parse the configured input.
98    ///
99    /// The returned [`ParseResult`] contains the best AST the parser could
100    /// produce, plus recovery diagnostics and syntax facts. Use
101    /// [`ParseResult::is_ok`] when a caller needs to reject recovered parses.
102    pub fn parse(mut self) -> ParseResult {
103        self.parse_impl()
104    }
105
106    #[cfg(feature = "benchmarking")]
107    #[doc(hidden)]
108    pub fn parse_with_benchmark_counters(self) -> (ParseResult, ParserBenchmarkCounters) {
109        let mut parser = self.rebuild_with_benchmark_counters();
110        let output = parser.parse_impl();
111        (output, parser.finish_benchmark_counters())
112    }
113
114    pub(super) fn parse_command_list(&mut self) -> Result<Option<Vec<Stmt>>> {
115        self.tick()?;
116        let mut current = match self.parse_pipeline()? {
117            Some(stmt) => stmt,
118            None => return Ok(None),
119        };
120
121        let mut stmts = Vec::with_capacity(2);
122
123        loop {
124            let (op, terminator, allow_empty_tail) = match self.current_token_kind {
125                Some(TokenKind::And) => (Some(BinaryOp::And), None, false),
126                Some(TokenKind::Or) => (Some(BinaryOp::Or), None, false),
127                Some(TokenKind::Semicolon) => (None, Some(StmtTerminator::Semicolon), true),
128                Some(TokenKind::Background) => (
129                    None,
130                    Some(StmtTerminator::Background(BackgroundOperator::Plain)),
131                    true,
132                ),
133                Some(TokenKind::BackgroundPipe) => (
134                    None,
135                    Some(StmtTerminator::Background(BackgroundOperator::Pipe)),
136                    true,
137                ),
138                Some(TokenKind::BackgroundBang) => (
139                    None,
140                    Some(StmtTerminator::Background(BackgroundOperator::Bang)),
141                    true,
142                ),
143                _ => break,
144            };
145            let operator_span = self.current_span;
146            self.advance();
147
148            self.skip_newlines()?;
149            if allow_empty_tail && self.current_token.is_none() {
150                current.terminator = terminator;
151                current.terminator_span = Some(operator_span);
152                stmts.push(current);
153                return Ok(Some(stmts));
154            }
155
156            if let Some(binary_op) = op {
157                if let Some(right) = self.parse_pipeline()? {
158                    current = Self::binary_stmt(current, binary_op, operator_span, right);
159                } else {
160                    break;
161                }
162                continue;
163            }
164
165            let Some(terminator) = terminator else {
166                unreachable!("list terminator should be present");
167            };
168            if let Some(next) = self.parse_pipeline()? {
169                current.terminator = Some(terminator);
170                current.terminator_span = Some(operator_span);
171                stmts.push(current);
172                current = next;
173            } else if allow_empty_tail {
174                if self
175                    .current_keyword()
176                    .is_some_and(Self::is_non_command_keyword)
177                {
178                    if matches!(terminator, StmtTerminator::Background(_)) {
179                        current.terminator = Some(terminator);
180                        current.terminator_span = Some(operator_span);
181                        stmts.push(current);
182                        return Ok(Some(stmts));
183                    }
184                    break;
185                }
186                if matches!(
187                    self.current_token_kind,
188                    Some(TokenKind::Semicolon | TokenKind::Newline)
189                ) {
190                    self.advance();
191                }
192                current.terminator = Some(terminator);
193                current.terminator_span = Some(operator_span);
194                stmts.push(current);
195                return Ok(Some(stmts));
196            } else {
197                break;
198            }
199        }
200
201        stmts.push(current);
202        Ok(Some(stmts))
203    }
204
205    /// Parse a pipeline (commands connected by |)
206    ///
207    /// Handles `!` pipeline negation: `! cmd | cmd2` negates the exit code.
208    pub(super) fn parse_pipeline(&mut self) -> Result<Option<Stmt>> {
209        let start_span = self.current_span;
210
211        // Check for pipeline negation: `! command`
212        let negated = self.at(TokenKind::Word) && self.current_word_str() == Some("!");
213        if negated {
214            self.advance();
215        }
216
217        let mut stmt = match self.parse_command()? {
218            Some(cmd) => Self::lower_non_sequence_command_to_stmt(cmd),
219            None => {
220                if negated {
221                    return Err(self.error("expected command after !"));
222                }
223                return Ok(None);
224            }
225        };
226
227        let mut saw_pipe = false;
228        while self.at_in_set(PIPE_OPERATOR_TOKENS) {
229            saw_pipe = true;
230            let op = if self.at(TokenKind::PipeBoth) {
231                BinaryOp::PipeAll
232            } else {
233                BinaryOp::Pipe
234            };
235            let operator_span = self.current_span;
236            self.advance();
237            self.skip_newlines()?;
238
239            if let Some(cmd) = self.parse_command()? {
240                let right = Self::lower_non_sequence_command_to_stmt(cmd);
241                stmt = Self::binary_stmt(stmt, op, operator_span, right);
242            } else {
243                return Err(self.error("expected command after |"));
244            }
245        }
246
247        if negated || saw_pipe {
248            stmt.negated = negated;
249            stmt.span = start_span.merge(self.current_span);
250        }
251        Ok(Some(stmt))
252    }
253
254    pub(super) fn parse_compound_with_redirects(
255        &mut self,
256        parser: impl FnOnce(&mut Self) -> Result<CompoundCommand>,
257    ) -> Result<Option<Command>> {
258        let compound = parser(self)?;
259        let redirects = self.parse_trailing_redirects();
260        Ok(Some(Command::Compound(Box::new(compound), redirects)))
261    }
262
263    pub(super) fn current_starts_prefix_redirect_compound(&self) -> bool {
264        match self.current_keyword() {
265            Some(Keyword::If)
266            | Some(Keyword::While)
267            | Some(Keyword::Until)
268            | Some(Keyword::Case)
269            | Some(Keyword::Select)
270            | Some(Keyword::Time)
271            | Some(Keyword::Coproc) => true,
272            Some(Keyword::For) => self.dialect == ShellDialect::Zsh,
273            Some(Keyword::Repeat) => self.zsh_short_repeat_enabled(),
274            Some(Keyword::Foreach) => self.zsh_short_loops_enabled(),
275            Some(Keyword::Function) => false,
276            None => matches!(
277                self.current_token_kind,
278                Some(TokenKind::DoubleLeftParen | TokenKind::LeftParen | TokenKind::LeftBrace)
279            ),
280            _ => false,
281        }
282    }
283
284    pub(super) fn parse_prefix_redirected_compound_command(&mut self) -> Result<Option<Command>> {
285        if !self.current_token_kind.is_some_and(Self::is_redirect_kind) {
286            return Ok(None);
287        }
288
289        let checkpoint = self.checkpoint();
290        let mut redirects = self.parse_trailing_redirects();
291        if redirects.is_empty() || !self.current_starts_prefix_redirect_compound() {
292            self.restore(checkpoint);
293            return Ok(None);
294        }
295
296        let Some(mut command) = self.parse_command()? else {
297            self.restore(checkpoint);
298            return Ok(None);
299        };
300
301        match &mut command {
302            Command::Compound(_, trailing) => {
303                redirects.append(trailing);
304                *trailing = redirects;
305                Ok(Some(command))
306            }
307            _ => {
308                self.restore(checkpoint);
309                Ok(None)
310            }
311        }
312    }
313
314    pub(super) fn classify_flow_control_name(&self, word: &Word) -> Option<FlowControlBuiltinKind> {
315        let name = self.single_literal_word_text(word)?;
316        match name {
317            "break" => Some(FlowControlBuiltinKind::Break),
318            "continue" => Some(FlowControlBuiltinKind::Continue),
319            "return" => Some(FlowControlBuiltinKind::Return),
320            "exit" => Some(FlowControlBuiltinKind::Exit),
321            _ => None,
322        }
323    }
324
325    pub(super) fn classify_decl_variant_name(&self, word: &Word) -> Option<Name> {
326        let name = self.single_literal_word_text(word)?;
327        match name {
328            "declare" | "local" | "export" | "readonly" | "typeset" => Some(Name::from(name)),
329            "integer" if self.dialect == ShellDialect::Zsh => Some(Name::from(name)),
330            _ => None,
331        }
332    }
333
334    pub(super) fn classify_simple_command(&mut self, command: SimpleCommand) -> Command {
335        let kind = self.classify_flow_control_name(&command.name);
336
337        if let Some(kind) = kind {
338            let SimpleCommand {
339                args,
340                redirects,
341                assignments,
342                span,
343                ..
344            } = command;
345            let mut args = args.into_iter();
346
347            return match kind {
348                FlowControlBuiltinKind::Break => {
349                    Command::Builtin(BuiltinCommand::Break(BreakCommand {
350                        depth: args.next(),
351                        extra_args: args.collect(),
352                        redirects,
353                        assignments,
354                        span,
355                    }))
356                }
357                FlowControlBuiltinKind::Continue => {
358                    Command::Builtin(BuiltinCommand::Continue(ContinueCommand {
359                        depth: args.next(),
360                        extra_args: args.collect(),
361                        redirects,
362                        assignments,
363                        span,
364                    }))
365                }
366                FlowControlBuiltinKind::Return => {
367                    Command::Builtin(BuiltinCommand::Return(ReturnCommand {
368                        code: args.next(),
369                        extra_args: args.collect(),
370                        redirects,
371                        assignments,
372                        span,
373                    }))
374                }
375                FlowControlBuiltinKind::Exit => {
376                    Command::Builtin(BuiltinCommand::Exit(ExitCommand {
377                        code: args.next(),
378                        extra_args: args.collect(),
379                        redirects,
380                        assignments,
381                        span,
382                    }))
383                }
384            };
385        }
386
387        if let Some(variant) = self.classify_decl_variant_name(&command.name) {
388            let SimpleCommand {
389                name,
390                args,
391                redirects,
392                assignments,
393                span,
394            } = command;
395            return Command::Decl(Box::new(DeclClause {
396                variant,
397                variant_span: name.span,
398                operands: self.classify_decl_operands(args),
399                redirects,
400                assignments,
401                span,
402            }));
403        }
404
405        Command::Simple(command)
406    }
407
408    pub(super) fn is_operand_like_double_paren_token(token: &LexedToken<'_>) -> bool {
409        match token.kind {
410            TokenKind::LiteralWord | TokenKind::QuotedWord => true,
411            TokenKind::Word => token.word_string().is_some_and(|text| {
412                !text.chars().all(|ch| ch.is_ascii_punctuation())
413                    && !Self::word_contains_obvious_arithmetic_punctuation(&text)
414            }),
415            _ => false,
416        }
417    }
418
419    pub(super) fn word_contains_obvious_arithmetic_punctuation(text: &str) -> bool {
420        text.chars().any(|ch| {
421            matches!(
422                ch,
423                ',' | '='
424                    | '+'
425                    | '*'
426                    | '/'
427                    | '%'
428                    | '<'
429                    | '>'
430                    | '&'
431                    | '|'
432                    | '^'
433                    | '!'
434                    | '?'
435                    | ':'
436                    | '['
437                    | ']'
438            )
439        })
440    }
441
442    pub(super) fn suspicious_double_paren_is_command_style(
443        &mut self,
444        checkpoint: &ParserCheckpoint<'a>,
445    ) -> bool {
446        self.restore(checkpoint.clone());
447        let parses_as_arithmetic = self.parse_arithmetic_command().is_ok();
448        self.restore(checkpoint.clone());
449        !parses_as_arithmetic
450    }
451
452    pub(super) fn looks_like_command_style_double_paren(&mut self) -> bool {
453        if self.current_token_kind != Some(TokenKind::DoubleLeftParen) {
454            return false;
455        }
456
457        let checkpoint = self.checkpoint();
458        self.advance();
459        let mut paren_depth = 0_i32;
460        let mut previous_top_level_operand = false;
461
462        loop {
463            match self.current_token_kind {
464                Some(TokenKind::DoubleLeftParen) => {
465                    paren_depth += 2;
466                    previous_top_level_operand = false;
467                    self.advance();
468                }
469                Some(TokenKind::LeftParen) => {
470                    paren_depth += 1;
471                    previous_top_level_operand = false;
472                    self.advance();
473                }
474                Some(TokenKind::DoubleRightParen) => {
475                    if paren_depth == 0 {
476                        self.restore(checkpoint);
477                        return false;
478                    }
479                    if paren_depth == 1 {
480                        self.restore(checkpoint);
481                        return false;
482                    }
483                    paren_depth -= 2;
484                    previous_top_level_operand = false;
485                    self.advance();
486                }
487                Some(TokenKind::RightParen) => {
488                    if paren_depth == 0 {
489                        return self.suspicious_double_paren_is_command_style(&checkpoint);
490                    }
491                    paren_depth -= 1;
492                    previous_top_level_operand = false;
493                    self.advance();
494                }
495                Some(TokenKind::Newline) | Some(TokenKind::Semicolon) if paren_depth == 0 => {
496                    previous_top_level_operand = false;
497                    self.advance();
498                }
499                Some(TokenKind::Comment) if self.dialect == ShellDialect::Zsh => {
500                    self.restore(checkpoint);
501                    return false;
502                }
503                Some(_)
504                    if paren_depth == 0
505                        && self
506                            .current_token
507                            .as_ref()
508                            .is_some_and(Self::is_operand_like_double_paren_token) =>
509                {
510                    if previous_top_level_operand {
511                        return self.suspicious_double_paren_is_command_style(&checkpoint);
512                    }
513                    previous_top_level_operand = true;
514                    self.advance();
515                }
516                Some(_) => {
517                    previous_top_level_operand = false;
518                    self.advance();
519                }
520                None => {
521                    self.restore(checkpoint);
522                    return false;
523                }
524            }
525        }
526    }
527
528    pub(super) fn split_current_double_left_paren(&mut self) {
529        let (left_span, right_span) = Self::split_double_left_paren(self.current_span);
530        self.set_current_kind(TokenKind::LeftParen, left_span);
531        self.synthetic_tokens
532            .push_front(SyntheticToken::punctuation(
533                TokenKind::LeftParen,
534                right_span,
535            ));
536    }
537
538    pub(in crate::parser) fn split_current_double_right_paren(&mut self) {
539        let (left_span, right_span) = Self::split_double_right_paren(self.current_span);
540        self.set_current_kind(TokenKind::RightParen, left_span);
541        self.synthetic_tokens
542            .push_front(SyntheticToken::punctuation(
543                TokenKind::RightParen,
544                right_span,
545            ));
546    }
547
548    /// Parse a single command (simple or compound)
549    pub(super) fn parse_command(&mut self) -> Result<Option<Command>> {
550        self.skip_newlines()?;
551        self.check_error_token()?;
552        self.maybe_expand_current_alias_chain();
553        self.check_error_token()?;
554
555        if !self.zsh_short_repeat_enabled() && self.looks_like_disabled_repeat_loop()? {
556            self.ensure_repeat_loop()?;
557        }
558        if !self.zsh_short_loops_enabled() && self.looks_like_disabled_foreach_loop()? {
559            self.ensure_foreach_loop()?;
560        }
561
562        if let Some(command) = self.parse_prefix_redirected_compound_command()? {
563            return Ok(Some(command));
564        }
565
566        if let Some(command) = self.try_parse_zsh_attached_parens_function()? {
567            return Ok(Some(command));
568        }
569
570        // Check for compound commands and function keyword
571        match self.current_keyword() {
572            Some(Keyword::If) => return self.parse_compound_with_redirects(|s| s.parse_if()),
573            Some(Keyword::For) => return self.parse_compound_with_redirects(|s| s.parse_for()),
574            Some(Keyword::Repeat) if self.zsh_short_repeat_enabled() => {
575                return self.parse_compound_with_redirects(|s| s.parse_repeat());
576            }
577            Some(Keyword::Foreach) if self.zsh_short_loops_enabled() => {
578                return self.parse_compound_with_redirects(|s| s.parse_foreach());
579            }
580            Some(Keyword::While) => {
581                return self.parse_compound_with_redirects(|s| s.parse_while());
582            }
583            Some(Keyword::Until) => {
584                return self.parse_compound_with_redirects(|s| s.parse_until());
585            }
586            Some(Keyword::Case) => return self.parse_compound_with_redirects(|s| s.parse_case()),
587            Some(Keyword::Select) => {
588                return self.parse_compound_with_redirects(|s| s.parse_select());
589            }
590            Some(Keyword::Time) => return self.parse_compound_with_redirects(|s| s.parse_time()),
591            Some(Keyword::Coproc) => {
592                return self.parse_compound_with_redirects(|s| s.parse_coproc());
593            }
594            Some(Keyword::Function) => return self.parse_function_keyword().map(Some),
595            _ => {}
596        }
597
598        if self.at(TokenKind::Word)
599            && let Some(word) = self.current_source_like_word_text()
600            && self.peek_next_is(TokenKind::LeftParen)
601        {
602            let checkpoint = self.checkpoint();
603            self.advance();
604            self.advance();
605            let is_right_paren = self.at(TokenKind::RightParen);
606            self.restore(checkpoint);
607            if is_right_paren {
608                // Check for POSIX-style function: name() { body }
609                // Exclude obvious assignment-like heads such as `a[(1+2)*3]=9`.
610                if !word.contains('=') && !word.contains('[') {
611                    return self.parse_function_posix().map(Some);
612                }
613            } else if word.contains('$') && !word.contains('=') {
614                return Err(self.error("unexpected '(' after command word"));
615            }
616        }
617
618        // Check for conditional expression [[ ... ]]
619        if self.at(TokenKind::DoubleLeftBracket) {
620            return self.parse_compound_with_redirects(|s| s.parse_conditional());
621        }
622
623        // Check for arithmetic command ((expression))
624        if self.at(TokenKind::DoubleLeftParen) {
625            if self.looks_like_command_style_double_paren() {
626                self.split_current_double_left_paren();
627                return self.parse_compound_with_redirects(|s| s.parse_subshell());
628            }
629
630            let checkpoint = self.checkpoint();
631            if let Ok(compound) = self.parse_arithmetic_command() {
632                let redirects = self.parse_trailing_redirects();
633                return Ok(Some(Command::Compound(Box::new(compound), redirects)));
634            }
635            self.restore(checkpoint);
636
637            self.split_current_double_left_paren();
638            return self.parse_compound_with_redirects(|s| s.parse_subshell());
639        }
640
641        if self.dialect == ShellDialect::Zsh && self.at(TokenKind::LeftParen) {
642            let checkpoint = self.checkpoint();
643            self.advance();
644            let is_right_paren = self.at(TokenKind::RightParen);
645            self.restore(checkpoint);
646            if is_right_paren {
647                return self.parse_anonymous_paren_function().map(Some);
648            }
649        }
650
651        // Check for subshell
652        if self.at(TokenKind::LeftParen) {
653            return self.parse_compound_with_redirects(|s| s.parse_subshell());
654        }
655
656        // Check for brace group
657        if self.at(TokenKind::LeftBrace) {
658            return self.parse_compound_with_redirects(|s| {
659                s.parse_brace_group(BraceBodyContext::Ordinary)
660            });
661        }
662
663        // Default to simple command
664        match self.parse_simple_command()? {
665            Some(cmd) => Ok(Some(self.classify_simple_command(cmd))),
666            None => Ok(None),
667        }
668    }
669}