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