1#![allow(clippy::unwrap_used)]
15
16mod ast;
17pub mod budget;
18mod lexer;
19mod span;
20mod tokens;
21
22pub use ast::*;
23pub use budget::{BudgetError, validate as validate_budget};
24pub use lexer::{Lexer, SpannedToken};
25pub use span::{Position, Span};
26
27use crate::error::{Error, Result};
28use crate::limits::LimitExceeded;
29use crate::time_compat::Instant;
30use std::time::Duration;
31
32const DEFAULT_MAX_AST_DEPTH: usize = 100;
34
35const HARD_MAX_AST_DEPTH: usize = 100;
45
46const DEFAULT_MAX_PARSER_OPERATIONS: usize = 100_000;
48
49pub struct Parser<'a> {
51 input: &'a str,
52 lexer: Lexer<'a>,
53 current_token: Option<tokens::Token>,
54 current_span: Span,
56 peeked_token: Option<SpannedToken>,
58 max_depth: usize,
60 current_depth: usize,
62 fuel: usize,
64 max_fuel: usize,
66 timeout: Option<Duration>,
68 started_at: Instant,
70}
71
72impl<'a> Parser<'a> {
73 pub fn new(input: &'a str) -> Self {
75 Self::with_limits(input, DEFAULT_MAX_AST_DEPTH, DEFAULT_MAX_PARSER_OPERATIONS)
76 }
77
78 pub fn with_max_depth(input: &'a str, max_depth: usize) -> Self {
80 Self::with_limits(input, max_depth, DEFAULT_MAX_PARSER_OPERATIONS)
81 }
82
83 pub fn with_fuel(input: &'a str, max_fuel: usize) -> Self {
85 Self::with_limits(input, DEFAULT_MAX_AST_DEPTH, max_fuel)
86 }
87
88 pub fn with_limits(input: &'a str, max_depth: usize, max_fuel: usize) -> Self {
94 Self::with_limits_and_timeout(input, max_depth, max_fuel, None)
95 }
96
97 pub fn with_limits_and_timeout(
99 input: &'a str,
100 max_depth: usize,
101 max_fuel: usize,
102 timeout: Option<Duration>,
103 ) -> Self {
104 let mut lexer = Lexer::with_max_subst_depth(input, max_depth.min(HARD_MAX_AST_DEPTH));
105 let spanned = lexer.next_spanned_token();
106 let (current_token, current_span) = match spanned {
107 Some(st) => (Some(st.token), st.span),
108 None => (None, Span::new()),
109 };
110 Self {
111 input,
112 lexer,
113 current_token,
114 current_span,
115 peeked_token: None,
116 max_depth: max_depth.min(HARD_MAX_AST_DEPTH),
117 current_depth: 0,
118 fuel: max_fuel,
119 max_fuel,
120 timeout,
121 started_at: Instant::now(),
122 }
123 }
124
125 pub fn current_span(&self) -> Span {
127 self.current_span
128 }
129
130 pub fn parse_word_string(input: &str) -> Word {
133 let parser = Parser::new(input);
134 parser.parse_word(input.to_string())
135 }
136
137 pub fn parse_word_string_with_limits(input: &str, max_depth: usize, max_fuel: usize) -> Word {
140 let parser = Parser::with_limits(input, max_depth, max_fuel);
141 parser.parse_word(input.to_string())
142 }
143
144 fn error(&self, message: impl Into<String>) -> Error {
146 Error::parse_at(
147 message,
148 self.current_span.start.line,
149 self.current_span.start.column,
150 )
151 }
152
153 fn current_command_end_offset(&self) -> usize {
154 if self.current_token.is_some() {
155 self.current_span.start.offset
156 } else {
157 self.current_span.end.offset
161 }
162 }
163
164 fn source_slice(&self, start_offset: usize, end_offset: usize) -> Option<String> {
165 self.input.get(start_offset..end_offset).map(str::to_owned)
166 }
167
168 fn tick(&mut self) -> Result<()> {
170 if let Some(timeout) = self.timeout
171 && self.started_at.elapsed() > timeout
172 {
173 return Err(Error::ResourceLimit(LimitExceeded::ParserTimeout(timeout)));
174 }
175 if self.fuel == 0 {
176 let used = self.max_fuel;
177 return Err(Error::parse(format!(
178 "parser fuel exhausted ({} operations, max {})",
179 used, self.max_fuel
180 )));
181 }
182 self.fuel -= 1;
183 Ok(())
184 }
185
186 fn tick_units(&mut self, units: usize) -> Result<()> {
191 if let Some(timeout) = self.timeout
192 && self.started_at.elapsed() > timeout
193 {
194 return Err(Error::ResourceLimit(LimitExceeded::ParserTimeout(timeout)));
195 }
196 if self.fuel < units {
197 let used = self.max_fuel;
198 return Err(Error::parse(format!(
199 "parser fuel exhausted ({} operations, max {})",
200 used, self.max_fuel
201 )));
202 }
203 self.fuel -= units;
204 Ok(())
205 }
206
207 fn push_depth(&mut self) -> Result<()> {
209 self.current_depth += 1;
210 if self.current_depth > self.max_depth {
211 return Err(Error::parse(format!(
212 "AST nesting too deep ({} levels, max {})",
213 self.current_depth, self.max_depth
214 )));
215 }
216 Ok(())
217 }
218
219 fn pop_depth(&mut self) {
221 if self.current_depth > 0 {
222 self.current_depth -= 1;
223 }
224 }
225
226 fn check_error_token(&self) -> Result<()> {
228 if let Some(tokens::Token::Error(msg)) = &self.current_token {
229 return Err(self.error(format!("syntax error: {}", msg)));
230 }
231 Ok(())
232 }
233
234 pub fn parse(mut self) -> Result<Script> {
236 self.parse_script()
237 }
238
239 fn parse_script(&mut self) -> Result<Script> {
240 self.check_error_token()?;
242
243 let start_span = self.current_span;
244 let mut commands = Vec::new();
245
246 while self.current_token.is_some() {
247 self.tick()?;
248 self.skip_newlines()?;
249 self.check_error_token()?;
250 if self.current_token.is_none() {
251 break;
252 }
253 let start_offset = self.current_span.start.offset;
254 if let Some(cmd) = self.parse_command_list()? {
255 commands.push(cmd);
256 } else if self.current_token.is_some() && self.current_span.start.offset == start_offset
257 {
258 return Err(self.error("unexpected token"));
259 }
260 }
261
262 let end_span = self.current_span;
263 Ok(Script {
264 commands,
265 span: start_span.merge(end_span),
266 })
267 }
268
269 fn advance(&mut self) {
270 if let Some(peeked) = self.peeked_token.take() {
271 self.current_token = Some(peeked.token);
272 self.current_span = peeked.span;
273 } else {
274 match self.lexer.next_spanned_token() {
275 Some(st) => {
276 self.current_token = Some(st.token);
277 self.current_span = st.span;
278 }
279 None => {
280 self.current_token = None;
281 }
283 }
284 }
285 }
286
287 fn peek_next(&mut self) -> Option<&tokens::Token> {
289 if self.peeked_token.is_none() {
290 self.peeked_token = self.lexer.next_spanned_token();
291 }
292 self.peeked_token.as_ref().map(|st| &st.token)
293 }
294
295 fn skip_newlines(&mut self) -> Result<()> {
296 while matches!(self.current_token, Some(tokens::Token::Newline)) {
297 self.tick()?;
298 self.advance();
299 }
300 Ok(())
301 }
302
303 fn parse_command_list(&mut self) -> Result<Option<Command>> {
305 self.tick()?;
306 match self.current_token {
307 Some(tokens::Token::Pipe) => return Err(self.error("unexpected token: |")),
308 Some(tokens::Token::And) => return Err(self.error("unexpected token: &&")),
309 Some(tokens::Token::Or) => return Err(self.error("unexpected token: ||")),
310 _ => {}
311 }
312 let start_span = self.current_span;
313 let first = match self.parse_pipeline()? {
314 Some(cmd) => cmd,
315 None => return Ok(None),
316 };
317
318 let mut rest = Vec::new();
319
320 loop {
321 let op = match &self.current_token {
322 Some(tokens::Token::And) => {
323 self.advance();
324 ListOperator::And
325 }
326 Some(tokens::Token::Or) => {
327 self.advance();
328 ListOperator::Or
329 }
330 Some(tokens::Token::Semicolon) => {
331 self.advance();
332 self.skip_newlines()?;
333 if self.current_token.is_none()
335 || matches!(self.current_token, Some(tokens::Token::Newline))
336 {
337 break;
338 }
339 ListOperator::Semicolon
340 }
341 Some(tokens::Token::Background) => {
342 self.advance();
343 self.skip_newlines()?;
344 if self.current_token.is_none()
346 || matches!(self.current_token, Some(tokens::Token::Newline))
347 {
348 rest.push((
350 ListOperator::Background,
351 Command::Simple(SimpleCommand {
352 name: Word::literal(""),
353 args: vec![],
354 redirects: vec![],
355 assignments: vec![],
356 span: self.current_span,
357 }),
358 ));
359 break;
360 }
361 ListOperator::Background
362 }
363 _ => break,
364 };
365
366 self.skip_newlines()?;
367
368 if let Some(cmd) = self.parse_pipeline()? {
369 rest.push((op, cmd));
370 } else {
371 break;
372 }
373 }
374
375 if rest.is_empty() {
376 Ok(Some(first))
377 } else {
378 Ok(Some(Command::List(CommandList {
379 first: Box::new(first),
380 rest,
381 span: start_span.merge(self.current_span),
382 })))
383 }
384 }
385
386 fn parse_pipeline(&mut self) -> Result<Option<Command>> {
390 let start_span = self.current_span;
391
392 let negated = match &self.current_token {
394 Some(tokens::Token::Word(w)) if w == "!" => {
395 self.advance();
396 true
397 }
398 _ => false,
399 };
400
401 let first = match self.parse_command()? {
402 Some(cmd) => cmd,
403 None => {
404 if negated {
405 return Err(self.error("expected command after !"));
406 }
407 return Ok(None);
408 }
409 };
410
411 let mut commands = vec![first];
412
413 while matches!(self.current_token, Some(tokens::Token::Pipe)) {
414 self.advance();
415 self.skip_newlines()?;
416
417 if let Some(cmd) = self.parse_command()? {
418 commands.push(cmd);
419 } else {
420 return Err(self.error("expected command after |"));
421 }
422 }
423
424 if commands.len() == 1 && !negated {
425 Ok(Some(commands.remove(0)))
426 } else {
427 Ok(Some(Command::Pipeline(Pipeline {
428 negated,
429 commands,
430 span: start_span.merge(self.current_span),
431 })))
432 }
433 }
434
435 fn parse_trailing_redirects(&mut self) -> Result<Vec<Redirect>> {
437 let mut redirects = Vec::new();
438 loop {
439 match &self.current_token {
440 Some(tokens::Token::RedirectOut) | Some(tokens::Token::Clobber) => {
441 let kind = if matches!(&self.current_token, Some(tokens::Token::Clobber)) {
442 RedirectKind::Clobber
443 } else {
444 RedirectKind::Output
445 };
446 self.advance();
447 if let Ok(target) = self.expect_word() {
448 redirects.push(Redirect {
449 fd: None,
450 fd_var: None,
451 kind,
452 target,
453 });
454 }
455 }
456 Some(tokens::Token::RedirectAppend) => {
457 self.advance();
458 if let Ok(target) = self.expect_word() {
459 redirects.push(Redirect {
460 fd: None,
461 fd_var: None,
462 kind: RedirectKind::Append,
463 target,
464 });
465 }
466 }
467 Some(tokens::Token::RedirectIn) => {
468 self.advance();
469 if let Ok(target) = self.expect_word() {
470 redirects.push(Redirect {
471 fd: None,
472 fd_var: None,
473 kind: RedirectKind::Input,
474 target,
475 });
476 }
477 }
478 Some(tokens::Token::RedirectBoth) => {
479 self.advance();
480 if let Ok(target) = self.expect_word() {
481 redirects.push(Redirect {
482 fd: None,
483 fd_var: None,
484 kind: RedirectKind::OutputBoth,
485 target,
486 });
487 }
488 }
489 Some(tokens::Token::DupOutput) => {
490 self.advance();
491 if let Ok(target) = self.expect_word() {
492 redirects.push(Redirect {
493 fd: Some(1),
494 fd_var: None,
495 kind: RedirectKind::DupOutput,
496 target,
497 });
498 }
499 }
500 Some(tokens::Token::RedirectFd(fd)) => {
501 let fd = *fd;
502 self.advance();
503 if let Ok(target) = self.expect_word() {
504 redirects.push(Redirect {
505 fd: Some(fd),
506 fd_var: None,
507 kind: RedirectKind::Output,
508 target,
509 });
510 }
511 }
512 Some(tokens::Token::RedirectFdAppend(fd)) => {
513 let fd = *fd;
514 self.advance();
515 if let Ok(target) = self.expect_word() {
516 redirects.push(Redirect {
517 fd: Some(fd),
518 fd_var: None,
519 kind: RedirectKind::Append,
520 target,
521 });
522 }
523 }
524 Some(tokens::Token::DupFd(src_fd, dst_fd)) => {
525 let src_fd = *src_fd;
526 let dst_fd = *dst_fd;
527 self.advance();
528 redirects.push(Redirect {
529 fd: Some(src_fd),
530 fd_var: None,
531 kind: RedirectKind::DupOutput,
532 target: Word::literal(dst_fd.to_string()),
533 });
534 }
535 Some(tokens::Token::DupFdCloseOut(fd)) => {
536 let fd = *fd;
537 self.advance();
538 redirects.push(Redirect {
539 fd: Some(fd),
540 fd_var: None,
541 kind: RedirectKind::DupOutput,
542 target: Word::literal("-"),
543 });
544 }
545 Some(tokens::Token::DupInput) => {
546 self.advance();
547 if let Ok(target) = self.expect_word() {
548 redirects.push(Redirect {
549 fd: Some(0),
550 fd_var: None,
551 kind: RedirectKind::DupInput,
552 target,
553 });
554 }
555 }
556 Some(tokens::Token::DupFdIn(src_fd, dst_fd)) => {
557 let src_fd = *src_fd;
558 let dst_fd = *dst_fd;
559 self.advance();
560 redirects.push(Redirect {
561 fd: Some(src_fd),
562 fd_var: None,
563 kind: RedirectKind::DupInput,
564 target: Word::literal(dst_fd.to_string()),
565 });
566 }
567 Some(tokens::Token::DupFdClose(fd)) => {
568 let fd = *fd;
569 self.advance();
570 redirects.push(Redirect {
571 fd: Some(fd),
572 fd_var: None,
573 kind: RedirectKind::DupInput,
574 target: Word::literal("-"),
575 });
576 }
577 Some(tokens::Token::RedirectFdIn(fd)) => {
578 let fd = *fd;
579 self.advance();
580 if let Ok(target) = self.expect_word() {
581 redirects.push(Redirect {
582 fd: Some(fd),
583 fd_var: None,
584 kind: RedirectKind::Input,
585 target,
586 });
587 }
588 }
589 Some(tokens::Token::HereString) => {
590 self.advance();
591 if let Ok(target) = self.expect_word() {
592 redirects.push(Redirect {
593 fd: None,
594 fd_var: None,
595 kind: RedirectKind::HereString,
596 target,
597 });
598 }
599 }
600 Some(tokens::Token::HereDoc) | Some(tokens::Token::HereDocStrip) => {
601 let strip_tabs =
602 matches!(self.current_token, Some(tokens::Token::HereDocStrip));
603 self.advance();
604 let (delimiter, quoted) = match &self.current_token {
605 Some(tokens::Token::Word(w)) => (w.clone(), false),
606 Some(tokens::Token::LiteralWord(w)) => (w.clone(), true),
607 Some(tokens::Token::QuotedWord(w))
608 | Some(tokens::Token::QuotedGlobWord(w)) => (w.clone(), true),
609 _ => break,
610 };
611 let (content, rest_of_line_chars) = self
612 .lexer
613 .read_heredoc_with_strip_metered(&delimiter, strip_tabs);
614 self.tick_units(rest_of_line_chars)?;
615 let content = if strip_tabs {
616 let had_trailing_newline = content.ends_with('\n');
617 let mut stripped: String = content
618 .lines()
619 .map(|l| l.trim_start_matches('\t'))
620 .collect::<Vec<_>>()
621 .join("\n");
622 if had_trailing_newline {
623 stripped.push('\n');
624 }
625 stripped
626 } else {
627 content
628 };
629 self.advance();
630 let target = if quoted {
631 Word::quoted_literal(content)
632 } else {
633 self.parse_word(content)
634 };
635 let kind = if strip_tabs {
636 RedirectKind::HereDocStrip
637 } else {
638 RedirectKind::HereDoc
639 };
640 redirects.push(Redirect {
641 fd: None,
642 fd_var: None,
643 kind,
644 target,
645 });
646 break;
649 }
650 _ => break,
651 }
652 }
653 Ok(redirects)
654 }
655
656 fn parse_compound_with_redirects(
658 &mut self,
659 parser: impl FnOnce(&mut Self) -> Result<CompoundCommand>,
660 ) -> Result<Option<Command>> {
661 let compound = parser(self)?;
662 let redirects = self.parse_trailing_redirects()?;
663 Ok(Some(Command::Compound(compound, redirects)))
664 }
665
666 fn parse_command(&mut self) -> Result<Option<Command>> {
668 self.skip_newlines()?;
669 self.check_error_token()?;
670
671 if let Some(tokens::Token::Word(w)) = &self.current_token {
673 let word = w.clone();
674 match word.as_str() {
675 "if" => return self.parse_compound_with_redirects(|s| s.parse_if()),
676 "for" => return self.parse_compound_with_redirects(|s| s.parse_for()),
677 "while" => return self.parse_compound_with_redirects(|s| s.parse_while()),
678 "until" => return self.parse_compound_with_redirects(|s| s.parse_until()),
679 "case" => return self.parse_compound_with_redirects(|s| s.parse_case()),
680 "select" => return self.parse_compound_with_redirects(|s| s.parse_select()),
681 "time" => return self.parse_compound_with_redirects(|s| s.parse_time()),
682 "coproc" => return self.parse_compound_with_redirects(|s| s.parse_coproc()),
683 "function" => return self.parse_function_keyword().map(Some),
684 _ => {
685 if !word.contains('=')
688 && matches!(self.peek_next(), Some(tokens::Token::LeftParen))
689 {
690 return self.parse_function_posix().map(Some);
691 }
692 }
693 }
694 }
695
696 if matches!(self.current_token, Some(tokens::Token::DoubleLeftBracket)) {
698 return self.parse_compound_with_redirects(|s| s.parse_conditional());
699 }
700
701 if matches!(self.current_token, Some(tokens::Token::DoubleLeftParen)) {
703 return self.parse_compound_with_redirects(|s| s.parse_arithmetic_command());
704 }
705
706 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
708 return self.parse_compound_with_redirects(|s| s.parse_subshell());
709 }
710
711 if matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
713 return self.parse_compound_with_redirects(|s| s.parse_brace_group());
714 }
715
716 match self.parse_simple_command()? {
718 Some(cmd) => Ok(Some(Command::Simple(cmd))),
719 None => Ok(None),
720 }
721 }
722
723 fn parse_if(&mut self) -> Result<CompoundCommand> {
725 let start_span = self.current_span;
726 self.push_depth()?;
727 self.advance(); self.skip_newlines()?;
729
730 let condition = self.parse_compound_list("then")?;
732
733 self.expect_keyword("then")?;
735 self.skip_newlines()?;
736
737 let then_branch = self.parse_compound_list_until(&["elif", "else", "fi"])?;
739
740 if then_branch.is_empty() {
742 self.pop_depth();
743 return Err(self.error("syntax error: empty then clause"));
744 }
745
746 let mut elif_branches = Vec::new();
748 while self.is_keyword("elif") {
749 self.advance(); self.skip_newlines()?;
751
752 let elif_condition = self.parse_compound_list("then")?;
753 self.expect_keyword("then")?;
754 self.skip_newlines()?;
755
756 let elif_body = self.parse_compound_list_until(&["elif", "else", "fi"])?;
757
758 if elif_body.is_empty() {
760 self.pop_depth();
761 return Err(self.error("syntax error: empty elif clause"));
762 }
763
764 elif_branches.push((elif_condition, elif_body));
765 }
766
767 let else_branch = if self.is_keyword("else") {
769 self.advance(); self.skip_newlines()?;
771 let branch = self.parse_compound_list("fi")?;
772
773 if branch.is_empty() {
775 self.pop_depth();
776 return Err(self.error("syntax error: empty else clause"));
777 }
778
779 Some(branch)
780 } else {
781 None
782 };
783
784 self.expect_keyword("fi")?;
786
787 self.pop_depth();
788 Ok(CompoundCommand::If(IfCommand {
789 condition,
790 then_branch,
791 elif_branches,
792 else_branch,
793 span: start_span.merge(self.current_span),
794 }))
795 }
796
797 fn parse_for(&mut self) -> Result<CompoundCommand> {
799 let start_span = self.current_span;
800 self.push_depth()?;
801 self.advance(); self.skip_newlines()?;
803
804 if matches!(self.current_token, Some(tokens::Token::DoubleLeftParen)) {
806 let result = self.parse_arithmetic_for_inner(start_span);
807 self.pop_depth();
808 return result;
809 }
810
811 let variable = match &self.current_token {
813 Some(tokens::Token::Word(w))
814 | Some(tokens::Token::LiteralWord(w))
815 | Some(tokens::Token::QuotedWord(w))
816 | Some(tokens::Token::QuotedGlobWord(w)) => w.clone(),
817 _ => {
818 self.pop_depth();
819 return Err(Error::parse(
820 "expected variable name in for loop".to_string(),
821 ));
822 }
823 };
824 self.advance();
825
826 let words = if self.is_keyword("in") {
828 self.advance(); let mut words = Vec::new();
832 loop {
833 match &self.current_token {
834 Some(tokens::Token::Word(w))
839 | Some(tokens::Token::QuotedWord(w))
840 | Some(tokens::Token::QuotedGlobWord(w)) => {
841 let is_quoted = matches!(
842 &self.current_token,
843 Some(tokens::Token::QuotedWord(_))
844 | Some(tokens::Token::QuotedGlobWord(_))
845 );
846 let mut word = self.parse_word(w.clone());
847 if is_quoted {
848 word.quoted = true;
849 }
850 if matches!(&self.current_token, Some(tokens::Token::QuotedGlobWord(_))) {
851 word.has_unquoted_glob = true;
852 }
853 words.push(word);
854 self.advance();
855 }
856 Some(tokens::Token::LiteralWord(w)) => {
857 words.push(Word {
858 parts: vec![WordPart::Literal(w.clone())],
859 quoted: true,
860 has_unquoted_glob: false,
861 part_quoted: Vec::new(),
862 });
863 self.advance();
864 }
865 Some(tokens::Token::Newline) | Some(tokens::Token::Semicolon) => {
866 self.advance();
867 break;
868 }
869 _ => break,
870 }
871 }
872 Some(words)
873 } else {
874 if matches!(self.current_token, Some(tokens::Token::Semicolon)) {
877 self.advance();
878 }
879 None
880 };
881
882 self.skip_newlines()?;
883
884 self.expect_keyword("do")?;
886 self.skip_newlines()?;
887
888 let body = self.parse_compound_list("done")?;
890
891 if body.is_empty() {
893 self.pop_depth();
894 return Err(self.error("syntax error: empty for loop body"));
895 }
896
897 self.expect_keyword("done")?;
899
900 self.pop_depth();
901 Ok(CompoundCommand::For(ForCommand {
902 variable,
903 words,
904 body,
905 span: start_span.merge(self.current_span),
906 }))
907 }
908
909 fn parse_select(&mut self) -> Result<CompoundCommand> {
911 let start_span = self.current_span;
912 self.push_depth()?;
913 self.advance(); self.skip_newlines()?;
915
916 let variable = match &self.current_token {
918 Some(tokens::Token::Word(w))
919 | Some(tokens::Token::LiteralWord(w))
920 | Some(tokens::Token::QuotedWord(w))
921 | Some(tokens::Token::QuotedGlobWord(w)) => w.clone(),
922 _ => {
923 self.pop_depth();
924 return Err(Error::parse("expected variable name in select".to_string()));
925 }
926 };
927 self.advance();
928
929 if !self.is_keyword("in") {
931 self.pop_depth();
932 return Err(Error::parse("expected 'in' in select".to_string()));
933 }
934 self.advance(); let mut words = Vec::new();
938 loop {
939 match &self.current_token {
940 Some(tokens::Token::Word(w))
944 | Some(tokens::Token::QuotedWord(w))
945 | Some(tokens::Token::QuotedGlobWord(w)) => {
946 let is_quoted = matches!(
947 &self.current_token,
948 Some(tokens::Token::QuotedWord(_)) | Some(tokens::Token::QuotedGlobWord(_))
949 );
950 let mut word = self.parse_word(w.clone());
951 if is_quoted {
952 word.quoted = true;
953 }
954 if matches!(&self.current_token, Some(tokens::Token::QuotedGlobWord(_))) {
955 word.has_unquoted_glob = true;
956 }
957 words.push(word);
958 self.advance();
959 }
960 Some(tokens::Token::LiteralWord(w)) => {
961 words.push(Word {
962 parts: vec![WordPart::Literal(w.clone())],
963 quoted: true,
964 has_unquoted_glob: false,
965 part_quoted: Vec::new(),
966 });
967 self.advance();
968 }
969 Some(tokens::Token::Newline) | Some(tokens::Token::Semicolon) => {
970 self.advance();
971 break;
972 }
973 _ => break,
974 }
975 }
976
977 self.skip_newlines()?;
978
979 self.expect_keyword("do")?;
981 self.skip_newlines()?;
982
983 let body = self.parse_compound_list("done")?;
985
986 if body.is_empty() {
988 self.pop_depth();
989 return Err(self.error("syntax error: empty select loop body"));
990 }
991
992 self.expect_keyword("done")?;
994
995 self.pop_depth();
996 Ok(CompoundCommand::Select(SelectCommand {
997 variable,
998 words,
999 body,
1000 span: start_span.merge(self.current_span),
1001 }))
1002 }
1003
1004 fn parse_arithmetic_for_inner(&mut self, start_span: Span) -> Result<CompoundCommand> {
1007 self.advance(); let mut parts: Vec<String> = Vec::new();
1011 let mut current_expr = String::new();
1012 let mut paren_depth = 0;
1013
1014 loop {
1015 match &self.current_token {
1016 Some(tokens::Token::DoubleRightParen) => {
1017 parts.push(current_expr.trim().to_string());
1019 self.advance();
1020 break;
1021 }
1022 Some(tokens::Token::LeftParen) => {
1023 paren_depth += 1;
1024 current_expr.push('(');
1025 self.advance();
1026 }
1027 Some(tokens::Token::RightParen) => {
1028 if paren_depth > 0 {
1029 paren_depth -= 1;
1030 current_expr.push(')');
1031 self.advance();
1032 } else {
1033 self.advance();
1035 }
1036 }
1037 Some(tokens::Token::Semicolon) => {
1038 if paren_depth == 0 {
1039 parts.push(current_expr.trim().to_string());
1041 current_expr.clear();
1042 } else {
1043 current_expr.push(';');
1044 }
1045 self.advance();
1046 }
1047 Some(tokens::Token::Word(w))
1048 | Some(tokens::Token::LiteralWord(w))
1049 | Some(tokens::Token::QuotedWord(w))
1050 | Some(tokens::Token::QuotedGlobWord(w)) => {
1051 let skip_space = current_expr.ends_with('<')
1053 || current_expr.ends_with('>')
1054 || current_expr.ends_with(' ')
1055 || current_expr.ends_with('(')
1056 || current_expr.is_empty();
1057 if !skip_space {
1058 current_expr.push(' ');
1059 }
1060 current_expr.push_str(w);
1061 self.advance();
1062 }
1063 Some(tokens::Token::Newline) => {
1064 self.advance();
1065 }
1066 Some(tokens::Token::RedirectIn) => {
1068 current_expr.push('<');
1069 self.advance();
1070 }
1071 Some(tokens::Token::RedirectOut) => {
1072 current_expr.push('>');
1073 self.advance();
1074 }
1075 Some(tokens::Token::And) => {
1076 current_expr.push_str("&&");
1077 self.advance();
1078 }
1079 Some(tokens::Token::Or) => {
1080 current_expr.push_str("||");
1081 self.advance();
1082 }
1083 Some(tokens::Token::Pipe) => {
1084 current_expr.push('|');
1085 self.advance();
1086 }
1087 Some(tokens::Token::Background) => {
1088 current_expr.push('&');
1089 self.advance();
1090 }
1091 None => {
1092 return Err(Error::parse(
1093 "unexpected end of input in for loop".to_string(),
1094 ));
1095 }
1096 _ => {
1097 self.advance();
1098 }
1099 }
1100 }
1101
1102 while parts.len() < 3 {
1104 parts.push(String::new());
1105 }
1106
1107 let init = parts.first().cloned().unwrap_or_default();
1108 let condition = parts.get(1).cloned().unwrap_or_default();
1109 let step = parts.get(2).cloned().unwrap_or_default();
1110
1111 self.skip_newlines()?;
1112
1113 if matches!(self.current_token, Some(tokens::Token::Semicolon)) {
1115 self.advance();
1116 }
1117 self.skip_newlines()?;
1118
1119 self.expect_keyword("do")?;
1121 self.skip_newlines()?;
1122
1123 let body = self.parse_compound_list("done")?;
1125
1126 if body.is_empty() {
1128 return Err(self.error("syntax error: empty for loop body"));
1129 }
1130
1131 self.expect_keyword("done")?;
1133
1134 Ok(CompoundCommand::ArithmeticFor(ArithmeticForCommand {
1135 init,
1136 condition,
1137 step,
1138 body,
1139 span: start_span.merge(self.current_span),
1140 }))
1141 }
1142
1143 fn parse_while(&mut self) -> Result<CompoundCommand> {
1145 let start_span = self.current_span;
1146 self.push_depth()?;
1147 self.advance(); self.skip_newlines()?;
1149
1150 let condition = self.parse_compound_list("do")?;
1152
1153 self.expect_keyword("do")?;
1155 self.skip_newlines()?;
1156
1157 let body = self.parse_compound_list("done")?;
1159
1160 if body.is_empty() {
1162 self.pop_depth();
1163 return Err(self.error("syntax error: empty while loop body"));
1164 }
1165
1166 self.expect_keyword("done")?;
1168
1169 self.pop_depth();
1170 Ok(CompoundCommand::While(WhileCommand {
1171 condition,
1172 body,
1173 span: start_span.merge(self.current_span),
1174 }))
1175 }
1176
1177 fn parse_until(&mut self) -> Result<CompoundCommand> {
1179 let start_span = self.current_span;
1180 self.push_depth()?;
1181 self.advance(); self.skip_newlines()?;
1183
1184 let condition = self.parse_compound_list("do")?;
1186
1187 self.expect_keyword("do")?;
1189 self.skip_newlines()?;
1190
1191 let body = self.parse_compound_list("done")?;
1193
1194 if body.is_empty() {
1196 self.pop_depth();
1197 return Err(self.error("syntax error: empty until loop body"));
1198 }
1199
1200 self.expect_keyword("done")?;
1202
1203 self.pop_depth();
1204 Ok(CompoundCommand::Until(UntilCommand {
1205 condition,
1206 body,
1207 span: start_span.merge(self.current_span),
1208 }))
1209 }
1210
1211 fn parse_case(&mut self) -> Result<CompoundCommand> {
1213 let start_span = self.current_span;
1214 self.push_depth()?;
1215 self.advance(); self.skip_newlines()?;
1217
1218 let word = self.expect_word()?;
1220 self.skip_newlines()?;
1221
1222 self.expect_keyword("in")?;
1224 self.skip_newlines()?;
1225
1226 let mut cases = Vec::new();
1228 while !self.is_keyword("esac") && self.current_token.is_some() {
1229 self.skip_newlines()?;
1230 if self.is_keyword("esac") {
1231 break;
1232 }
1233
1234 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1237 self.advance();
1238 }
1239
1240 let mut patterns = Vec::new();
1241 while matches!(
1242 &self.current_token,
1243 Some(tokens::Token::Word(_))
1244 | Some(tokens::Token::LiteralWord(_))
1245 | Some(tokens::Token::QuotedWord(_))
1246 | Some(tokens::Token::QuotedGlobWord(_))
1247 ) {
1248 let pattern = match &self.current_token {
1249 Some(tokens::Token::LiteralWord(w)) => Word {
1250 parts: vec![WordPart::Literal(w.clone())],
1253 quoted: true,
1254 has_unquoted_glob: false,
1255 part_quoted: Vec::new(),
1256 },
1257 Some(tokens::Token::Word(w))
1258 | Some(tokens::Token::QuotedWord(w))
1259 | Some(tokens::Token::QuotedGlobWord(w)) => self.parse_word(w.clone()),
1260 _ => unreachable!(),
1261 };
1262 patterns.push(pattern);
1263 self.advance();
1264
1265 if matches!(self.current_token, Some(tokens::Token::Pipe)) {
1267 self.advance();
1268 } else {
1269 break;
1270 }
1271 }
1272
1273 if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1275 self.pop_depth();
1276 return Err(self.error("expected ')' after case pattern"));
1277 }
1278 self.advance();
1279 self.skip_newlines()?;
1280
1281 let mut commands = Vec::new();
1283 while !self.is_case_terminator()
1284 && !self.is_keyword("esac")
1285 && self.current_token.is_some()
1286 {
1287 if let Some(cmd) = self.parse_command_list()? {
1288 commands.push(cmd);
1289 }
1290 self.skip_newlines()?;
1291 }
1292
1293 let terminator = self.parse_case_terminator();
1294 cases.push(CaseItem {
1295 patterns,
1296 commands,
1297 terminator,
1298 });
1299 self.skip_newlines()?;
1300 }
1301
1302 self.expect_keyword("esac")?;
1304
1305 self.pop_depth();
1306 Ok(CompoundCommand::Case(CaseCommand {
1307 word,
1308 cases,
1309 span: start_span.merge(self.current_span),
1310 }))
1311 }
1312
1313 fn parse_time(&mut self) -> Result<CompoundCommand> {
1318 let start_span = self.current_span;
1319 self.advance(); self.skip_newlines()?;
1321
1322 let posix_format = if let Some(tokens::Token::Word(w)) = &self.current_token {
1324 if w == "-p" {
1325 self.advance();
1326 self.skip_newlines()?;
1327 true
1328 } else {
1329 false
1330 }
1331 } else {
1332 false
1333 };
1334
1335 let command = self.parse_pipeline()?;
1338
1339 Ok(CompoundCommand::Time(TimeCommand {
1340 posix_format,
1341 command: command.map(Box::new),
1342 span: start_span.merge(self.current_span),
1343 }))
1344 }
1345
1346 fn parse_coproc(&mut self) -> Result<CompoundCommand> {
1353 self.tick()?;
1354 self.push_depth()?;
1355
1356 let result = (|| {
1357 let start_span = self.current_span;
1358 self.advance(); self.skip_newlines()?;
1360
1361 let (name, consumed_name) = if let Some(tokens::Token::Word(w)) = &self.current_token {
1364 let word = w.clone();
1365 let is_compound_keyword = matches!(
1366 word.as_str(),
1367 "if" | "for" | "while" | "until" | "case" | "select" | "time" | "coproc"
1368 );
1369 let next_is_compound_start = matches!(
1370 self.peek_next(),
1371 Some(tokens::Token::LeftBrace) | Some(tokens::Token::LeftParen)
1372 );
1373 if !is_compound_keyword && next_is_compound_start {
1374 self.advance(); self.skip_newlines()?;
1376 (word, true)
1377 } else {
1378 ("COPROC".to_string(), false)
1379 }
1380 } else {
1381 ("COPROC".to_string(), false)
1382 };
1383
1384 let _ = consumed_name;
1385
1386 let body = self.parse_pipeline()?;
1388 let body = body.ok_or_else(|| self.error("coproc: missing command"))?;
1389
1390 Ok(CompoundCommand::Coproc(ast::CoprocCommand {
1391 name,
1392 body: Box::new(body),
1393 span: start_span.merge(self.current_span),
1394 }))
1395 })();
1396
1397 self.pop_depth();
1398 result
1399 }
1400
1401 fn is_case_terminator(&self) -> bool {
1403 matches!(
1404 self.current_token,
1405 Some(tokens::Token::DoubleSemicolon)
1406 | Some(tokens::Token::SemiAmp)
1407 | Some(tokens::Token::DoubleSemiAmp)
1408 )
1409 }
1410
1411 fn parse_case_terminator(&mut self) -> ast::CaseTerminator {
1413 match self.current_token {
1414 Some(tokens::Token::SemiAmp) => {
1415 self.advance();
1416 ast::CaseTerminator::FallThrough
1417 }
1418 Some(tokens::Token::DoubleSemiAmp) => {
1419 self.advance();
1420 ast::CaseTerminator::Continue
1421 }
1422 Some(tokens::Token::DoubleSemicolon) => {
1423 self.advance();
1424 ast::CaseTerminator::Break
1425 }
1426 _ => ast::CaseTerminator::Break,
1427 }
1428 }
1429
1430 fn parse_subshell(&mut self) -> Result<CompoundCommand> {
1432 self.push_depth()?;
1433 self.advance(); self.skip_newlines()?;
1435
1436 let mut commands = Vec::new();
1437 while !matches!(
1438 self.current_token,
1439 Some(tokens::Token::RightParen) | Some(tokens::Token::DoubleRightParen) | None
1440 ) {
1441 self.skip_newlines()?;
1442 if matches!(
1443 self.current_token,
1444 Some(tokens::Token::RightParen) | Some(tokens::Token::DoubleRightParen)
1445 ) {
1446 break;
1447 }
1448 if let Some(cmd) = self.parse_command_list()? {
1449 commands.push(cmd);
1450 }
1451 }
1452
1453 if matches!(self.current_token, Some(tokens::Token::DoubleRightParen)) {
1454 self.current_token = Some(tokens::Token::RightParen);
1456 } else if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1457 self.pop_depth();
1458 return Err(Error::parse("expected ')' to close subshell".to_string()));
1459 } else {
1460 self.advance(); }
1462
1463 self.pop_depth();
1464 Ok(CompoundCommand::Subshell(commands))
1465 }
1466
1467 fn parse_brace_group(&mut self) -> Result<CompoundCommand> {
1469 self.push_depth()?;
1470 self.advance(); self.skip_newlines()?;
1472
1473 let mut commands = Vec::new();
1474 while !matches!(self.current_token, Some(tokens::Token::RightBrace) | None) {
1475 self.skip_newlines()?;
1476 if matches!(self.current_token, Some(tokens::Token::RightBrace)) {
1477 break;
1478 }
1479 if let Some(cmd) = self.parse_command_list()? {
1480 commands.push(cmd);
1481 }
1482 }
1483
1484 if !matches!(self.current_token, Some(tokens::Token::RightBrace)) {
1485 self.pop_depth();
1486 return Err(Error::parse(
1487 "expected '}' to close brace group".to_string(),
1488 ));
1489 }
1490
1491 if commands.is_empty() {
1493 self.pop_depth();
1494 return Err(self.error("syntax error: empty brace group"));
1495 }
1496
1497 self.advance(); self.pop_depth();
1500 Ok(CompoundCommand::BraceGroup(commands))
1501 }
1502
1503 fn parse_conditional(&mut self) -> Result<CompoundCommand> {
1506 self.advance(); let mut words = Vec::new();
1509 let mut saw_regex_op = false;
1510
1511 loop {
1512 match &self.current_token {
1513 Some(tokens::Token::DoubleRightBracket) => {
1514 self.advance(); break;
1516 }
1517 Some(tokens::Token::Word(w))
1518 | Some(tokens::Token::LiteralWord(w))
1519 | Some(tokens::Token::QuotedWord(w))
1520 | Some(tokens::Token::QuotedGlobWord(w)) => {
1521 let w_clone = w.clone();
1522 let is_quoted = matches!(
1523 self.current_token,
1524 Some(tokens::Token::QuotedWord(_)) | Some(tokens::Token::QuotedGlobWord(_))
1525 );
1526 let is_literal =
1527 matches!(self.current_token, Some(tokens::Token::LiteralWord(_)));
1528
1529 if saw_regex_op {
1534 if w_clone.contains('$') && !is_quoted && !is_literal {
1535 let parsed = self.parse_word(w_clone);
1537 words.push(parsed);
1538 self.advance();
1539 } else {
1540 let pattern = self.collect_conditional_regex_pattern(&w_clone);
1541 words.push(Word::literal(&pattern));
1542 }
1543 saw_regex_op = false;
1544 continue;
1545 }
1546
1547 if w_clone == "=~" {
1548 saw_regex_op = true;
1549 }
1550
1551 let word = if is_literal {
1552 Word {
1553 parts: vec![WordPart::Literal(w_clone)],
1554 quoted: true,
1555 has_unquoted_glob: false,
1556 part_quoted: Vec::new(),
1557 }
1558 } else {
1559 let mut parsed = self.parse_word(w_clone);
1560 if is_quoted {
1561 parsed.quoted = true;
1562 }
1563 if matches!(self.current_token, Some(tokens::Token::QuotedGlobWord(_))) {
1564 parsed.has_unquoted_glob = true;
1565 }
1566 parsed
1567 };
1568 words.push(word);
1569 self.advance();
1570 }
1571 Some(tokens::Token::And) => {
1573 words.push(Word::literal("&&"));
1574 self.advance();
1575 }
1576 Some(tokens::Token::Or) => {
1577 words.push(Word::literal("||"));
1578 self.advance();
1579 }
1580 Some(tokens::Token::LeftParen) => {
1581 if saw_regex_op {
1582 let pattern = self.collect_conditional_regex_pattern("(");
1584 words.push(Word::literal(&pattern));
1585 saw_regex_op = false;
1586 continue;
1587 }
1588 words.push(Word::literal("("));
1589 self.advance();
1590 }
1591 Some(tokens::Token::RightParen) => {
1592 words.push(Word::literal(")"));
1593 self.advance();
1594 }
1595 None => {
1596 return Err(crate::error::Error::parse(
1597 "unexpected end of input in [[ ]]".to_string(),
1598 ));
1599 }
1600 _ => {
1601 self.advance();
1603 }
1604 }
1605 }
1606
1607 Ok(CompoundCommand::Conditional(words))
1608 }
1609
1610 fn collect_conditional_regex_pattern(&mut self, first_word: &str) -> String {
1612 let mut pattern = first_word.to_string();
1613 self.advance(); loop {
1617 match &self.current_token {
1618 Some(tokens::Token::DoubleRightBracket) => break,
1619 Some(tokens::Token::And) | Some(tokens::Token::Or) => break,
1620 Some(tokens::Token::LeftParen) => {
1621 pattern.push('(');
1622 self.advance();
1623 }
1624 Some(tokens::Token::RightParen) => {
1625 pattern.push(')');
1626 self.advance();
1627 }
1628 Some(tokens::Token::Word(w))
1629 | Some(tokens::Token::LiteralWord(w))
1630 | Some(tokens::Token::QuotedWord(w))
1631 | Some(tokens::Token::QuotedGlobWord(w)) => {
1632 pattern.push_str(w);
1633 self.advance();
1634 }
1635 _ => break,
1636 }
1637 }
1638
1639 pattern
1640 }
1641
1642 fn current_token_starts_with_eq(&self) -> Option<String> {
1645 match &self.current_token {
1646 Some(tokens::Token::Assignment) => Some(String::new()),
1647 Some(tokens::Token::Word(w)) | Some(tokens::Token::LiteralWord(w)) => {
1648 w.strip_prefix('=').map(|rest| rest.to_string())
1649 }
1650 _ => None,
1651 }
1652 }
1653
1654 fn parse_arithmetic_command(&mut self) -> Result<CompoundCommand> {
1655 self.advance(); let mut expr = String::new();
1659 let mut depth = 1;
1660
1661 loop {
1662 match &self.current_token {
1663 Some(tokens::Token::DoubleLeftParen) => {
1664 depth += 1;
1665 expr.push_str("((");
1666 self.advance();
1667 }
1668 Some(tokens::Token::DoubleRightParen) => {
1669 depth -= 1;
1670 if depth == 0 {
1671 self.advance(); break;
1673 }
1674 expr.push_str("))");
1675 self.advance();
1676 }
1677 Some(tokens::Token::LeftParen) => {
1678 expr.push('(');
1679 self.advance();
1680 }
1681 Some(tokens::Token::RightParen) => {
1682 expr.push(')');
1683 self.advance();
1684 }
1685 Some(tokens::Token::Word(w))
1686 | Some(tokens::Token::LiteralWord(w))
1687 | Some(tokens::Token::QuotedWord(w))
1688 | Some(tokens::Token::QuotedGlobWord(w)) => {
1689 if !expr.is_empty() && !expr.ends_with(' ') && !expr.ends_with('(') {
1690 expr.push(' ');
1691 }
1692 expr.push_str(w);
1693 self.advance();
1694 }
1695 Some(tokens::Token::Semicolon) => {
1696 expr.push(';');
1697 self.advance();
1698 }
1699 Some(tokens::Token::Newline) => {
1700 self.advance();
1701 }
1702 Some(tokens::Token::RedirectIn) => {
1704 self.advance();
1705 if let Some(rest) = self.current_token_starts_with_eq() {
1707 expr.push_str("<=");
1708 if !rest.is_empty() {
1709 expr.push_str(&rest);
1710 }
1711 self.advance();
1712 } else {
1713 expr.push('<');
1714 }
1715 }
1716 Some(tokens::Token::RedirectOut) => {
1717 self.advance();
1718 if let Some(rest) = self.current_token_starts_with_eq() {
1720 expr.push_str(">=");
1721 if !rest.is_empty() {
1722 expr.push_str(&rest);
1723 }
1724 self.advance();
1725 } else {
1726 expr.push('>');
1727 }
1728 }
1729 Some(tokens::Token::And) => {
1730 expr.push_str("&&");
1731 self.advance();
1732 }
1733 Some(tokens::Token::Or) => {
1734 expr.push_str("||");
1735 self.advance();
1736 }
1737 Some(tokens::Token::Pipe) => {
1738 expr.push('|');
1739 self.advance();
1740 }
1741 Some(tokens::Token::Background) => {
1742 expr.push('&');
1743 self.advance();
1744 }
1745 Some(tokens::Token::Assignment) => {
1746 expr.push('=');
1747 self.advance();
1748 }
1749 Some(tokens::Token::RedirectFd(fd)) => {
1751 let fd = *fd;
1752 self.advance();
1753 if let Some(rest) = self.current_token_starts_with_eq() {
1754 expr.push_str(&format!("{}>=", fd));
1756 if !rest.is_empty() {
1757 expr.push_str(&rest);
1758 }
1759 self.advance();
1760 } else {
1761 expr.push_str(&format!("{}>", fd));
1762 }
1763 }
1764 Some(tokens::Token::RedirectFdAppend(fd)) => {
1765 let fd = *fd;
1767 expr.push_str(&format!("{}>>", fd));
1768 self.advance();
1769 }
1770 Some(tokens::Token::RedirectFdIn(fd)) => {
1771 let fd = *fd;
1772 self.advance();
1773 if let Some(rest) = self.current_token_starts_with_eq() {
1774 expr.push_str(&format!("{}<=", fd));
1775 if !rest.is_empty() {
1776 expr.push_str(&rest);
1777 }
1778 self.advance();
1779 } else {
1780 expr.push_str(&format!("{}<", fd));
1781 }
1782 }
1783 Some(tokens::Token::RedirectAppend) => {
1784 expr.push_str(">>");
1786 self.advance();
1787 }
1788 None => {
1789 return Err(Error::parse(
1790 "unexpected end of input in arithmetic command".to_string(),
1791 ));
1792 }
1793 _ => {
1794 self.advance();
1795 }
1796 }
1797 }
1798
1799 Ok(CompoundCommand::Arithmetic(expr.trim().to_string()))
1800 }
1801
1802 fn parse_function_keyword(&mut self) -> Result<Command> {
1804 let start_span = self.current_span;
1805 self.advance(); self.skip_newlines()?;
1807
1808 let name = match &self.current_token {
1810 Some(tokens::Token::Word(w)) => w.clone(),
1811 _ => return Err(self.error("expected function name")),
1812 };
1813 self.advance();
1814 self.skip_newlines()?;
1815
1816 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1818 self.advance(); if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1820 return Err(Error::parse(
1821 "expected ')' in function definition".to_string(),
1822 ));
1823 }
1824 self.advance(); self.skip_newlines()?;
1826 }
1827
1828 if !matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
1830 return Err(Error::parse("expected '{' for function body".to_string()));
1831 }
1832
1833 let body = self.parse_brace_group()?;
1835 let end_offset = self.current_command_end_offset();
1836
1837 Ok(Command::Function(FunctionDef {
1838 name,
1839 body: Box::new(Command::Compound(body, Vec::new())),
1840 source: self.source_slice(start_span.start.offset, end_offset),
1841 span: start_span.merge(self.current_span),
1842 }))
1843 }
1844
1845 fn parse_function_posix(&mut self) -> Result<Command> {
1847 let start_span = self.current_span;
1848 let name = match &self.current_token {
1850 Some(tokens::Token::Word(w)) => w.clone(),
1851 _ => return Err(self.error("expected function name")),
1852 };
1853 self.advance();
1854
1855 if !matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1857 return Err(self.error("expected '(' in function definition"));
1858 }
1859 self.advance(); if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1862 return Err(self.error("expected ')' in function definition"));
1863 }
1864 self.advance(); self.skip_newlines()?;
1866
1867 if !matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
1869 return Err(self.error("expected '{' for function body"));
1870 }
1871
1872 let body = self.parse_brace_group()?;
1874 let end_offset = self.current_command_end_offset();
1875
1876 Ok(Command::Function(FunctionDef {
1877 name,
1878 body: Box::new(Command::Compound(body, Vec::new())),
1879 source: self.source_slice(start_span.start.offset, end_offset),
1880 span: start_span.merge(self.current_span),
1881 }))
1882 }
1883
1884 fn parse_compound_list(&mut self, terminator: &str) -> Result<Vec<Command>> {
1886 self.parse_compound_list_until(&[terminator])
1887 }
1888
1889 fn parse_compound_list_until(&mut self, terminators: &[&str]) -> Result<Vec<Command>> {
1891 let mut commands = Vec::new();
1892
1893 loop {
1894 self.skip_newlines()?;
1895
1896 if let Some(tokens::Token::Word(w)) = &self.current_token
1898 && terminators.contains(&w.as_str())
1899 {
1900 break;
1901 }
1902
1903 if self.current_token.is_none() {
1904 break;
1905 }
1906
1907 if let Some(cmd) = self.parse_command_list()? {
1908 commands.push(cmd);
1909 } else {
1910 break;
1911 }
1912 }
1913
1914 Ok(commands)
1915 }
1916
1917 const NON_COMMAND_WORDS: &'static [&'static str] =
1920 &["then", "else", "elif", "fi", "do", "done", "esac", "in"];
1921
1922 fn is_non_command_word(word: &str) -> bool {
1924 Self::NON_COMMAND_WORDS.contains(&word)
1925 }
1926
1927 fn is_keyword(&self, keyword: &str) -> bool {
1929 matches!(&self.current_token, Some(tokens::Token::Word(w)) if w == keyword)
1930 }
1931
1932 fn expect_keyword(&mut self, keyword: &str) -> Result<()> {
1934 if self.is_keyword(keyword) {
1935 self.advance();
1936 Ok(())
1937 } else {
1938 Err(self.error(format!("expected '{}'", keyword)))
1939 }
1940 }
1941
1942 fn split_array_elements(s: &str) -> Vec<(String, bool)> {
1947 let mut result = Vec::new();
1948 let mut current = String::new();
1949 let mut chars = s.chars().peekable();
1950 let mut in_double_quote = false;
1951 let mut in_single_quote = false;
1952 let mut is_quoted = false;
1953
1954 while let Some(c) = chars.next() {
1955 match c {
1956 '"' if !in_single_quote => {
1957 in_double_quote = !in_double_quote;
1958 is_quoted = true;
1959 }
1961 '\'' if !in_double_quote => {
1962 in_single_quote = !in_single_quote;
1963 is_quoted = true;
1964 }
1966 '\\' if in_double_quote => {
1967 if let Some(&next) = chars.peek() {
1969 if matches!(next, '$' | '`' | '"' | '\\' | '\n') {
1970 current.push(chars.next().unwrap());
1971 } else {
1972 current.push(c);
1973 }
1974 } else {
1975 current.push(c);
1976 }
1977 }
1978 c if c.is_ascii_whitespace() && !in_double_quote && !in_single_quote => {
1979 if !current.is_empty() {
1980 result.push((current.clone(), is_quoted));
1981 current.clear();
1982 is_quoted = false;
1983 }
1984 }
1985 _ => {
1986 current.push(c);
1987 }
1988 }
1989 }
1990 if !current.is_empty() {
1991 result.push((current, is_quoted));
1992 }
1993 result
1994 }
1995
1996 fn strip_quotes(s: &str) -> &str {
1997 if s.len() >= 2
1998 && ((s.starts_with('"') && s.ends_with('"'))
1999 || (s.starts_with('\'') && s.ends_with('\'')))
2000 {
2001 return &s[1..s.len() - 1];
2002 }
2003 s
2004 }
2005
2006 fn assignment_operator_pos(word: &str) -> Option<usize> {
2008 let mut bracket_depth = 0usize;
2009 let mut in_single_quote = false;
2010 let mut in_double_quote = false;
2011 let mut escaped = false;
2012
2013 for (pos, c) in word.char_indices() {
2014 if escaped {
2015 escaped = false;
2016 continue;
2017 }
2018
2019 if bracket_depth > 0 && c == '\\' {
2020 escaped = true;
2021 continue;
2022 }
2023
2024 match c {
2025 '\'' if bracket_depth > 0 && !in_double_quote => {
2026 in_single_quote = !in_single_quote;
2027 }
2028 '"' if bracket_depth > 0 && !in_single_quote => {
2029 in_double_quote = !in_double_quote;
2030 }
2031 '[' if !in_single_quote && !in_double_quote => {
2032 bracket_depth += 1;
2033 }
2034 ']' if bracket_depth > 0 && !in_single_quote && !in_double_quote => {
2035 bracket_depth -= 1;
2036 }
2037 '=' if bracket_depth == 0 => return Some(pos),
2038 _ => {}
2039 }
2040 }
2041
2042 None
2043 }
2044
2045 fn is_assignment(word: &str) -> Option<(&str, Option<&str>, &str, bool)> {
2048 let eq_pos = Self::assignment_operator_pos(word)?;
2049 let mut lhs = &word[..eq_pos];
2050 let is_append = lhs.ends_with('+');
2051 if is_append {
2052 lhs = &lhs[..lhs.len() - 1];
2053 }
2054 let value = &word[eq_pos + 1..];
2055
2056 if let Some(bracket_pos) = lhs.find('[') {
2058 let name = &lhs[..bracket_pos];
2059 if name.is_empty() {
2061 return None;
2062 }
2063 let mut chars = name.chars();
2064 let first = chars.next().unwrap();
2065 if !first.is_ascii_alphabetic() && first != '_' {
2066 return None;
2067 }
2068 for c in chars {
2069 if !c.is_ascii_alphanumeric() && c != '_' {
2070 return None;
2071 }
2072 }
2073 if lhs.ends_with(']') {
2075 let index = &lhs[bracket_pos + 1..lhs.len() - 1];
2076 return Some((name, Some(index), value, is_append));
2077 }
2078 } else {
2079 if lhs.is_empty() {
2081 return None;
2082 }
2083 let mut chars = lhs.chars();
2084 let first = chars.next().unwrap();
2085 if !first.is_ascii_alphabetic() && first != '_' {
2086 return None;
2087 }
2088 for c in chars {
2089 if !c.is_ascii_alphanumeric() && c != '_' {
2090 return None;
2091 }
2092 }
2093 return Some((lhs, None, value, is_append));
2094 }
2095 None
2096 }
2097
2098 fn collect_array_elements(&mut self) -> Vec<Word> {
2101 let mut elements = Vec::new();
2102 loop {
2103 match &self.current_token {
2104 Some(tokens::Token::RightParen) => {
2105 self.advance();
2106 break;
2107 }
2108 Some(tokens::Token::Word(elem))
2109 | Some(tokens::Token::LiteralWord(elem))
2110 | Some(tokens::Token::QuotedWord(elem))
2111 | Some(tokens::Token::QuotedGlobWord(elem)) => {
2112 let elem_clone = elem.clone();
2113 let word = if matches!(&self.current_token, Some(tokens::Token::LiteralWord(_)))
2114 {
2115 Word {
2116 parts: vec![WordPart::Literal(elem_clone)],
2117 quoted: true,
2118 has_unquoted_glob: false,
2119 part_quoted: Vec::new(),
2120 }
2121 } else if matches!(
2122 &self.current_token,
2123 Some(tokens::Token::QuotedWord(_)) | Some(tokens::Token::QuotedGlobWord(_))
2124 ) {
2125 let mut w = self.parse_word(elem_clone);
2126 w.quoted = true;
2127 w
2128 } else {
2129 self.parse_word(elem_clone)
2130 };
2131 elements.push(word);
2132 self.advance();
2133 }
2134 None => break,
2135 _ => {
2136 self.advance();
2137 }
2138 }
2139 }
2140 elements
2141 }
2142
2143 fn try_parse_assignment(&mut self, w: &str) -> Option<(Assignment, bool)> {
2147 let (name, index, value, is_append) = Self::is_assignment(w)?;
2148 let name = name.to_string();
2149 let index = index.map(|s| s.to_string());
2150 let value_str = value.to_string();
2151
2152 if value_str.starts_with('(') && value_str.ends_with(')') {
2154 let inner = &value_str[1..value_str.len() - 1];
2155 let elements: Vec<Word> = Self::split_array_elements(inner)
2156 .into_iter()
2157 .map(|(s, quoted)| {
2158 if quoted {
2159 let mut w = self.parse_word(s);
2160 w.quoted = true;
2161 w
2162 } else {
2163 self.parse_word(s)
2164 }
2165 })
2166 .collect();
2167 return Some((
2168 Assignment {
2169 name,
2170 index,
2171 value: AssignmentValue::Array(elements),
2172 append: is_append,
2173 },
2174 true,
2175 ));
2176 }
2177
2178 if value_str.is_empty() {
2180 self.advance();
2181 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
2182 self.advance(); let elements = self.collect_array_elements();
2184 return Some((
2185 Assignment {
2186 name,
2187 index,
2188 value: AssignmentValue::Array(elements),
2189 append: is_append,
2190 },
2191 false,
2192 ));
2193 }
2194 return Some((
2196 Assignment {
2197 name,
2198 index,
2199 value: AssignmentValue::Scalar(Word::literal("")),
2200 append: is_append,
2201 },
2202 false,
2203 ));
2204 }
2205
2206 let value_word = if value_str.starts_with('"') && value_str.ends_with('"') {
2208 let inner = Self::strip_quotes(&value_str);
2209 let mut w = self.parse_word(inner.to_string());
2210 w.quoted = true;
2211 w
2212 } else if value_str.starts_with('\'') && value_str.ends_with('\'') {
2213 let inner = Self::strip_quotes(&value_str);
2214 Word {
2215 parts: vec![WordPart::Literal(inner.to_string())],
2216 quoted: true,
2217 has_unquoted_glob: false,
2218 part_quoted: Vec::new(),
2219 }
2220 } else {
2221 self.parse_word(value_str)
2222 };
2223 Some((
2224 Assignment {
2225 name,
2226 index,
2227 value: AssignmentValue::Scalar(value_word),
2228 append: is_append,
2229 },
2230 true,
2231 ))
2232 }
2233
2234 fn try_parse_compound_array_arg(&mut self, saved_w: String) -> Option<Word> {
2238 if !matches!(self.current_token, Some(tokens::Token::LeftParen)) {
2239 return None;
2240 }
2241 self.advance(); let mut compound = saved_w;
2243 compound.push('(');
2244 loop {
2245 match &self.current_token {
2246 Some(tokens::Token::RightParen) => {
2247 compound.push(')');
2248 self.advance();
2249 break;
2250 }
2251 Some(tokens::Token::Word(elem))
2252 | Some(tokens::Token::LiteralWord(elem))
2253 | Some(tokens::Token::QuotedWord(elem))
2254 | Some(tokens::Token::QuotedGlobWord(elem)) => {
2255 if !compound.ends_with('(') {
2256 compound.push(' ');
2257 }
2258 compound.push_str(elem);
2259 self.advance();
2260 }
2261 None => break,
2262 _ => {
2263 self.advance();
2264 }
2265 }
2266 }
2267 Some(self.parse_word(compound))
2268 }
2269
2270 fn parse_heredoc_redirect(
2272 &mut self,
2273 strip_tabs: bool,
2274 redirects: &mut Vec<Redirect>,
2275 ) -> Result<()> {
2276 self.advance();
2277 let (delimiter, quoted) = match &self.current_token {
2279 Some(tokens::Token::Word(w)) => (w.clone(), false),
2280 Some(tokens::Token::LiteralWord(w)) => (w.clone(), true),
2281 Some(tokens::Token::QuotedWord(w)) | Some(tokens::Token::QuotedGlobWord(w)) => {
2282 (w.clone(), true)
2283 }
2284 _ => return Err(Error::parse("expected delimiter after <<".to_string())),
2285 };
2286
2287 let (content, rest_of_line_chars) = self
2288 .lexer
2289 .read_heredoc_with_strip_metered(&delimiter, strip_tabs);
2290 self.tick_units(rest_of_line_chars)?;
2291
2292 let content = if strip_tabs {
2294 let had_trailing_newline = content.ends_with('\n');
2295 let mut stripped: String = content
2296 .lines()
2297 .map(|l: &str| l.trim_start_matches('\t'))
2298 .collect::<Vec<_>>()
2299 .join("\n");
2300 if had_trailing_newline {
2301 stripped.push('\n');
2302 }
2303 stripped
2304 } else {
2305 content
2306 };
2307
2308 let target = if quoted {
2309 Word::quoted_literal(content)
2310 } else {
2311 self.parse_word(content)
2312 };
2313
2314 let kind = if strip_tabs {
2315 RedirectKind::HereDocStrip
2316 } else {
2317 RedirectKind::HereDoc
2318 };
2319
2320 redirects.push(Redirect {
2321 fd: None,
2322 fd_var: None,
2323 kind,
2324 target,
2325 });
2326
2327 self.advance();
2329
2330 self.collect_trailing_redirects(redirects);
2332 Ok(())
2333 }
2334
2335 fn collect_trailing_redirects(&mut self, redirects: &mut Vec<Redirect>) {
2337 while let Some(tok) = &self.current_token {
2338 match tok {
2339 tokens::Token::RedirectOut | tokens::Token::Clobber => {
2340 let kind = if matches!(&self.current_token, Some(tokens::Token::Clobber)) {
2341 RedirectKind::Clobber
2342 } else {
2343 RedirectKind::Output
2344 };
2345 self.advance();
2346 if let Ok(target) = self.expect_word() {
2347 redirects.push(Redirect {
2348 fd: None,
2349 fd_var: None,
2350 kind,
2351 target,
2352 });
2353 }
2354 }
2355 tokens::Token::RedirectAppend => {
2356 self.advance();
2357 if let Ok(target) = self.expect_word() {
2358 redirects.push(Redirect {
2359 fd: None,
2360 fd_var: None,
2361 kind: RedirectKind::Append,
2362 target,
2363 });
2364 }
2365 }
2366 tokens::Token::RedirectFd(fd) => {
2367 let fd = *fd;
2368 self.advance();
2369 if let Ok(target) = self.expect_word() {
2370 redirects.push(Redirect {
2371 fd: Some(fd),
2372 fd_var: None,
2373 kind: RedirectKind::Output,
2374 target,
2375 });
2376 }
2377 }
2378 tokens::Token::DupFdCloseOut(fd) => {
2379 let fd = *fd;
2380 self.advance();
2381 redirects.push(Redirect {
2382 fd: Some(fd),
2383 fd_var: None,
2384 kind: RedirectKind::DupOutput,
2385 target: Word::literal("-"),
2386 });
2387 }
2388 tokens::Token::DupInput => {
2389 self.advance();
2390 if let Ok(target) = self.expect_word() {
2391 redirects.push(Redirect {
2392 fd: Some(0),
2393 fd_var: None,
2394 kind: RedirectKind::DupInput,
2395 target,
2396 });
2397 }
2398 }
2399 tokens::Token::DupFdIn(src_fd, dst_fd) => {
2400 let src_fd = *src_fd;
2401 let dst_fd = *dst_fd;
2402 self.advance();
2403 redirects.push(Redirect {
2404 fd: Some(src_fd),
2405 fd_var: None,
2406 kind: RedirectKind::DupInput,
2407 target: Word::literal(dst_fd.to_string()),
2408 });
2409 }
2410 tokens::Token::DupFdClose(fd) => {
2411 let fd = *fd;
2412 self.advance();
2413 redirects.push(Redirect {
2414 fd: Some(fd),
2415 fd_var: None,
2416 kind: RedirectKind::DupInput,
2417 target: Word::literal("-"),
2418 });
2419 }
2420 tokens::Token::RedirectFdIn(fd) => {
2421 let fd = *fd;
2422 self.advance();
2423 if let Ok(target) = self.expect_word() {
2424 redirects.push(Redirect {
2425 fd: Some(fd),
2426 fd_var: None,
2427 kind: RedirectKind::Input,
2428 target,
2429 });
2430 }
2431 }
2432 _ => break,
2433 }
2434 }
2435 }
2436
2437 fn pop_fd_var(words: &mut Vec<Word>) -> Option<String> {
2441 if let Some(last) = words.last()
2442 && last.parts.len() == 1
2443 && let WordPart::Literal(ref s) = last.parts[0]
2444 && s.starts_with('{')
2445 && s.ends_with('}')
2446 && s.len() > 2
2447 && s[1..s.len() - 1]
2448 .chars()
2449 .all(|c| c.is_alphanumeric() || c == '_')
2450 {
2451 let var_name = s[1..s.len() - 1].to_string();
2452 words.pop();
2453 return Some(var_name);
2454 }
2455 None
2456 }
2457
2458 fn parse_simple_command(&mut self) -> Result<Option<SimpleCommand>> {
2459 self.tick()?;
2460 self.skip_newlines()?;
2461 self.check_error_token()?;
2462 let start_span = self.current_span;
2463
2464 let mut assignments = Vec::new();
2465 let mut words = Vec::new();
2466 let mut redirects = Vec::new();
2467
2468 loop {
2469 match &self.current_token {
2470 Some(tokens::Token::Word(w))
2471 | Some(tokens::Token::LiteralWord(w))
2472 | Some(tokens::Token::QuotedWord(w))
2473 | Some(tokens::Token::QuotedGlobWord(w)) => {
2474 let is_literal =
2475 matches!(&self.current_token, Some(tokens::Token::LiteralWord(_)));
2476 let is_quoted = matches!(
2477 &self.current_token,
2478 Some(tokens::Token::QuotedWord(_)) | Some(tokens::Token::QuotedGlobWord(_))
2479 );
2480 let is_glob_quoted =
2481 matches!(&self.current_token, Some(tokens::Token::QuotedGlobWord(_)));
2482 let w = w.clone();
2484
2485 if words.is_empty() && Self::is_non_command_word(&w) {
2487 break;
2488 }
2489
2490 if words.is_empty()
2492 && !is_literal
2493 && let Some((assignment, needs_advance)) = self.try_parse_assignment(&w)
2494 {
2495 if needs_advance {
2496 self.advance();
2497 }
2498 assignments.push(assignment);
2499 continue;
2500 }
2501
2502 if w.ends_with('=') && !words.is_empty() {
2505 self.advance();
2506 if let Some(word) = self.try_parse_compound_array_arg(w.clone()) {
2507 words.push(word);
2508 continue;
2509 }
2510 let word = if is_literal {
2512 Word {
2513 parts: vec![WordPart::Literal(w)],
2514 quoted: true,
2515 has_unquoted_glob: false,
2516 part_quoted: Vec::new(),
2517 }
2518 } else {
2519 let mut word = self.parse_word(w);
2520 if is_quoted {
2521 word.quoted = true;
2522 }
2523 if is_glob_quoted {
2524 word.has_unquoted_glob = true;
2525 }
2526 word
2527 };
2528 words.push(word);
2529 continue;
2530 }
2531
2532 let word = if is_literal {
2533 Word {
2534 parts: vec![WordPart::Literal(w)],
2535 quoted: true,
2536 has_unquoted_glob: false,
2537 part_quoted: Vec::new(),
2538 }
2539 } else {
2540 let mut word = self.parse_word(w);
2541 if is_quoted {
2542 word.quoted = true;
2543 }
2544 if is_glob_quoted {
2545 word.has_unquoted_glob = true;
2546 }
2547 word
2548 };
2549 words.push(word);
2550 self.advance();
2551 }
2552 Some(tokens::Token::RedirectOut) | Some(tokens::Token::Clobber) => {
2553 let kind = if matches!(&self.current_token, Some(tokens::Token::Clobber)) {
2554 RedirectKind::Clobber
2555 } else {
2556 RedirectKind::Output
2557 };
2558 let fd_var = Self::pop_fd_var(&mut words);
2559 self.advance();
2560 let target = self.expect_word()?;
2561 redirects.push(Redirect {
2562 fd: None,
2563 fd_var,
2564 kind,
2565 target,
2566 });
2567 }
2568 Some(tokens::Token::RedirectAppend) => {
2569 let fd_var = Self::pop_fd_var(&mut words);
2570 self.advance();
2571 let target = self.expect_word()?;
2572 redirects.push(Redirect {
2573 fd: None,
2574 fd_var,
2575 kind: RedirectKind::Append,
2576 target,
2577 });
2578 }
2579 Some(tokens::Token::RedirectIn) => {
2580 let fd_var = Self::pop_fd_var(&mut words);
2581 self.advance();
2582 let target = self.expect_word()?;
2583 redirects.push(Redirect {
2584 fd: None,
2585 fd_var,
2586 kind: RedirectKind::Input,
2587 target,
2588 });
2589 }
2590 Some(tokens::Token::HereString) => {
2591 let fd_var = Self::pop_fd_var(&mut words);
2592 self.advance();
2593 let target = self.expect_word()?;
2594 redirects.push(Redirect {
2595 fd: None,
2596 fd_var,
2597 kind: RedirectKind::HereString,
2598 target,
2599 });
2600 }
2601 Some(tokens::Token::HereDoc) | Some(tokens::Token::HereDocStrip) => {
2602 let strip_tabs =
2603 matches!(self.current_token, Some(tokens::Token::HereDocStrip));
2604 self.parse_heredoc_redirect(strip_tabs, &mut redirects)?;
2605 break;
2606 }
2607 Some(tokens::Token::ProcessSubIn) | Some(tokens::Token::ProcessSubOut) => {
2608 let word = self.expect_word()?;
2609 words.push(word);
2610 }
2611 Some(tokens::Token::RedirectBoth) => {
2612 let fd_var = Self::pop_fd_var(&mut words);
2613 self.advance();
2614 let target = self.expect_word()?;
2615 redirects.push(Redirect {
2616 fd: None,
2617 fd_var,
2618 kind: RedirectKind::OutputBoth,
2619 target,
2620 });
2621 }
2622 Some(tokens::Token::DupOutput) => {
2623 let fd_var = Self::pop_fd_var(&mut words);
2624 self.advance();
2625 let target = self.expect_word()?;
2626 redirects.push(Redirect {
2627 fd: if fd_var.is_some() { None } else { Some(1) },
2628 fd_var,
2629 kind: RedirectKind::DupOutput,
2630 target,
2631 });
2632 }
2633 Some(tokens::Token::RedirectFd(fd)) => {
2634 let fd = *fd;
2635 self.advance();
2636 let target = self.expect_word()?;
2637 redirects.push(Redirect {
2638 fd: Some(fd),
2639 fd_var: None,
2640 kind: RedirectKind::Output,
2641 target,
2642 });
2643 }
2644 Some(tokens::Token::RedirectFdAppend(fd)) => {
2645 let fd = *fd;
2646 self.advance();
2647 let target = self.expect_word()?;
2648 redirects.push(Redirect {
2649 fd: Some(fd),
2650 fd_var: None,
2651 kind: RedirectKind::Append,
2652 target,
2653 });
2654 }
2655 Some(tokens::Token::DupFd(src_fd, dst_fd)) => {
2656 let src_fd = *src_fd;
2657 let dst_fd = *dst_fd;
2658 self.advance();
2659 redirects.push(Redirect {
2660 fd: Some(src_fd),
2661 fd_var: None,
2662 kind: RedirectKind::DupOutput,
2663 target: Word::literal(dst_fd.to_string()),
2664 });
2665 }
2666 Some(tokens::Token::DupFdCloseOut(fd)) => {
2667 let fd = *fd;
2668 self.advance();
2669 redirects.push(Redirect {
2670 fd: Some(fd),
2671 fd_var: None,
2672 kind: RedirectKind::DupOutput,
2673 target: Word::literal("-"),
2674 });
2675 }
2676 Some(tokens::Token::DupInput) => {
2677 let fd_var = Self::pop_fd_var(&mut words);
2678 self.advance();
2679 let target = self.expect_word()?;
2680 redirects.push(Redirect {
2681 fd: if fd_var.is_some() { None } else { Some(0) },
2682 fd_var,
2683 kind: RedirectKind::DupInput,
2684 target,
2685 });
2686 }
2687 Some(tokens::Token::DupFdIn(src_fd, dst_fd)) => {
2688 let src_fd = *src_fd;
2689 let dst_fd = *dst_fd;
2690 self.advance();
2691 redirects.push(Redirect {
2692 fd: Some(src_fd),
2693 fd_var: None,
2694 kind: RedirectKind::DupInput,
2695 target: Word::literal(dst_fd.to_string()),
2696 });
2697 }
2698 Some(tokens::Token::DupFdClose(fd)) => {
2699 let fd = *fd;
2700 self.advance();
2701 redirects.push(Redirect {
2702 fd: Some(fd),
2703 fd_var: None,
2704 kind: RedirectKind::DupInput,
2705 target: Word::literal("-"),
2706 });
2707 }
2708 Some(tokens::Token::RedirectFdIn(fd)) => {
2709 let fd = *fd;
2710 self.advance();
2711 let target = self.expect_word()?;
2712 redirects.push(Redirect {
2713 fd: Some(fd),
2714 fd_var: None,
2715 kind: RedirectKind::Input,
2716 target,
2717 });
2718 }
2719 Some(tokens::Token::LeftBrace) | Some(tokens::Token::RightBrace)
2721 if !words.is_empty() =>
2722 {
2723 let sym = if matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
2724 "{"
2725 } else {
2726 "}"
2727 };
2728 words.push(Word::literal(sym));
2729 self.advance();
2730 }
2731 Some(tokens::Token::Newline)
2732 | Some(tokens::Token::Semicolon)
2733 | Some(tokens::Token::Pipe)
2734 | Some(tokens::Token::And)
2735 | Some(tokens::Token::Or)
2736 | None => break,
2737 _ => break,
2738 }
2739 }
2740
2741 if words.is_empty() && !assignments.is_empty() {
2743 return Ok(Some(SimpleCommand {
2744 name: Word::literal(""),
2745 args: Vec::new(),
2746 redirects,
2747 assignments,
2748 span: start_span.merge(self.current_span),
2749 }));
2750 }
2751
2752 if words.is_empty() {
2753 return Ok(None);
2754 }
2755
2756 let name = words.remove(0);
2757 let args = words;
2758
2759 Ok(Some(SimpleCommand {
2760 name,
2761 args,
2762 redirects,
2763 assignments,
2764 span: start_span.merge(self.current_span),
2765 }))
2766 }
2767
2768 fn expect_word(&mut self) -> Result<Word> {
2770 match &self.current_token {
2771 Some(tokens::Token::Word(w)) => {
2772 let word = self.parse_word(w.clone());
2773 self.advance();
2774 Ok(word)
2775 }
2776 Some(tokens::Token::LiteralWord(w)) => {
2777 let word = Word {
2779 parts: vec![WordPart::Literal(w.clone())],
2780 quoted: true,
2781 has_unquoted_glob: false,
2782 part_quoted: Vec::new(),
2783 };
2784 self.advance();
2785 Ok(word)
2786 }
2787 Some(tokens::Token::QuotedWord(w)) | Some(tokens::Token::QuotedGlobWord(w)) => {
2788 let word = self.parse_word(w.clone());
2790 self.advance();
2791 Ok(word)
2792 }
2793 Some(tokens::Token::ProcessSubIn) | Some(tokens::Token::ProcessSubOut) => {
2794 let is_input = matches!(self.current_token, Some(tokens::Token::ProcessSubIn));
2802 let body_start_offset = self.current_span.end.offset;
2804 self.advance();
2805
2806 let mut depth = 1;
2807 let body_end_offset;
2808 loop {
2809 match &self.current_token {
2810 Some(tokens::Token::LeftParen) => {
2811 depth += 1;
2812 self.advance();
2813 }
2814 Some(tokens::Token::RightParen) => {
2815 depth -= 1;
2816 if depth == 0 {
2817 body_end_offset = self.current_span.start.offset;
2819 self.advance();
2820 break;
2821 }
2822 self.advance();
2823 }
2824 Some(tokens::Token::ProcessSubIn) | Some(tokens::Token::ProcessSubOut) => {
2825 depth += 1;
2827 self.advance();
2828 }
2829 Some(tokens::Token::Error(e)) => {
2830 let msg = e.clone();
2831 self.advance();
2832 return Err(Error::parse(format!(
2833 "lexer error in process substitution: {}",
2834 msg
2835 )));
2836 }
2837 None => {
2838 return Err(Error::parse(
2839 "unexpected end of input in process substitution".to_string(),
2840 ));
2841 }
2842 _ => {
2843 self.advance();
2844 }
2845 }
2846 }
2847
2848 let body_len = body_end_offset.saturating_sub(body_start_offset);
2849 self.tick_units(body_len)?;
2850
2851 if self.current_depth >= self.max_depth {
2857 return Err(Error::parse(format!(
2858 "AST nesting too deep ({} levels, max {})",
2859 self.current_depth + 1,
2860 self.max_depth
2861 )));
2862 }
2863 let inner_result = {
2864 let cmd_src = self
2865 .input
2866 .get(body_start_offset..body_end_offset)
2867 .unwrap_or("");
2868 let mut inner_parser = Parser::with_limits_and_timeout(
2869 cmd_src,
2870 self.max_depth,
2871 self.fuel,
2872 self.timeout,
2873 );
2874 inner_parser.current_depth = self.current_depth + 1;
2875 inner_parser.started_at = self.started_at;
2876 let result = inner_parser.parse_script();
2877 (result, inner_parser.fuel)
2878 };
2879 let (parse_result, remaining_fuel) = inner_result;
2880 self.fuel = remaining_fuel;
2881 let commands = match parse_result {
2882 Ok(script) => script.commands,
2883 Err(err) if Self::is_parser_budget_error(&err) => return Err(err),
2884 Err(_) => Vec::new(),
2885 };
2886
2887 Ok(Word {
2888 parts: vec![WordPart::ProcessSubstitution { commands, is_input }],
2889 quoted: false,
2890 has_unquoted_glob: false,
2891 part_quoted: Vec::new(),
2892 })
2893 }
2894 _ => Err(self.error("expected word")),
2895 }
2896 }
2897
2898 fn is_parser_budget_error(err: &Error) -> bool {
2899 match err {
2900 Error::ResourceLimit(_) => true,
2901 Error::Parse { message, .. } => {
2902 message.starts_with("AST nesting too deep")
2903 || message.starts_with("parser fuel exhausted")
2904 }
2905 _ => false,
2906 }
2907 }
2908
2909 #[allow(dead_code)]
2911 fn current_word_to_word(&self) -> Option<Word> {
2913 match &self.current_token {
2914 Some(tokens::Token::Word(w))
2915 | Some(tokens::Token::QuotedWord(w))
2916 | Some(tokens::Token::QuotedGlobWord(w)) => Some(self.parse_word(w.clone())),
2917 Some(tokens::Token::LiteralWord(w)) => Some(Word {
2918 parts: vec![WordPart::Literal(w.clone())],
2919 quoted: true,
2920 has_unquoted_glob: false,
2921 part_quoted: Vec::new(),
2922 }),
2923 _ => None,
2924 }
2925 }
2926
2927 #[allow(dead_code)]
2928 fn is_current_word(&self) -> bool {
2930 matches!(
2931 &self.current_token,
2932 Some(tokens::Token::Word(_))
2933 | Some(tokens::Token::LiteralWord(_))
2934 | Some(tokens::Token::QuotedWord(_))
2935 | Some(tokens::Token::QuotedGlobWord(_))
2936 )
2937 }
2938
2939 #[allow(dead_code)]
2940 fn current_word_str(&self) -> Option<String> {
2942 match &self.current_token {
2943 Some(tokens::Token::Word(w))
2944 | Some(tokens::Token::LiteralWord(w))
2945 | Some(tokens::Token::QuotedWord(w))
2946 | Some(tokens::Token::QuotedGlobWord(w)) => Some(w.clone()),
2947 _ => None,
2948 }
2949 }
2950
2951 fn parse_word(&self, s: String) -> Word {
2953 let mut parts = Vec::new();
2954 let mut part_quoted = Vec::new();
2955 let mut chars = s.chars().peekable();
2956 let mut current = String::new();
2957 let mut in_quoted_segment = false;
2958 macro_rules! push_part {
2959 ($part:expr) => {{
2960 parts.push($part);
2961 part_quoted.push(in_quoted_segment);
2962 }};
2963 }
2964
2965 while let Some(ch) = chars.next() {
2966 if ch == '\x00' {
2967 if let Some(literal_ch) = chars.next() {
2969 current.push(literal_ch);
2970 }
2971 } else if ch == '\u{1e}' {
2972 in_quoted_segment = true;
2973 } else if ch == '\u{1f}' {
2974 in_quoted_segment = false;
2975 } else if ch == '$' {
2976 if !current.is_empty() {
2978 push_part!(WordPart::Literal(std::mem::take(&mut current)));
2979 }
2980
2981 if chars.peek() == Some(&'\'') {
2983 chars.next(); let mut ansi = String::new();
2985 while let Some(c) = chars.next() {
2986 if c == '\'' {
2987 break;
2988 }
2989 if c == '\\' {
2990 if let Some(esc) = chars.next() {
2991 match esc {
2992 'n' => ansi.push('\n'),
2993 't' => ansi.push('\t'),
2994 'r' => ansi.push('\r'),
2995 'a' => ansi.push('\x07'),
2996 'b' => ansi.push('\x08'),
2997 'e' | 'E' => ansi.push('\x1B'),
2998 '\\' => ansi.push('\\'),
2999 '\'' => ansi.push('\''),
3000 _ => {
3001 ansi.push('\\');
3002 ansi.push(esc);
3003 }
3004 }
3005 }
3006 } else {
3007 ansi.push(c);
3008 }
3009 }
3010 push_part!(WordPart::Literal(ansi));
3011 } else if chars.peek() == Some(&'(') {
3012 chars.next(); if chars.peek() == Some(&'(') {
3017 chars.next(); let mut expr = String::new();
3019 let mut depth = 2;
3020 for c in chars.by_ref() {
3021 if c == '(' {
3022 depth += 1;
3023 expr.push(c);
3024 } else if c == ')' {
3025 depth -= 1;
3026 if depth == 0 {
3027 break;
3028 }
3029 expr.push(c);
3030 } else {
3031 expr.push(c);
3032 }
3033 }
3034 if expr.ends_with(')') {
3036 expr.pop();
3037 }
3038 push_part!(WordPart::ArithmeticExpansion(expr));
3039 } else {
3040 let mut cmd_str = String::new();
3042 let mut depth = 1;
3043 for c in chars.by_ref() {
3044 if c == '(' {
3045 depth += 1;
3046 cmd_str.push(c);
3047 } else if c == ')' {
3048 depth -= 1;
3049 if depth == 0 {
3050 break;
3051 }
3052 cmd_str.push(c);
3053 } else {
3054 cmd_str.push(c);
3055 }
3056 }
3057 let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
3060 let inner_parser =
3061 Parser::with_limits(&cmd_str, remaining_depth, self.fuel);
3062 if let Ok(script) = inner_parser.parse() {
3063 push_part!(WordPart::CommandSubstitution(script.commands));
3064 }
3065 }
3066 } else if chars.peek() == Some(&'{') {
3067 chars.next(); if chars.peek() == Some(&'#') {
3072 chars.next(); let mut var_name = String::new();
3074 while let Some(&c) = chars.peek() {
3075 if c == '}' || c == '[' {
3076 break;
3077 }
3078 var_name.push(chars.next().unwrap());
3079 }
3080 if chars.peek() == Some(&'[') {
3082 chars.next(); let mut index = String::new();
3084 while let Some(&c) = chars.peek() {
3085 if c == ']' {
3086 chars.next();
3087 break;
3088 }
3089 index.push(chars.next().unwrap());
3090 }
3091 if chars.peek() == Some(&'}') {
3093 chars.next();
3094 }
3095 if index == "@" || index == "*" {
3096 push_part!(WordPart::ArrayLength(var_name));
3097 } else {
3098 push_part!(WordPart::Length(format!("{}[{}]", var_name, index)));
3100 }
3101 } else {
3102 if chars.peek() == Some(&'}') {
3104 chars.next();
3105 }
3106 push_part!(WordPart::Length(var_name));
3107 }
3108 } else if chars.peek() == Some(&'!') {
3109 chars.next(); let mut var_name = String::new();
3113 while let Some(&c) = chars.peek() {
3114 if c == '}'
3115 || c == '['
3116 || c == '*'
3117 || c == '@'
3118 || c == ':'
3119 || c == '-'
3120 || c == '='
3121 || c == '+'
3122 || c == '?'
3123 {
3124 break;
3125 }
3126 var_name.push(chars.next().unwrap());
3127 }
3128 if chars.peek() == Some(&'[') {
3130 chars.next(); let mut index = String::new();
3132 while let Some(&c) = chars.peek() {
3133 if c == ']' {
3134 chars.next();
3135 break;
3136 }
3137 index.push(chars.next().unwrap());
3138 }
3139 if chars.peek() == Some(&'}') {
3141 chars.next();
3142 }
3143 if index == "@" || index == "*" {
3144 push_part!(WordPart::ArrayIndices(var_name));
3145 } else {
3146 push_part!(WordPart::Variable(format!("!{}[{}]", var_name, index)));
3148 }
3149 } else if chars.peek() == Some(&'}') {
3150 chars.next(); push_part!(WordPart::IndirectExpansion {
3153 name: var_name,
3154 operator: None,
3155 operand: String::new(),
3156 colon_variant: false,
3157 });
3158 } else if chars.peek() == Some(&':') {
3159 let mut lookahead = chars.clone();
3161 lookahead.next(); if matches!(
3163 lookahead.peek(),
3164 Some(&'-') | Some(&'=') | Some(&'+') | Some(&'?')
3165 ) {
3166 chars.next(); let op_char = chars.next().unwrap();
3168 let operand = self.read_brace_operand(&mut chars);
3169 let operator = match op_char {
3170 '-' => ParameterOp::UseDefault,
3171 '=' => ParameterOp::AssignDefault,
3172 '+' => ParameterOp::UseReplacement,
3173 '?' => ParameterOp::Error,
3174 _ => unreachable!(),
3175 };
3176 push_part!(WordPart::IndirectExpansion {
3177 name: var_name,
3178 operator: Some(operator),
3179 operand,
3180 colon_variant: true,
3181 });
3182 } else {
3183 let mut suffix = String::new();
3185 while let Some(&c) = chars.peek() {
3186 if c == '}' {
3187 chars.next();
3188 break;
3189 }
3190 suffix.push(chars.next().unwrap());
3191 }
3192 push_part!(WordPart::Variable(format!("!{}{}", var_name, suffix)));
3193 }
3194 } else if matches!(
3195 chars.peek(),
3196 Some(&'-') | Some(&'=') | Some(&'+') | Some(&'?')
3197 ) {
3198 let op_char = chars.next().unwrap();
3200 let operand = self.read_brace_operand(&mut chars);
3201 let operator = match op_char {
3202 '-' => ParameterOp::UseDefault,
3203 '=' => ParameterOp::AssignDefault,
3204 '+' => ParameterOp::UseReplacement,
3205 '?' => ParameterOp::Error,
3206 _ => unreachable!(),
3207 };
3208 push_part!(WordPart::IndirectExpansion {
3209 name: var_name,
3210 operator: Some(operator),
3211 operand,
3212 colon_variant: false,
3213 });
3214 } else {
3215 let mut suffix = String::new();
3217 while let Some(&c) = chars.peek() {
3218 if c == '}' {
3219 chars.next();
3220 break;
3221 }
3222 suffix.push(chars.next().unwrap());
3223 }
3224 if suffix.ends_with('*') || suffix.ends_with('@') {
3226 let full_prefix =
3227 format!("{}{}", var_name, &suffix[..suffix.len() - 1]);
3228 push_part!(WordPart::PrefixMatch(full_prefix));
3229 } else {
3230 push_part!(WordPart::Variable(format!("!{}{}", var_name, suffix)));
3231 }
3232 }
3233 } else {
3234 let mut var_name = String::new();
3236 while let Some(&c) = chars.peek() {
3237 if c.is_ascii_alphanumeric() || c == '_' {
3238 var_name.push(chars.next().unwrap());
3239 } else {
3240 break;
3241 }
3242 }
3243
3244 if var_name.is_empty()
3246 && let Some(&c) = chars.peek()
3247 && matches!(c, '@' | '*')
3248 {
3249 var_name.push(chars.next().unwrap());
3250 }
3251
3252 if chars.peek() == Some(&'[') {
3254 chars.next(); let mut index = String::new();
3256 let mut bracket_depth: i32 = 0;
3260 let mut brace_depth: i32 = 0;
3261 while let Some(&c) = chars.peek() {
3262 if c == ']' && bracket_depth == 0 && brace_depth == 0 {
3263 chars.next();
3264 break;
3265 }
3266 match c {
3267 '[' => bracket_depth += 1,
3268 ']' => bracket_depth -= 1,
3269 '$' => {
3270 index.push(chars.next().unwrap());
3271 if chars.peek() == Some(&'{') {
3272 brace_depth += 1;
3273 index.push(chars.next().unwrap());
3274 continue;
3275 }
3276 continue;
3277 }
3278 '{' => brace_depth += 1,
3279 '}' if brace_depth > 0 => brace_depth -= 1,
3280 '}' => {}
3281 _ => {}
3282 }
3283 index.push(chars.next().unwrap());
3284 }
3285 if index.len() >= 2
3287 && ((index.starts_with('"') && index.ends_with('"'))
3288 || (index.starts_with('\'') && index.ends_with('\'')))
3289 {
3290 index = index[1..index.len() - 1].to_string();
3291 }
3292 if let Some(&next_c) = chars.peek() {
3294 if next_c == ':' {
3295 let mut lookahead = chars.clone();
3297 lookahead.next(); let is_param_op = matches!(
3299 lookahead.peek(),
3300 Some(&'-') | Some(&'=') | Some(&'+') | Some(&'?')
3301 );
3302 if is_param_op {
3303 chars.next(); let arr_name = format!("{}[{}]", var_name, index);
3305 let op_char = chars.next().unwrap();
3306 let operand = self.read_brace_operand(&mut chars);
3307 let operator = match op_char {
3308 '-' => ParameterOp::UseDefault,
3309 '=' => ParameterOp::AssignDefault,
3310 '+' => ParameterOp::UseReplacement,
3311 '?' => ParameterOp::Error,
3312 _ => unreachable!(),
3313 };
3314 push_part!(WordPart::ParameterExpansion {
3315 name: arr_name,
3316 operator,
3317 operand,
3318 colon_variant: true,
3319 });
3320 } else {
3321 chars.next(); let mut offset = String::new();
3324 while let Some(&c) = chars.peek() {
3325 if c == ':' || c == '}' {
3326 break;
3327 }
3328 offset.push(chars.next().unwrap());
3329 }
3330 let length = if chars.peek() == Some(&':') {
3331 chars.next();
3332 let mut len = String::new();
3333 while let Some(&c) = chars.peek() {
3334 if c == '}' {
3335 break;
3336 }
3337 len.push(chars.next().unwrap());
3338 }
3339 Some(len)
3340 } else {
3341 None
3342 };
3343 if chars.peek() == Some(&'}') {
3344 chars.next();
3345 }
3346 push_part!(WordPart::ArraySlice {
3347 name: var_name,
3348 offset,
3349 length,
3350 });
3351 }
3352 } else if matches!(next_c, '-' | '+' | '=' | '?') {
3353 let arr_name = format!("{}[{}]", var_name, index);
3355 let op_char = chars.next().unwrap();
3356 let operand = self.read_brace_operand(&mut chars);
3357 let operator = match op_char {
3358 '-' => ParameterOp::UseDefault,
3359 '=' => ParameterOp::AssignDefault,
3360 '+' => ParameterOp::UseReplacement,
3361 '?' => ParameterOp::Error,
3362 _ => unreachable!(),
3363 };
3364 push_part!(WordPart::ParameterExpansion {
3365 name: arr_name,
3366 operator,
3367 operand,
3368 colon_variant: false,
3369 });
3370 } else {
3371 if chars.peek() == Some(&'}') {
3373 chars.next();
3374 }
3375 push_part!(WordPart::ArrayAccess {
3376 name: var_name,
3377 index,
3378 });
3379 }
3380 } else {
3381 push_part!(WordPart::ArrayAccess {
3382 name: var_name,
3383 index,
3384 });
3385 }
3386 } else if let Some(&c) = chars.peek() {
3387 match c {
3389 ':' => {
3390 chars.next(); match chars.peek() {
3392 Some(&'-') | Some(&'=') | Some(&'+') | Some(&'?') => {
3393 let op_char = chars.next().unwrap();
3394 let operand = self.read_brace_operand(&mut chars);
3395 let operator = match op_char {
3396 '-' => ParameterOp::UseDefault,
3397 '=' => ParameterOp::AssignDefault,
3398 '+' => ParameterOp::UseReplacement,
3399 '?' => ParameterOp::Error,
3400 _ => unreachable!(),
3401 };
3402 push_part!(WordPart::ParameterExpansion {
3403 name: var_name,
3404 operator,
3405 operand,
3406 colon_variant: true,
3407 });
3408 }
3409 _ => {
3410 let mut offset = String::new();
3412 while let Some(&ch) = chars.peek() {
3413 if ch == ':' || ch == '}' {
3414 break;
3415 }
3416 offset.push(chars.next().unwrap());
3417 }
3418 let length = if chars.peek() == Some(&':') {
3419 chars.next(); let mut len = String::new();
3421 while let Some(&ch) = chars.peek() {
3422 if ch == '}' {
3423 break;
3424 }
3425 len.push(chars.next().unwrap());
3426 }
3427 Some(len)
3428 } else {
3429 None
3430 };
3431 if chars.peek() == Some(&'}') {
3432 chars.next();
3433 }
3434 push_part!(WordPart::Substring {
3435 name: var_name,
3436 offset,
3437 length,
3438 });
3439 }
3440 }
3441 }
3442 '-' | '=' | '+' | '?' => {
3444 let op_char = chars.next().unwrap();
3445 let operand = self.read_brace_operand(&mut chars);
3446 let operator = match op_char {
3447 '-' => ParameterOp::UseDefault,
3448 '=' => ParameterOp::AssignDefault,
3449 '+' => ParameterOp::UseReplacement,
3450 '?' => ParameterOp::Error,
3451 _ => unreachable!(),
3452 };
3453 push_part!(WordPart::ParameterExpansion {
3454 name: var_name,
3455 operator,
3456 operand,
3457 colon_variant: false,
3458 });
3459 }
3460 '#' => {
3461 chars.next();
3462 if chars.peek() == Some(&'#') {
3463 chars.next();
3464 let op = self.read_brace_operand(&mut chars);
3465 push_part!(WordPart::ParameterExpansion {
3466 name: var_name,
3467 operator: ParameterOp::RemovePrefixLong,
3468 operand: op,
3469 colon_variant: false,
3470 });
3471 } else {
3472 let op = self.read_brace_operand(&mut chars);
3473 push_part!(WordPart::ParameterExpansion {
3474 name: var_name,
3475 operator: ParameterOp::RemovePrefixShort,
3476 operand: op,
3477 colon_variant: false,
3478 });
3479 }
3480 }
3481 '%' => {
3482 chars.next();
3483 if chars.peek() == Some(&'%') {
3484 chars.next();
3485 let op = self.read_brace_operand(&mut chars);
3486 push_part!(WordPart::ParameterExpansion {
3487 name: var_name,
3488 operator: ParameterOp::RemoveSuffixLong,
3489 operand: op,
3490 colon_variant: false,
3491 });
3492 } else {
3493 let op = self.read_brace_operand(&mut chars);
3494 push_part!(WordPart::ParameterExpansion {
3495 name: var_name,
3496 operator: ParameterOp::RemoveSuffixShort,
3497 operand: op,
3498 colon_variant: false,
3499 });
3500 }
3501 }
3502 '/' => {
3503 chars.next();
3504 let replace_all = if chars.peek() == Some(&'/') {
3505 chars.next();
3506 true
3507 } else {
3508 false
3509 };
3510 let mut pattern = String::new();
3511 while let Some(&ch) = chars.peek() {
3512 if ch == '/' || ch == '}' {
3513 break;
3514 }
3515 if ch == '\\' {
3516 chars.next();
3517 if let Some(&next) = chars.peek()
3518 && next == '/'
3519 {
3520 pattern.push(chars.next().unwrap());
3521 continue;
3522 }
3523 pattern.push('\\');
3524 continue;
3525 }
3526 pattern.push(chars.next().unwrap());
3527 }
3528 let replacement = if chars.peek() == Some(&'/') {
3529 chars.next();
3530 let mut repl = String::new();
3531 while let Some(&ch) = chars.peek() {
3532 if ch == '}' {
3533 break;
3534 }
3535 repl.push(chars.next().unwrap());
3536 }
3537 repl
3538 } else {
3539 String::new()
3540 };
3541 if chars.peek() == Some(&'}') {
3542 chars.next();
3543 }
3544 let op = if replace_all {
3545 ParameterOp::ReplaceAll {
3546 pattern,
3547 replacement,
3548 }
3549 } else {
3550 ParameterOp::ReplaceFirst {
3551 pattern,
3552 replacement,
3553 }
3554 };
3555 push_part!(WordPart::ParameterExpansion {
3556 name: var_name,
3557 operator: op,
3558 operand: String::new(),
3559 colon_variant: false,
3560 });
3561 }
3562 '^' => {
3563 chars.next();
3564 let op = if chars.peek() == Some(&'^') {
3565 chars.next();
3566 ParameterOp::UpperAll
3567 } else {
3568 ParameterOp::UpperFirst
3569 };
3570 if chars.peek() == Some(&'}') {
3571 chars.next();
3572 }
3573 push_part!(WordPart::ParameterExpansion {
3574 name: var_name,
3575 operator: op,
3576 operand: String::new(),
3577 colon_variant: false,
3578 });
3579 }
3580 ',' => {
3581 chars.next();
3582 let op = if chars.peek() == Some(&',') {
3583 chars.next();
3584 ParameterOp::LowerAll
3585 } else {
3586 ParameterOp::LowerFirst
3587 };
3588 if chars.peek() == Some(&'}') {
3589 chars.next();
3590 }
3591 push_part!(WordPart::ParameterExpansion {
3592 name: var_name,
3593 operator: op,
3594 operand: String::new(),
3595 colon_variant: false,
3596 });
3597 }
3598 '@' => {
3599 chars.next();
3600 if let Some(&op) = chars.peek() {
3601 chars.next();
3602 if chars.peek() == Some(&'}') {
3603 chars.next();
3604 }
3605 push_part!(WordPart::Transformation {
3606 name: var_name,
3607 operator: op,
3608 });
3609 } else {
3610 if chars.peek() == Some(&'}') {
3611 chars.next();
3612 }
3613 push_part!(WordPart::Variable(var_name));
3614 }
3615 }
3616 '}' => {
3617 chars.next();
3618 if !var_name.is_empty() {
3619 push_part!(WordPart::Variable(var_name));
3620 }
3621 }
3622 _ => {
3623 while let Some(&ch) = chars.peek() {
3624 if ch == '}' {
3625 chars.next();
3626 break;
3627 }
3628 chars.next();
3629 }
3630 if !var_name.is_empty() {
3631 push_part!(WordPart::Variable(var_name));
3632 }
3633 }
3634 }
3635 } else if !var_name.is_empty() {
3636 push_part!(WordPart::Variable(var_name));
3637 }
3638 }
3639 } else if let Some(&c) = chars.peek() {
3640 if matches!(c, '?' | '#' | '@' | '*' | '!' | '$' | '-') || c.is_ascii_digit() {
3642 push_part!(WordPart::Variable(chars.next().unwrap().to_string()));
3643 } else {
3644 let mut var_name = String::new();
3646 while let Some(&c) = chars.peek() {
3647 if c.is_ascii_alphanumeric() || c == '_' {
3648 var_name.push(chars.next().unwrap());
3649 } else {
3650 break;
3651 }
3652 }
3653 if !var_name.is_empty() {
3654 push_part!(WordPart::Variable(var_name));
3655 } else {
3656 current.push('$');
3658 }
3659 }
3660 } else {
3661 current.push('$');
3663 }
3664 } else {
3665 current.push(ch);
3666 }
3667 }
3668
3669 if !current.is_empty() {
3671 push_part!(WordPart::Literal(current));
3672 }
3673
3674 if parts.is_empty() {
3676 push_part!(WordPart::Literal(String::new()));
3677 }
3678
3679 Word {
3680 parts,
3681 quoted: false,
3682 has_unquoted_glob: false,
3683 part_quoted,
3684 }
3685 }
3686
3687 fn read_brace_operand(&self, chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
3689 let mut operand = String::new();
3690 let mut depth = 1; while let Some(&c) = chars.peek() {
3692 if c == '{' {
3693 depth += 1;
3694 operand.push(chars.next().unwrap());
3695 } else if c == '}' {
3696 depth -= 1;
3697 if depth == 0 {
3698 chars.next(); break;
3700 }
3701 operand.push(chars.next().unwrap());
3702 } else {
3703 operand.push(chars.next().unwrap());
3704 }
3705 }
3706 operand
3707 }
3708}
3709
3710#[cfg(test)]
3711mod tests {
3712 use super::*;
3713 use std::time::Duration;
3714
3715 #[test]
3716 fn test_parse_simple_command() {
3717 let parser = Parser::new("echo hello");
3718 let script = parser.parse().unwrap();
3719
3720 assert_eq!(script.commands.len(), 1);
3721
3722 if let Command::Simple(cmd) = &script.commands[0] {
3723 assert_eq!(cmd.name.to_string(), "echo");
3724 assert_eq!(cmd.args.len(), 1);
3725 assert_eq!(cmd.args[0].to_string(), "hello");
3726 } else {
3727 panic!("expected simple command");
3728 }
3729 }
3730
3731 #[test]
3732 fn test_parse_timeout_exceeded() {
3733 let parser =
3734 Parser::with_limits_and_timeout("echo hello", 100, 100_000, Some(Duration::ZERO));
3735 let err = parser.parse().expect_err("expected parser timeout");
3736 assert!(matches!(
3737 err,
3738 Error::ResourceLimit(LimitExceeded::ParserTimeout(timeout)) if timeout == Duration::ZERO
3739 ));
3740 }
3741
3742 #[test]
3743 fn test_parse_multiple_args() {
3744 let parser = Parser::new("echo hello world");
3745 let script = parser.parse().unwrap();
3746
3747 if let Command::Simple(cmd) = &script.commands[0] {
3748 assert_eq!(cmd.name.to_string(), "echo");
3749 assert_eq!(cmd.args.len(), 2);
3750 assert_eq!(cmd.args[0].to_string(), "hello");
3751 assert_eq!(cmd.args[1].to_string(), "world");
3752 } else {
3753 panic!("expected simple command");
3754 }
3755 }
3756
3757 #[test]
3758 fn test_parse_variable() {
3759 let parser = Parser::new("echo $HOME");
3760 let script = parser.parse().unwrap();
3761
3762 if let Command::Simple(cmd) = &script.commands[0] {
3763 assert_eq!(cmd.args.len(), 1);
3764 assert_eq!(cmd.args[0].parts.len(), 1);
3765 assert!(matches!(&cmd.args[0].parts[0], WordPart::Variable(v) if v == "HOME"));
3766 } else {
3767 panic!("expected simple command");
3768 }
3769 }
3770
3771 #[test]
3772 fn test_parse_pipeline() {
3773 let parser = Parser::new("echo hello | cat");
3774 let script = parser.parse().unwrap();
3775
3776 assert_eq!(script.commands.len(), 1);
3777 assert!(matches!(&script.commands[0], Command::Pipeline(_)));
3778
3779 if let Command::Pipeline(pipeline) = &script.commands[0] {
3780 assert_eq!(pipeline.commands.len(), 2);
3781 }
3782 }
3783
3784 #[test]
3785 fn test_parse_redirect_out() {
3786 let parser = Parser::new("echo hello > /tmp/out");
3787 let script = parser.parse().unwrap();
3788
3789 if let Command::Simple(cmd) = &script.commands[0] {
3790 assert_eq!(cmd.redirects.len(), 1);
3791 assert_eq!(cmd.redirects[0].kind, RedirectKind::Output);
3792 assert_eq!(cmd.redirects[0].target.to_string(), "/tmp/out");
3793 } else {
3794 panic!("expected simple command");
3795 }
3796 }
3797
3798 #[test]
3799 fn test_parse_redirect_append() {
3800 let parser = Parser::new("echo hello >> /tmp/out");
3801 let script = parser.parse().unwrap();
3802
3803 if let Command::Simple(cmd) = &script.commands[0] {
3804 assert_eq!(cmd.redirects.len(), 1);
3805 assert_eq!(cmd.redirects[0].kind, RedirectKind::Append);
3806 } else {
3807 panic!("expected simple command");
3808 }
3809 }
3810
3811 #[test]
3812 fn test_parse_redirect_in() {
3813 let parser = Parser::new("cat < /tmp/in");
3814 let script = parser.parse().unwrap();
3815
3816 if let Command::Simple(cmd) = &script.commands[0] {
3817 assert_eq!(cmd.redirects.len(), 1);
3818 assert_eq!(cmd.redirects[0].kind, RedirectKind::Input);
3819 } else {
3820 panic!("expected simple command");
3821 }
3822 }
3823
3824 #[test]
3825 fn test_parse_command_list_and() {
3826 let parser = Parser::new("true && echo success");
3827 let script = parser.parse().unwrap();
3828
3829 assert!(matches!(&script.commands[0], Command::List(_)));
3830 }
3831
3832 #[test]
3833 fn test_parse_command_list_or() {
3834 let parser = Parser::new("false || echo fallback");
3835 let script = parser.parse().unwrap();
3836
3837 assert!(matches!(&script.commands[0], Command::List(_)));
3838 }
3839
3840 #[test]
3841 fn test_heredoc_pipe() {
3842 let parser = Parser::new("cat <<EOF | sort\nc\na\nb\nEOF\n");
3843 let script = parser.parse().unwrap();
3844 assert!(
3845 matches!(&script.commands[0], Command::Pipeline(_)),
3846 "heredoc with pipe should parse as Pipeline"
3847 );
3848 }
3849
3850 #[test]
3851 fn test_heredoc_multiple_on_line() {
3852 let input = "while cat <<E1 && cat <<E2; do cat <<E3; break; done\n1\nE1\n2\nE2\n3\nE3\n";
3853 let parser = Parser::new(input);
3854 let script = parser.parse().unwrap();
3855 assert_eq!(script.commands.len(), 1);
3856 if let Command::Compound(comp, _) = &script.commands[0] {
3857 if let CompoundCommand::While(w) = comp {
3858 assert!(
3859 !w.condition.is_empty(),
3860 "while condition should be non-empty"
3861 );
3862 assert!(!w.body.is_empty(), "while body should be non-empty");
3863 } else {
3864 panic!("expected While compound command");
3865 }
3866 } else {
3867 panic!("expected Compound command");
3868 }
3869 }
3870
3871 #[test]
3872 fn test_empty_function_body_rejected() {
3873 let parser = Parser::new("f() { }");
3874 assert!(
3875 parser.parse().is_err(),
3876 "empty function body should be rejected"
3877 );
3878 }
3879
3880 #[test]
3881 fn test_empty_while_body_rejected() {
3882 let parser = Parser::new("while true; do\ndone");
3883 assert!(
3884 parser.parse().is_err(),
3885 "empty while body should be rejected"
3886 );
3887 }
3888
3889 #[test]
3890 fn test_empty_for_body_rejected() {
3891 let parser = Parser::new("for i in 1 2 3; do\ndone");
3892 assert!(parser.parse().is_err(), "empty for body should be rejected");
3893 }
3894
3895 #[test]
3896 fn test_empty_if_then_rejected() {
3897 let parser = Parser::new("if true; then\nfi");
3898 assert!(
3899 parser.parse().is_err(),
3900 "empty then clause should be rejected"
3901 );
3902 }
3903
3904 #[test]
3905 fn test_empty_else_rejected() {
3906 let parser = Parser::new("if false; then echo yes; else\nfi");
3907 assert!(
3908 parser.parse().is_err(),
3909 "empty else clause should be rejected"
3910 );
3911 }
3912
3913 #[test]
3914 fn test_unterminated_single_quote_rejected() {
3915 let parser = Parser::new("echo 'unterminated");
3916 assert!(
3917 parser.parse().is_err(),
3918 "unterminated single quote should be rejected"
3919 );
3920 }
3921
3922 #[test]
3923 fn test_unterminated_double_quote_rejected() {
3924 let parser = Parser::new("echo \"unterminated");
3925 assert!(
3926 parser.parse().is_err(),
3927 "unterminated double quote should be rejected"
3928 );
3929 }
3930
3931 #[test]
3932 fn test_leading_pipe_rejected() {
3933 let parser = Parser::new("| cat");
3934 assert!(parser.parse().is_err(), "leading | should be rejected");
3935 }
3936
3937 #[test]
3938 fn test_leading_and_rejected() {
3939 let parser = Parser::new("&& echo hi");
3940 assert!(parser.parse().is_err(), "leading && should be rejected");
3941 }
3942
3943 #[test]
3944 fn test_leading_or_rejected() {
3945 let parser = Parser::new("|| echo hi");
3946 assert!(parser.parse().is_err(), "leading || should be rejected");
3947 }
3948
3949 #[test]
3950 fn test_nonempty_function_body_accepted() {
3951 let parser = Parser::new("f() { echo hi; }");
3952 assert!(
3953 parser.parse().is_ok(),
3954 "non-empty function body should be accepted"
3955 );
3956 }
3957
3958 #[test]
3959 fn test_nonempty_while_body_accepted() {
3960 let parser = Parser::new("while true; do echo hi; done");
3961 assert!(
3962 parser.parse().is_ok(),
3963 "non-empty while body should be accepted"
3964 );
3965 }
3966
3967 #[test]
3969 fn test_nested_expansion_in_array_subscript() {
3970 let parser = Parser::new("echo ${arr[$RANDOM % ${#arr[@]}]}");
3973 let script = parser.parse().unwrap();
3974 assert_eq!(script.commands.len(), 1);
3975 if let Command::Simple(cmd) = &script.commands[0] {
3976 assert_eq!(cmd.name.to_string(), "echo");
3977 assert_eq!(cmd.args.len(), 1);
3978 let arg = &cmd.args[0];
3980 let has_array_access = arg.parts.iter().any(|p| {
3981 matches!(
3982 p,
3983 WordPart::ArrayAccess { name, index }
3984 if name == "arr" && index.contains("${#arr[@]}")
3985 )
3986 });
3987 assert!(
3988 has_array_access,
3989 "expected ArrayAccess with nested index, got: {:?}",
3990 arg.parts
3991 );
3992 } else {
3993 panic!("expected simple command");
3994 }
3995 }
3996
3997 #[test]
3999 fn test_assignment_nested_subscript_parses() {
4000 let parser = Parser::new("x=${arr[$RANDOM % ${#arr[@]}]}");
4001 assert!(
4002 parser.parse().is_ok(),
4003 "assignment with nested subscript should parse"
4004 );
4005 }
4006
4007 #[test]
4008 fn test_ansi_c_quoted_dollar_is_literal_and_quoted() {
4009 let parser = Parser::new("echo $'$(printf pwned)'");
4010 let script = parser.parse().unwrap();
4011 if let Command::Simple(cmd) = &script.commands[0] {
4012 assert_eq!(cmd.args.len(), 1);
4013 let arg = &cmd.args[0];
4014 assert!(arg.quoted, "ANSI-C quoted argument must be quoted");
4015 assert!(
4016 arg.parts
4017 .iter()
4018 .all(|part| matches!(part, WordPart::Literal(_))),
4019 "ANSI-C quoted argument must remain literal, got {:?}",
4020 arg.parts
4021 );
4022 assert_eq!(arg.to_string(), "$(printf pwned)");
4023 } else {
4024 panic!("expected simple command");
4025 }
4026 }
4027
4028 #[test]
4029 fn test_ansi_c_quoted_nul_before_dollar_stays_literal() {
4030 for input in [
4031 "echo $'\\0$(printf pwned)'",
4032 "echo $'\\x00$(printf pwned)'",
4033 "echo $'\\u0000${SECRET}'",
4034 "echo $'\\U00000000${SECRET}'",
4035 ] {
4036 let parser = Parser::new(input);
4037 let script = parser.parse().unwrap();
4038 if let Command::Simple(cmd) = &script.commands[0] {
4039 assert_eq!(cmd.args.len(), 1);
4040 let arg = &cmd.args[0];
4041 assert!(arg.quoted, "ANSI-C quoted argument must be quoted: {input}");
4042 assert!(
4043 arg.parts
4044 .iter()
4045 .all(|part| matches!(part, WordPart::Literal(_))),
4046 "ANSI-C quoted NUL must not expose expansions for {input}: {:?}",
4047 arg.parts
4048 );
4049 assert!(
4050 arg.to_string().starts_with('\0'),
4051 "decoded ANSI-C NUL should remain literal for {input}"
4052 );
4053 } else {
4054 panic!("expected simple command");
4055 }
4056 }
4057 }
4058
4059 #[test]
4060 fn test_single_quoted_segment_concatenation_stays_literal() {
4061 let parser = Parser::new("echo foo'$(id)'");
4062 let script = parser.parse().unwrap();
4063
4064 if let Command::Simple(cmd) = &script.commands[0] {
4065 assert_eq!(cmd.args.len(), 1);
4066 assert_eq!(cmd.args[0].to_string(), "foo$(id)");
4067 assert!(
4068 cmd.args[0]
4069 .parts
4070 .iter()
4071 .all(|p| matches!(p, WordPart::Literal(_))),
4072 "single-quoted segment should not produce expansions: {:?}",
4073 cmd.args[0].parts
4074 );
4075 } else {
4076 panic!("expected simple command");
4077 }
4078 }
4079
4080 #[test]
4081 fn test_locale_quote_marks_word_as_quoted_without_expansion() {
4082 let parser = Parser::new("echo $\"*.txt\"");
4083 let script = parser.parse().unwrap();
4084 if let Command::Simple(cmd) = &script.commands[0] {
4085 assert_eq!(cmd.args.len(), 1);
4086 assert!(
4087 cmd.args[0].quoted,
4088 "locale-quoted argument must be marked quoted"
4089 );
4090 assert_eq!(cmd.args[0].to_string(), "*.txt");
4091 } else {
4092 panic!("expected simple command");
4093 }
4094 }
4095
4096 #[test]
4097 fn test_case_literal_pattern_escaped_dollar_continuation_stays_literal() {
4098 let parser =
4099 Parser::new(r#"case "xy" in 'x'"\$(echo y)") echo MATCH ;; *) echo NOMATCH ;; esac"#);
4100 let script = parser.parse().unwrap();
4101
4102 let case = match &script.commands[0] {
4103 Command::Compound(CompoundCommand::Case(case), _) => case,
4104 other => panic!("expected case command, got: {other:?}"),
4105 };
4106 let pattern = &case.cases[0].patterns[0];
4107 assert_eq!(pattern.to_string(), r#"x$(echo y)"#);
4108 assert!(
4109 pattern
4110 .parts
4111 .iter()
4112 .all(|part| matches!(part, WordPart::Literal(_))),
4113 "escaped dollar in literal case pattern must not parse as expansion: {:?}",
4114 pattern.parts
4115 );
4116 }
4117
4118 #[test]
4119 fn test_single_quoted_assignment_value_stays_literal() {
4120 let parser = Parser::new("VAR='$(id)'");
4121 let script = parser.parse().unwrap();
4122
4123 if let Command::Simple(cmd) = &script.commands[0] {
4124 assert_eq!(cmd.assignments.len(), 1);
4125 assert_eq!(cmd.assignments[0].name, "VAR");
4126 match &cmd.assignments[0].value {
4127 AssignmentValue::Scalar(word) => {
4128 assert_eq!(word.to_string(), "$(id)");
4129 assert!(
4130 word.parts.iter().all(|p| matches!(p, WordPart::Literal(_))),
4131 "single-quoted assignment should remain literal: {:?}",
4132 word.parts
4133 );
4134 }
4135 AssignmentValue::Array(_) => panic!("expected scalar assignment"),
4136 }
4137 } else {
4138 panic!("expected simple command");
4139 }
4140 }
4141
4142 #[test]
4143 fn test_assignment_with_plus_equal_in_value_parses_as_assignment() {
4144 let parser = Parser::new("VAR=a+=b");
4145 let script = parser.parse().expect("script should parse");
4146 let cmd = match &script.commands[0] {
4147 Command::Simple(cmd) => cmd,
4148 other => panic!("expected simple command, got: {other:?}"),
4149 };
4150 assert_eq!(cmd.assignments.len(), 1);
4151 assert_eq!(cmd.assignments[0].name, "VAR");
4152 assert!(!cmd.assignments[0].append);
4153 match &cmd.assignments[0].value {
4154 AssignmentValue::Scalar(word) => assert_eq!(word.to_string(), "a+=b"),
4155 AssignmentValue::Array(_) => panic!("expected scalar assignment"),
4156 }
4157 }
4158
4159 #[test]
4160 fn test_array_append_assignment_with_equal_in_subscript_parses_as_assignment() {
4161 let parser = Parser::new("arr[i=0]+=x");
4162 let script = parser.parse().expect("script should parse");
4163 let cmd = match &script.commands[0] {
4164 Command::Simple(cmd) => cmd,
4165 other => panic!("expected simple command, got: {other:?}"),
4166 };
4167 assert_eq!(cmd.assignments.len(), 1);
4168 assert_eq!(cmd.assignments[0].name, "arr");
4169 assert_eq!(cmd.assignments[0].index.as_deref(), Some("i=0"));
4170 assert!(cmd.assignments[0].append);
4171 match &cmd.assignments[0].value {
4172 AssignmentValue::Scalar(word) => assert_eq!(word.to_string(), "x"),
4173 AssignmentValue::Array(_) => panic!("expected scalar assignment"),
4174 }
4175 }
4176
4177 #[test]
4178 fn test_assoc_append_assignment_with_equal_in_subscript_parses_as_assignment() {
4179 let parser = Parser::new("assoc[key=value]+=x");
4180 let script = parser.parse().expect("script should parse");
4181 let cmd = match &script.commands[0] {
4182 Command::Simple(cmd) => cmd,
4183 other => panic!("expected simple command, got: {other:?}"),
4184 };
4185 assert_eq!(cmd.assignments.len(), 1);
4186 assert_eq!(cmd.assignments[0].name, "assoc");
4187 assert_eq!(cmd.assignments[0].index.as_deref(), Some("key=value"));
4188 assert!(cmd.assignments[0].append);
4189 match &cmd.assignments[0].value {
4190 AssignmentValue::Scalar(word) => assert_eq!(word.to_string(), "x"),
4191 AssignmentValue::Array(_) => panic!("expected scalar assignment"),
4192 }
4193 }
4194
4195 fn nested_process_substitution(levels: usize) -> String {
4196 let mut script = String::from("cat ");
4197 for _ in 0..levels {
4198 script.push_str("<(cat ");
4199 }
4200 script.push_str("echo x");
4201 for _ in 0..levels {
4202 script.push_str("; )");
4203 }
4204 script
4205 }
4206
4207 #[test]
4208 fn test_nested_process_substitution_within_budget_parses() {
4209 let script = nested_process_substitution(3);
4210 let parser = Parser::with_limits(&script, 8, 1_000);
4211 parser
4212 .parse()
4213 .expect("nested process substitution within budget should parse");
4214 }
4215
4216 #[test]
4217 fn test_nested_process_substitution_consumes_depth_budget() {
4218 let script = nested_process_substitution(5);
4219 let parser = Parser::with_limits(&script, 4, 10_000);
4220 let err = parser
4221 .parse()
4222 .expect_err("nested process substitution must not bypass AST depth");
4223 assert!(
4224 err.to_string().contains("AST nesting too deep"),
4225 "expected AST depth error, got: {err}"
4226 );
4227 }
4228
4229 #[test]
4230 fn test_nested_process_substitution_consumes_fuel_budget() {
4231 let script = nested_process_substitution(8);
4232 let parser = Parser::with_limits(&script, 100, 8);
4233 let err = parser
4234 .parse()
4235 .expect_err("nested process substitution must not get fresh parser fuel");
4236 assert!(
4237 err.to_string().contains("parser fuel exhausted"),
4238 "expected parser fuel error, got: {err}"
4239 );
4240 }
4241
4242 #[test]
4243 fn test_process_substitution_whitespace_body_consumes_fuel_budget() {
4244 let script = format!("cat <({}echo x)", " ".repeat(256));
4245 let parser = Parser::with_limits(&script, 100, 200);
4246 let err = parser
4247 .parse()
4248 .expect_err("process substitution body scanning must consume parser fuel");
4249 assert!(
4250 err.to_string().contains("parser fuel exhausted"),
4251 "expected parser fuel error, got: {err}"
4252 );
4253 }
4254
4255 #[test]
4256 fn test_nested_coproc_respects_ast_depth_limit() {
4257 let parser = Parser::with_limits("coproc coproc echo x", 1, usize::MAX);
4258 let err = parser.parse().unwrap_err();
4259 assert!(
4260 err.to_string().contains("AST nesting too deep"),
4261 "expected controlled AST depth error, got: {err}"
4262 );
4263 }
4264
4265 #[test]
4266 fn test_nested_coproc_consumes_parser_fuel() {
4267 let parser = Parser::with_limits("coproc coproc echo x", 100, 3);
4268 let err = parser.parse().unwrap_err();
4269 assert!(
4270 err.to_string().contains("parser fuel exhausted"),
4271 "expected controlled parser fuel error, got: {err}"
4272 );
4273 }
4274
4275 #[test]
4276 fn test_chained_heredoc_reinjection_consumes_parser_fuel() {
4277 let script = ": <<E && : <<E && : <<E\nE\nE\nE\n";
4278 let err = Parser::with_fuel(script, 12).parse().unwrap_err();
4279 assert!(
4280 err.to_string().contains("parser fuel exhausted"),
4281 "expected heredoc rest-of-line reinjection to consume parser fuel, got: {err}"
4282 );
4283 }
4284
4285 #[test]
4286 fn test_array_subscript_single_double_quote_character_does_not_panic() {
4287 let parser = Parser::new(r#"echo "${arr[\"]}""#);
4288 let result = parser.parse();
4289
4290 assert!(
4291 result.is_ok(),
4292 "single-character quoted subscript should not panic: {result:?}"
4293 );
4294 }
4295
4296 #[test]
4297 fn test_double_quoted_param_expansion_obeys_parser_depth_limit() {
4298 let parser = Parser::with_limits(r#"echo "${a:-${b:-${c}}}""#, 2, usize::MAX);
4299 let err = parser
4300 .parse()
4301 .expect_err("nested parameter expansion should be rejected");
4302
4303 assert!(
4304 err.to_string()
4305 .contains("parameter expansion nesting too deep"),
4306 "expected parameter expansion depth error, got: {err}"
4307 );
4308 }
4309
4310 #[test]
4311 fn test_top_level_reserved_word_errors_immediately() {
4312 let parser = Parser::with_fuel("fi", usize::MAX);
4313 let err = parser.parse().unwrap_err();
4314 assert!(
4315 err.to_string().contains("unexpected token"),
4316 "expected immediate syntax error, got: {err}"
4317 );
4318 }
4319}