shuck_parser/parser/commands/
lists.rs1use 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 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 pub(super) fn parse_pipeline(&mut self) -> Result<Option<Stmt>> {
209 let start_span = self.current_span;
210
211 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 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 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 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 if self.at(TokenKind::DoubleLeftBracket) {
620 return self.parse_compound_with_redirects(|s| s.parse_conditional());
621 }
622
623 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 if self.at(TokenKind::LeftParen) {
653 return self.parse_compound_with_redirects(|s| s.parse_subshell());
654 }
655
656 if self.at(TokenKind::LeftBrace) {
658 return self.parse_compound_with_redirects(|s| {
659 s.parse_brace_group(BraceBodyContext::Ordinary)
660 });
661 }
662
663 match self.parse_simple_command()? {
665 Some(cmd) => Ok(Some(self.classify_simple_command(cmd))),
666 None => Ok(None),
667 }
668 }
669}