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