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};
28
29const DEFAULT_MAX_AST_DEPTH: usize = 100;
31
32const HARD_MAX_AST_DEPTH: usize = 100;
42
43const DEFAULT_MAX_PARSER_OPERATIONS: usize = 100_000;
45
46pub struct Parser<'a> {
48 lexer: Lexer<'a>,
49 current_token: Option<tokens::Token>,
50 current_span: Span,
52 peeked_token: Option<SpannedToken>,
54 max_depth: usize,
56 current_depth: usize,
58 fuel: usize,
60 max_fuel: usize,
62}
63
64impl<'a> Parser<'a> {
65 pub fn new(input: &'a str) -> Self {
67 Self::with_limits(input, DEFAULT_MAX_AST_DEPTH, DEFAULT_MAX_PARSER_OPERATIONS)
68 }
69
70 pub fn with_max_depth(input: &'a str, max_depth: usize) -> Self {
72 Self::with_limits(input, max_depth, DEFAULT_MAX_PARSER_OPERATIONS)
73 }
74
75 pub fn with_fuel(input: &'a str, max_fuel: usize) -> Self {
77 Self::with_limits(input, DEFAULT_MAX_AST_DEPTH, max_fuel)
78 }
79
80 pub fn with_limits(input: &'a str, max_depth: usize, max_fuel: usize) -> Self {
86 let mut lexer = Lexer::with_max_subst_depth(input, max_depth.min(HARD_MAX_AST_DEPTH));
87 let spanned = lexer.next_spanned_token();
88 let (current_token, current_span) = match spanned {
89 Some(st) => (Some(st.token), st.span),
90 None => (None, Span::new()),
91 };
92 Self {
93 lexer,
94 current_token,
95 current_span,
96 peeked_token: None,
97 max_depth: max_depth.min(HARD_MAX_AST_DEPTH),
98 current_depth: 0,
99 fuel: max_fuel,
100 max_fuel,
101 }
102 }
103
104 pub fn current_span(&self) -> Span {
106 self.current_span
107 }
108
109 pub fn parse_word_string(input: &str) -> Word {
112 let parser = Parser::new(input);
113 parser.parse_word(input.to_string())
114 }
115
116 pub fn parse_word_string_with_limits(input: &str, max_depth: usize, max_fuel: usize) -> Word {
119 let parser = Parser::with_limits(input, max_depth, max_fuel);
120 parser.parse_word(input.to_string())
121 }
122
123 fn error(&self, message: impl Into<String>) -> Error {
125 Error::parse_at(
126 message,
127 self.current_span.start.line,
128 self.current_span.start.column,
129 )
130 }
131
132 fn tick(&mut self) -> Result<()> {
134 if self.fuel == 0 {
135 let used = self.max_fuel;
136 return Err(Error::parse(format!(
137 "parser fuel exhausted ({} operations, max {})",
138 used, self.max_fuel
139 )));
140 }
141 self.fuel -= 1;
142 Ok(())
143 }
144
145 fn push_depth(&mut self) -> Result<()> {
147 self.current_depth += 1;
148 if self.current_depth > self.max_depth {
149 return Err(Error::parse(format!(
150 "AST nesting too deep ({} levels, max {})",
151 self.current_depth, self.max_depth
152 )));
153 }
154 Ok(())
155 }
156
157 fn pop_depth(&mut self) {
159 if self.current_depth > 0 {
160 self.current_depth -= 1;
161 }
162 }
163
164 fn check_error_token(&self) -> Result<()> {
166 if let Some(tokens::Token::Error(msg)) = &self.current_token {
167 return Err(self.error(format!("syntax error: {}", msg)));
168 }
169 Ok(())
170 }
171
172 pub fn parse(mut self) -> Result<Script> {
174 self.check_error_token()?;
176
177 let start_span = self.current_span;
178 let mut commands = Vec::new();
179
180 while self.current_token.is_some() {
181 self.tick()?;
182 self.skip_newlines()?;
183 self.check_error_token()?;
184 if self.current_token.is_none() {
185 break;
186 }
187 if let Some(cmd) = self.parse_command_list()? {
188 commands.push(cmd);
189 }
190 }
191
192 let end_span = self.current_span;
193 Ok(Script {
194 commands,
195 span: start_span.merge(end_span),
196 })
197 }
198
199 fn advance(&mut self) {
200 if let Some(peeked) = self.peeked_token.take() {
201 self.current_token = Some(peeked.token);
202 self.current_span = peeked.span;
203 } else {
204 match self.lexer.next_spanned_token() {
205 Some(st) => {
206 self.current_token = Some(st.token);
207 self.current_span = st.span;
208 }
209 None => {
210 self.current_token = None;
211 }
213 }
214 }
215 }
216
217 fn peek_next(&mut self) -> Option<&tokens::Token> {
219 if self.peeked_token.is_none() {
220 self.peeked_token = self.lexer.next_spanned_token();
221 }
222 self.peeked_token.as_ref().map(|st| &st.token)
223 }
224
225 fn skip_newlines(&mut self) -> Result<()> {
226 while matches!(self.current_token, Some(tokens::Token::Newline)) {
227 self.tick()?;
228 self.advance();
229 }
230 Ok(())
231 }
232
233 fn parse_command_list(&mut self) -> Result<Option<Command>> {
235 self.tick()?;
236 let start_span = self.current_span;
237 let first = match self.parse_pipeline()? {
238 Some(cmd) => cmd,
239 None => return Ok(None),
240 };
241
242 let mut rest = Vec::new();
243
244 loop {
245 let op = match &self.current_token {
246 Some(tokens::Token::And) => {
247 self.advance();
248 ListOperator::And
249 }
250 Some(tokens::Token::Or) => {
251 self.advance();
252 ListOperator::Or
253 }
254 Some(tokens::Token::Semicolon) => {
255 self.advance();
256 self.skip_newlines()?;
257 if self.current_token.is_none()
259 || matches!(self.current_token, Some(tokens::Token::Newline))
260 {
261 break;
262 }
263 ListOperator::Semicolon
264 }
265 Some(tokens::Token::Background) => {
266 self.advance();
267 self.skip_newlines()?;
268 if self.current_token.is_none()
270 || matches!(self.current_token, Some(tokens::Token::Newline))
271 {
272 rest.push((
274 ListOperator::Background,
275 Command::Simple(SimpleCommand {
276 name: Word::literal(""),
277 args: vec![],
278 redirects: vec![],
279 assignments: vec![],
280 span: self.current_span,
281 }),
282 ));
283 break;
284 }
285 ListOperator::Background
286 }
287 _ => break,
288 };
289
290 self.skip_newlines()?;
291
292 if let Some(cmd) = self.parse_pipeline()? {
293 rest.push((op, cmd));
294 } else {
295 break;
296 }
297 }
298
299 if rest.is_empty() {
300 Ok(Some(first))
301 } else {
302 Ok(Some(Command::List(CommandList {
303 first: Box::new(first),
304 rest,
305 span: start_span.merge(self.current_span),
306 })))
307 }
308 }
309
310 fn parse_pipeline(&mut self) -> Result<Option<Command>> {
314 let start_span = self.current_span;
315
316 let negated = match &self.current_token {
318 Some(tokens::Token::Word(w)) if w == "!" => {
319 self.advance();
320 true
321 }
322 _ => false,
323 };
324
325 let first = match self.parse_command()? {
326 Some(cmd) => cmd,
327 None => {
328 if negated {
329 return Err(self.error("expected command after !"));
330 }
331 return Ok(None);
332 }
333 };
334
335 let mut commands = vec![first];
336
337 while matches!(self.current_token, Some(tokens::Token::Pipe)) {
338 self.advance();
339 self.skip_newlines()?;
340
341 if let Some(cmd) = self.parse_command()? {
342 commands.push(cmd);
343 } else {
344 return Err(self.error("expected command after |"));
345 }
346 }
347
348 if commands.len() == 1 && !negated {
349 Ok(Some(commands.remove(0)))
350 } else {
351 Ok(Some(Command::Pipeline(Pipeline {
352 negated,
353 commands,
354 span: start_span.merge(self.current_span),
355 })))
356 }
357 }
358
359 fn parse_trailing_redirects(&mut self) -> Vec<Redirect> {
361 let mut redirects = Vec::new();
362 loop {
363 match &self.current_token {
364 Some(tokens::Token::RedirectOut) | Some(tokens::Token::Clobber) => {
365 let kind = if matches!(&self.current_token, Some(tokens::Token::Clobber)) {
366 RedirectKind::Clobber
367 } else {
368 RedirectKind::Output
369 };
370 self.advance();
371 if let Ok(target) = self.expect_word() {
372 redirects.push(Redirect {
373 fd: None,
374 kind,
375 target,
376 });
377 }
378 }
379 Some(tokens::Token::RedirectAppend) => {
380 self.advance();
381 if let Ok(target) = self.expect_word() {
382 redirects.push(Redirect {
383 fd: None,
384 kind: RedirectKind::Append,
385 target,
386 });
387 }
388 }
389 Some(tokens::Token::RedirectIn) => {
390 self.advance();
391 if let Ok(target) = self.expect_word() {
392 redirects.push(Redirect {
393 fd: None,
394 kind: RedirectKind::Input,
395 target,
396 });
397 }
398 }
399 Some(tokens::Token::RedirectBoth) => {
400 self.advance();
401 if let Ok(target) = self.expect_word() {
402 redirects.push(Redirect {
403 fd: None,
404 kind: RedirectKind::OutputBoth,
405 target,
406 });
407 }
408 }
409 Some(tokens::Token::DupOutput) => {
410 self.advance();
411 if let Ok(target) = self.expect_word() {
412 redirects.push(Redirect {
413 fd: Some(1),
414 kind: RedirectKind::DupOutput,
415 target,
416 });
417 }
418 }
419 Some(tokens::Token::RedirectFd(fd)) => {
420 let fd = *fd;
421 self.advance();
422 if let Ok(target) = self.expect_word() {
423 redirects.push(Redirect {
424 fd: Some(fd),
425 kind: RedirectKind::Output,
426 target,
427 });
428 }
429 }
430 Some(tokens::Token::RedirectFdAppend(fd)) => {
431 let fd = *fd;
432 self.advance();
433 if let Ok(target) = self.expect_word() {
434 redirects.push(Redirect {
435 fd: Some(fd),
436 kind: RedirectKind::Append,
437 target,
438 });
439 }
440 }
441 Some(tokens::Token::DupFd(src_fd, dst_fd)) => {
442 let src_fd = *src_fd;
443 let dst_fd = *dst_fd;
444 self.advance();
445 redirects.push(Redirect {
446 fd: Some(src_fd),
447 kind: RedirectKind::DupOutput,
448 target: Word::literal(dst_fd.to_string()),
449 });
450 }
451 Some(tokens::Token::DupInput) => {
452 self.advance();
453 if let Ok(target) = self.expect_word() {
454 redirects.push(Redirect {
455 fd: Some(0),
456 kind: RedirectKind::DupInput,
457 target,
458 });
459 }
460 }
461 Some(tokens::Token::DupFdIn(src_fd, dst_fd)) => {
462 let src_fd = *src_fd;
463 let dst_fd = *dst_fd;
464 self.advance();
465 redirects.push(Redirect {
466 fd: Some(src_fd),
467 kind: RedirectKind::DupInput,
468 target: Word::literal(dst_fd.to_string()),
469 });
470 }
471 Some(tokens::Token::DupFdClose(fd)) => {
472 let fd = *fd;
473 self.advance();
474 redirects.push(Redirect {
475 fd: Some(fd),
476 kind: RedirectKind::DupInput,
477 target: Word::literal("-"),
478 });
479 }
480 Some(tokens::Token::RedirectFdIn(fd)) => {
481 let fd = *fd;
482 self.advance();
483 if let Ok(target) = self.expect_word() {
484 redirects.push(Redirect {
485 fd: Some(fd),
486 kind: RedirectKind::Input,
487 target,
488 });
489 }
490 }
491 Some(tokens::Token::HereString) => {
492 self.advance();
493 if let Ok(target) = self.expect_word() {
494 redirects.push(Redirect {
495 fd: None,
496 kind: RedirectKind::HereString,
497 target,
498 });
499 }
500 }
501 Some(tokens::Token::HereDoc) | Some(tokens::Token::HereDocStrip) => {
502 let strip_tabs =
503 matches!(self.current_token, Some(tokens::Token::HereDocStrip));
504 self.advance();
505 let (delimiter, quoted) = match &self.current_token {
506 Some(tokens::Token::Word(w)) => (w.clone(), false),
507 Some(tokens::Token::LiteralWord(w)) => (w.clone(), true),
508 Some(tokens::Token::QuotedWord(w)) => (w.clone(), true),
509 _ => break,
510 };
511 let content = self.lexer.read_heredoc(&delimiter);
512 let content = if strip_tabs {
513 let had_trailing_newline = content.ends_with('\n');
514 let mut stripped: String = content
515 .lines()
516 .map(|l| l.trim_start_matches('\t'))
517 .collect::<Vec<_>>()
518 .join("\n");
519 if had_trailing_newline {
520 stripped.push('\n');
521 }
522 stripped
523 } else {
524 content
525 };
526 self.advance();
527 let target = if quoted {
528 Word::quoted_literal(content)
529 } else {
530 self.parse_word(content)
531 };
532 let kind = if strip_tabs {
533 RedirectKind::HereDocStrip
534 } else {
535 RedirectKind::HereDoc
536 };
537 redirects.push(Redirect {
538 fd: None,
539 kind,
540 target,
541 });
542 break;
545 }
546 _ => break,
547 }
548 }
549 redirects
550 }
551
552 fn parse_compound_with_redirects(
554 &mut self,
555 parser: impl FnOnce(&mut Self) -> Result<CompoundCommand>,
556 ) -> Result<Option<Command>> {
557 let compound = parser(self)?;
558 let redirects = self.parse_trailing_redirects();
559 Ok(Some(Command::Compound(compound, redirects)))
560 }
561
562 fn parse_command(&mut self) -> Result<Option<Command>> {
564 self.skip_newlines()?;
565 self.check_error_token()?;
566
567 if let Some(tokens::Token::Word(w)) = &self.current_token {
569 let word = w.clone();
570 match word.as_str() {
571 "if" => return self.parse_compound_with_redirects(|s| s.parse_if()),
572 "for" => return self.parse_compound_with_redirects(|s| s.parse_for()),
573 "while" => return self.parse_compound_with_redirects(|s| s.parse_while()),
574 "until" => return self.parse_compound_with_redirects(|s| s.parse_until()),
575 "case" => return self.parse_compound_with_redirects(|s| s.parse_case()),
576 "select" => return self.parse_compound_with_redirects(|s| s.parse_select()),
577 "time" => return self.parse_compound_with_redirects(|s| s.parse_time()),
578 "coproc" => return self.parse_compound_with_redirects(|s| s.parse_coproc()),
579 "function" => return self.parse_function_keyword().map(Some),
580 _ => {
581 if !word.contains('=')
584 && matches!(self.peek_next(), Some(tokens::Token::LeftParen))
585 {
586 return self.parse_function_posix().map(Some);
587 }
588 }
589 }
590 }
591
592 if matches!(self.current_token, Some(tokens::Token::DoubleLeftBracket)) {
594 return self.parse_compound_with_redirects(|s| s.parse_conditional());
595 }
596
597 if matches!(self.current_token, Some(tokens::Token::DoubleLeftParen)) {
599 return self.parse_compound_with_redirects(|s| s.parse_arithmetic_command());
600 }
601
602 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
604 return self.parse_compound_with_redirects(|s| s.parse_subshell());
605 }
606
607 if matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
609 return self.parse_compound_with_redirects(|s| s.parse_brace_group());
610 }
611
612 match self.parse_simple_command()? {
614 Some(cmd) => Ok(Some(Command::Simple(cmd))),
615 None => Ok(None),
616 }
617 }
618
619 fn parse_if(&mut self) -> Result<CompoundCommand> {
621 let start_span = self.current_span;
622 self.push_depth()?;
623 self.advance(); self.skip_newlines()?;
625
626 let condition = self.parse_compound_list("then")?;
628
629 self.expect_keyword("then")?;
631 self.skip_newlines()?;
632
633 let then_branch = self.parse_compound_list_until(&["elif", "else", "fi"])?;
635
636 if then_branch.is_empty() {
638 self.pop_depth();
639 return Err(self.error("syntax error: empty then clause"));
640 }
641
642 let mut elif_branches = Vec::new();
644 while self.is_keyword("elif") {
645 self.advance(); self.skip_newlines()?;
647
648 let elif_condition = self.parse_compound_list("then")?;
649 self.expect_keyword("then")?;
650 self.skip_newlines()?;
651
652 let elif_body = self.parse_compound_list_until(&["elif", "else", "fi"])?;
653
654 if elif_body.is_empty() {
656 self.pop_depth();
657 return Err(self.error("syntax error: empty elif clause"));
658 }
659
660 elif_branches.push((elif_condition, elif_body));
661 }
662
663 let else_branch = if self.is_keyword("else") {
665 self.advance(); self.skip_newlines()?;
667 let branch = self.parse_compound_list("fi")?;
668
669 if branch.is_empty() {
671 self.pop_depth();
672 return Err(self.error("syntax error: empty else clause"));
673 }
674
675 Some(branch)
676 } else {
677 None
678 };
679
680 self.expect_keyword("fi")?;
682
683 self.pop_depth();
684 Ok(CompoundCommand::If(IfCommand {
685 condition,
686 then_branch,
687 elif_branches,
688 else_branch,
689 span: start_span.merge(self.current_span),
690 }))
691 }
692
693 fn parse_for(&mut self) -> Result<CompoundCommand> {
695 let start_span = self.current_span;
696 self.push_depth()?;
697 self.advance(); self.skip_newlines()?;
699
700 if matches!(self.current_token, Some(tokens::Token::DoubleLeftParen)) {
702 let result = self.parse_arithmetic_for_inner(start_span);
703 self.pop_depth();
704 return result;
705 }
706
707 let variable = match &self.current_token {
709 Some(tokens::Token::Word(w))
710 | Some(tokens::Token::LiteralWord(w))
711 | Some(tokens::Token::QuotedWord(w)) => w.clone(),
712 _ => {
713 self.pop_depth();
714 return Err(Error::parse(
715 "expected variable name in for loop".to_string(),
716 ));
717 }
718 };
719 self.advance();
720
721 let words = if self.is_keyword("in") {
723 self.advance(); let mut words = Vec::new();
727 loop {
728 match &self.current_token {
729 Some(tokens::Token::Word(w)) if w == "do" => break,
730 Some(tokens::Token::Word(w)) | Some(tokens::Token::QuotedWord(w)) => {
731 let is_quoted =
732 matches!(&self.current_token, Some(tokens::Token::QuotedWord(_)));
733 let mut word = self.parse_word(w.clone());
734 if is_quoted {
735 word.quoted = true;
736 }
737 words.push(word);
738 self.advance();
739 }
740 Some(tokens::Token::LiteralWord(w)) => {
741 words.push(Word {
742 parts: vec![WordPart::Literal(w.clone())],
743 quoted: true,
744 });
745 self.advance();
746 }
747 Some(tokens::Token::Newline) | Some(tokens::Token::Semicolon) => {
748 self.advance();
749 break;
750 }
751 _ => break,
752 }
753 }
754 Some(words)
755 } else {
756 if matches!(self.current_token, Some(tokens::Token::Semicolon)) {
759 self.advance();
760 }
761 None
762 };
763
764 self.skip_newlines()?;
765
766 self.expect_keyword("do")?;
768 self.skip_newlines()?;
769
770 let body = self.parse_compound_list("done")?;
772
773 if body.is_empty() {
775 self.pop_depth();
776 return Err(self.error("syntax error: empty for loop body"));
777 }
778
779 self.expect_keyword("done")?;
781
782 self.pop_depth();
783 Ok(CompoundCommand::For(ForCommand {
784 variable,
785 words,
786 body,
787 span: start_span.merge(self.current_span),
788 }))
789 }
790
791 fn parse_select(&mut self) -> Result<CompoundCommand> {
793 let start_span = self.current_span;
794 self.push_depth()?;
795 self.advance(); self.skip_newlines()?;
797
798 let variable = match &self.current_token {
800 Some(tokens::Token::Word(w))
801 | Some(tokens::Token::LiteralWord(w))
802 | Some(tokens::Token::QuotedWord(w)) => w.clone(),
803 _ => {
804 self.pop_depth();
805 return Err(Error::parse("expected variable name in select".to_string()));
806 }
807 };
808 self.advance();
809
810 if !self.is_keyword("in") {
812 self.pop_depth();
813 return Err(Error::parse("expected 'in' in select".to_string()));
814 }
815 self.advance(); let mut words = Vec::new();
819 loop {
820 match &self.current_token {
821 Some(tokens::Token::Word(w)) if w == "do" => break,
822 Some(tokens::Token::Word(w)) | Some(tokens::Token::QuotedWord(w)) => {
823 let is_quoted =
824 matches!(&self.current_token, Some(tokens::Token::QuotedWord(_)));
825 let mut word = self.parse_word(w.clone());
826 if is_quoted {
827 word.quoted = true;
828 }
829 words.push(word);
830 self.advance();
831 }
832 Some(tokens::Token::LiteralWord(w)) => {
833 words.push(Word {
834 parts: vec![WordPart::Literal(w.clone())],
835 quoted: true,
836 });
837 self.advance();
838 }
839 Some(tokens::Token::Newline) | Some(tokens::Token::Semicolon) => {
840 self.advance();
841 break;
842 }
843 _ => break,
844 }
845 }
846
847 self.skip_newlines()?;
848
849 self.expect_keyword("do")?;
851 self.skip_newlines()?;
852
853 let body = self.parse_compound_list("done")?;
855
856 if body.is_empty() {
858 self.pop_depth();
859 return Err(self.error("syntax error: empty select loop body"));
860 }
861
862 self.expect_keyword("done")?;
864
865 self.pop_depth();
866 Ok(CompoundCommand::Select(SelectCommand {
867 variable,
868 words,
869 body,
870 span: start_span.merge(self.current_span),
871 }))
872 }
873
874 fn parse_arithmetic_for_inner(&mut self, start_span: Span) -> Result<CompoundCommand> {
877 self.advance(); let mut parts: Vec<String> = Vec::new();
881 let mut current_expr = String::new();
882 let mut paren_depth = 0;
883
884 loop {
885 match &self.current_token {
886 Some(tokens::Token::DoubleRightParen) => {
887 parts.push(current_expr.trim().to_string());
889 self.advance();
890 break;
891 }
892 Some(tokens::Token::LeftParen) => {
893 paren_depth += 1;
894 current_expr.push('(');
895 self.advance();
896 }
897 Some(tokens::Token::RightParen) => {
898 if paren_depth > 0 {
899 paren_depth -= 1;
900 current_expr.push(')');
901 self.advance();
902 } else {
903 self.advance();
905 }
906 }
907 Some(tokens::Token::Semicolon) => {
908 if paren_depth == 0 {
909 parts.push(current_expr.trim().to_string());
911 current_expr.clear();
912 } else {
913 current_expr.push(';');
914 }
915 self.advance();
916 }
917 Some(tokens::Token::Word(w))
918 | Some(tokens::Token::LiteralWord(w))
919 | Some(tokens::Token::QuotedWord(w)) => {
920 let skip_space = current_expr.ends_with('<')
922 || current_expr.ends_with('>')
923 || current_expr.ends_with(' ')
924 || current_expr.ends_with('(')
925 || current_expr.is_empty();
926 if !skip_space {
927 current_expr.push(' ');
928 }
929 current_expr.push_str(w);
930 self.advance();
931 }
932 Some(tokens::Token::Newline) => {
933 self.advance();
934 }
935 Some(tokens::Token::RedirectIn) => {
937 current_expr.push('<');
938 self.advance();
939 }
940 Some(tokens::Token::RedirectOut) => {
941 current_expr.push('>');
942 self.advance();
943 }
944 Some(tokens::Token::And) => {
945 current_expr.push_str("&&");
946 self.advance();
947 }
948 Some(tokens::Token::Or) => {
949 current_expr.push_str("||");
950 self.advance();
951 }
952 Some(tokens::Token::Pipe) => {
953 current_expr.push('|');
954 self.advance();
955 }
956 Some(tokens::Token::Background) => {
957 current_expr.push('&');
958 self.advance();
959 }
960 None => {
961 return Err(Error::parse(
962 "unexpected end of input in for loop".to_string(),
963 ));
964 }
965 _ => {
966 self.advance();
967 }
968 }
969 }
970
971 while parts.len() < 3 {
973 parts.push(String::new());
974 }
975
976 let init = parts.first().cloned().unwrap_or_default();
977 let condition = parts.get(1).cloned().unwrap_or_default();
978 let step = parts.get(2).cloned().unwrap_or_default();
979
980 self.skip_newlines()?;
981
982 if matches!(self.current_token, Some(tokens::Token::Semicolon)) {
984 self.advance();
985 }
986 self.skip_newlines()?;
987
988 self.expect_keyword("do")?;
990 self.skip_newlines()?;
991
992 let body = self.parse_compound_list("done")?;
994
995 if body.is_empty() {
997 return Err(self.error("syntax error: empty for loop body"));
998 }
999
1000 self.expect_keyword("done")?;
1002
1003 Ok(CompoundCommand::ArithmeticFor(ArithmeticForCommand {
1004 init,
1005 condition,
1006 step,
1007 body,
1008 span: start_span.merge(self.current_span),
1009 }))
1010 }
1011
1012 fn parse_while(&mut self) -> Result<CompoundCommand> {
1014 let start_span = self.current_span;
1015 self.push_depth()?;
1016 self.advance(); self.skip_newlines()?;
1018
1019 let condition = self.parse_compound_list("do")?;
1021
1022 self.expect_keyword("do")?;
1024 self.skip_newlines()?;
1025
1026 let body = self.parse_compound_list("done")?;
1028
1029 if body.is_empty() {
1031 self.pop_depth();
1032 return Err(self.error("syntax error: empty while loop body"));
1033 }
1034
1035 self.expect_keyword("done")?;
1037
1038 self.pop_depth();
1039 Ok(CompoundCommand::While(WhileCommand {
1040 condition,
1041 body,
1042 span: start_span.merge(self.current_span),
1043 }))
1044 }
1045
1046 fn parse_until(&mut self) -> Result<CompoundCommand> {
1048 let start_span = self.current_span;
1049 self.push_depth()?;
1050 self.advance(); self.skip_newlines()?;
1052
1053 let condition = self.parse_compound_list("do")?;
1055
1056 self.expect_keyword("do")?;
1058 self.skip_newlines()?;
1059
1060 let body = self.parse_compound_list("done")?;
1062
1063 if body.is_empty() {
1065 self.pop_depth();
1066 return Err(self.error("syntax error: empty until loop body"));
1067 }
1068
1069 self.expect_keyword("done")?;
1071
1072 self.pop_depth();
1073 Ok(CompoundCommand::Until(UntilCommand {
1074 condition,
1075 body,
1076 span: start_span.merge(self.current_span),
1077 }))
1078 }
1079
1080 fn parse_case(&mut self) -> Result<CompoundCommand> {
1082 let start_span = self.current_span;
1083 self.push_depth()?;
1084 self.advance(); self.skip_newlines()?;
1086
1087 let word = self.expect_word()?;
1089 self.skip_newlines()?;
1090
1091 self.expect_keyword("in")?;
1093 self.skip_newlines()?;
1094
1095 let mut cases = Vec::new();
1097 while !self.is_keyword("esac") && self.current_token.is_some() {
1098 self.skip_newlines()?;
1099 if self.is_keyword("esac") {
1100 break;
1101 }
1102
1103 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1106 self.advance();
1107 }
1108
1109 let mut patterns = Vec::new();
1110 while matches!(
1111 &self.current_token,
1112 Some(tokens::Token::Word(_))
1113 | Some(tokens::Token::LiteralWord(_))
1114 | Some(tokens::Token::QuotedWord(_))
1115 ) {
1116 let w = match &self.current_token {
1117 Some(tokens::Token::Word(w))
1118 | Some(tokens::Token::LiteralWord(w))
1119 | Some(tokens::Token::QuotedWord(w)) => w.clone(),
1120 _ => unreachable!(),
1121 };
1122 patterns.push(self.parse_word(w));
1123 self.advance();
1124
1125 if matches!(self.current_token, Some(tokens::Token::Pipe)) {
1127 self.advance();
1128 } else {
1129 break;
1130 }
1131 }
1132
1133 if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1135 self.pop_depth();
1136 return Err(self.error("expected ')' after case pattern"));
1137 }
1138 self.advance();
1139 self.skip_newlines()?;
1140
1141 let mut commands = Vec::new();
1143 while !self.is_case_terminator()
1144 && !self.is_keyword("esac")
1145 && self.current_token.is_some()
1146 {
1147 if let Some(cmd) = self.parse_command_list()? {
1148 commands.push(cmd);
1149 }
1150 self.skip_newlines()?;
1151 }
1152
1153 let terminator = self.parse_case_terminator();
1154 cases.push(CaseItem {
1155 patterns,
1156 commands,
1157 terminator,
1158 });
1159 self.skip_newlines()?;
1160 }
1161
1162 self.expect_keyword("esac")?;
1164
1165 self.pop_depth();
1166 Ok(CompoundCommand::Case(CaseCommand {
1167 word,
1168 cases,
1169 span: start_span.merge(self.current_span),
1170 }))
1171 }
1172
1173 fn parse_time(&mut self) -> Result<CompoundCommand> {
1178 let start_span = self.current_span;
1179 self.advance(); self.skip_newlines()?;
1181
1182 let posix_format = if let Some(tokens::Token::Word(w)) = &self.current_token {
1184 if w == "-p" {
1185 self.advance();
1186 self.skip_newlines()?;
1187 true
1188 } else {
1189 false
1190 }
1191 } else {
1192 false
1193 };
1194
1195 let command = self.parse_pipeline()?;
1198
1199 Ok(CompoundCommand::Time(TimeCommand {
1200 posix_format,
1201 command: command.map(Box::new),
1202 span: start_span.merge(self.current_span),
1203 }))
1204 }
1205
1206 fn parse_coproc(&mut self) -> Result<CompoundCommand> {
1213 let start_span = self.current_span;
1214 self.advance(); self.skip_newlines()?;
1216
1217 let (name, consumed_name) = if let Some(tokens::Token::Word(w)) = &self.current_token {
1220 let word = w.clone();
1221 let is_compound_keyword = matches!(
1222 word.as_str(),
1223 "if" | "for" | "while" | "until" | "case" | "select" | "time" | "coproc"
1224 );
1225 let next_is_compound_start = matches!(
1226 self.peek_next(),
1227 Some(tokens::Token::LeftBrace) | Some(tokens::Token::LeftParen)
1228 );
1229 if !is_compound_keyword && next_is_compound_start {
1230 self.advance(); self.skip_newlines()?;
1232 (word, true)
1233 } else {
1234 ("COPROC".to_string(), false)
1235 }
1236 } else {
1237 ("COPROC".to_string(), false)
1238 };
1239
1240 let _ = consumed_name;
1241
1242 let body = self.parse_pipeline()?;
1244 let body = body.ok_or_else(|| self.error("coproc: missing command"))?;
1245
1246 Ok(CompoundCommand::Coproc(ast::CoprocCommand {
1247 name,
1248 body: Box::new(body),
1249 span: start_span.merge(self.current_span),
1250 }))
1251 }
1252
1253 fn is_case_terminator(&self) -> bool {
1255 matches!(
1256 self.current_token,
1257 Some(tokens::Token::DoubleSemicolon)
1258 | Some(tokens::Token::SemiAmp)
1259 | Some(tokens::Token::DoubleSemiAmp)
1260 )
1261 }
1262
1263 fn parse_case_terminator(&mut self) -> ast::CaseTerminator {
1265 match self.current_token {
1266 Some(tokens::Token::SemiAmp) => {
1267 self.advance();
1268 ast::CaseTerminator::FallThrough
1269 }
1270 Some(tokens::Token::DoubleSemiAmp) => {
1271 self.advance();
1272 ast::CaseTerminator::Continue
1273 }
1274 Some(tokens::Token::DoubleSemicolon) => {
1275 self.advance();
1276 ast::CaseTerminator::Break
1277 }
1278 _ => ast::CaseTerminator::Break,
1279 }
1280 }
1281
1282 fn parse_subshell(&mut self) -> Result<CompoundCommand> {
1284 self.push_depth()?;
1285 self.advance(); self.skip_newlines()?;
1287
1288 let mut commands = Vec::new();
1289 while !matches!(
1290 self.current_token,
1291 Some(tokens::Token::RightParen) | Some(tokens::Token::DoubleRightParen) | None
1292 ) {
1293 self.skip_newlines()?;
1294 if matches!(
1295 self.current_token,
1296 Some(tokens::Token::RightParen) | Some(tokens::Token::DoubleRightParen)
1297 ) {
1298 break;
1299 }
1300 if let Some(cmd) = self.parse_command_list()? {
1301 commands.push(cmd);
1302 }
1303 }
1304
1305 if matches!(self.current_token, Some(tokens::Token::DoubleRightParen)) {
1306 self.current_token = Some(tokens::Token::RightParen);
1308 } else if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1309 self.pop_depth();
1310 return Err(Error::parse("expected ')' to close subshell".to_string()));
1311 } else {
1312 self.advance(); }
1314
1315 self.pop_depth();
1316 Ok(CompoundCommand::Subshell(commands))
1317 }
1318
1319 fn parse_brace_group(&mut self) -> Result<CompoundCommand> {
1321 self.push_depth()?;
1322 self.advance(); self.skip_newlines()?;
1324
1325 let mut commands = Vec::new();
1326 while !matches!(self.current_token, Some(tokens::Token::RightBrace) | None) {
1327 self.skip_newlines()?;
1328 if matches!(self.current_token, Some(tokens::Token::RightBrace)) {
1329 break;
1330 }
1331 if let Some(cmd) = self.parse_command_list()? {
1332 commands.push(cmd);
1333 }
1334 }
1335
1336 if !matches!(self.current_token, Some(tokens::Token::RightBrace)) {
1337 self.pop_depth();
1338 return Err(Error::parse(
1339 "expected '}' to close brace group".to_string(),
1340 ));
1341 }
1342
1343 if commands.is_empty() {
1345 self.pop_depth();
1346 return Err(self.error("syntax error: empty brace group"));
1347 }
1348
1349 self.advance(); self.pop_depth();
1352 Ok(CompoundCommand::BraceGroup(commands))
1353 }
1354
1355 fn parse_conditional(&mut self) -> Result<CompoundCommand> {
1358 self.advance(); let mut words = Vec::new();
1361 let mut saw_regex_op = false;
1362
1363 loop {
1364 match &self.current_token {
1365 Some(tokens::Token::DoubleRightBracket) => {
1366 self.advance(); break;
1368 }
1369 Some(tokens::Token::Word(w))
1370 | Some(tokens::Token::LiteralWord(w))
1371 | Some(tokens::Token::QuotedWord(w)) => {
1372 let w_clone = w.clone();
1373 let is_quoted =
1374 matches!(self.current_token, Some(tokens::Token::QuotedWord(_)));
1375 let is_literal =
1376 matches!(self.current_token, Some(tokens::Token::LiteralWord(_)));
1377
1378 if saw_regex_op {
1383 if w_clone.contains('$') && !is_quoted {
1384 let parsed = self.parse_word(w_clone);
1386 words.push(parsed);
1387 self.advance();
1388 } else {
1389 let pattern = self.collect_conditional_regex_pattern(&w_clone);
1390 words.push(Word::literal(&pattern));
1391 }
1392 saw_regex_op = false;
1393 continue;
1394 }
1395
1396 if w_clone == "=~" {
1397 saw_regex_op = true;
1398 }
1399
1400 let word = if is_literal {
1401 Word {
1402 parts: vec![WordPart::Literal(w_clone)],
1403 quoted: true,
1404 }
1405 } else {
1406 let mut parsed = self.parse_word(w_clone);
1407 if is_quoted {
1408 parsed.quoted = true;
1409 }
1410 parsed
1411 };
1412 words.push(word);
1413 self.advance();
1414 }
1415 Some(tokens::Token::And) => {
1417 words.push(Word::literal("&&"));
1418 self.advance();
1419 }
1420 Some(tokens::Token::Or) => {
1421 words.push(Word::literal("||"));
1422 self.advance();
1423 }
1424 Some(tokens::Token::LeftParen) => {
1425 if saw_regex_op {
1426 let pattern = self.collect_conditional_regex_pattern("(");
1428 words.push(Word::literal(&pattern));
1429 saw_regex_op = false;
1430 continue;
1431 }
1432 words.push(Word::literal("("));
1433 self.advance();
1434 }
1435 Some(tokens::Token::RightParen) => {
1436 words.push(Word::literal(")"));
1437 self.advance();
1438 }
1439 None => {
1440 return Err(crate::error::Error::parse(
1441 "unexpected end of input in [[ ]]".to_string(),
1442 ));
1443 }
1444 _ => {
1445 self.advance();
1447 }
1448 }
1449 }
1450
1451 Ok(CompoundCommand::Conditional(words))
1452 }
1453
1454 fn collect_conditional_regex_pattern(&mut self, first_word: &str) -> String {
1456 let mut pattern = first_word.to_string();
1457 self.advance(); loop {
1461 match &self.current_token {
1462 Some(tokens::Token::DoubleRightBracket) => break,
1463 Some(tokens::Token::And) | Some(tokens::Token::Or) => break,
1464 Some(tokens::Token::LeftParen) => {
1465 pattern.push('(');
1466 self.advance();
1467 }
1468 Some(tokens::Token::RightParen) => {
1469 pattern.push(')');
1470 self.advance();
1471 }
1472 Some(tokens::Token::Word(w))
1473 | Some(tokens::Token::LiteralWord(w))
1474 | Some(tokens::Token::QuotedWord(w)) => {
1475 pattern.push_str(w);
1476 self.advance();
1477 }
1478 _ => break,
1479 }
1480 }
1481
1482 pattern
1483 }
1484
1485 fn parse_arithmetic_command(&mut self) -> Result<CompoundCommand> {
1486 self.advance(); let mut expr = String::new();
1490 let mut depth = 1;
1491
1492 loop {
1493 match &self.current_token {
1494 Some(tokens::Token::DoubleLeftParen) => {
1495 depth += 1;
1496 expr.push_str("((");
1497 self.advance();
1498 }
1499 Some(tokens::Token::DoubleRightParen) => {
1500 depth -= 1;
1501 if depth == 0 {
1502 self.advance(); break;
1504 }
1505 expr.push_str("))");
1506 self.advance();
1507 }
1508 Some(tokens::Token::LeftParen) => {
1509 expr.push('(');
1510 self.advance();
1511 }
1512 Some(tokens::Token::RightParen) => {
1513 expr.push(')');
1514 self.advance();
1515 }
1516 Some(tokens::Token::Word(w))
1517 | Some(tokens::Token::LiteralWord(w))
1518 | Some(tokens::Token::QuotedWord(w)) => {
1519 if !expr.is_empty() && !expr.ends_with(' ') && !expr.ends_with('(') {
1520 expr.push(' ');
1521 }
1522 expr.push_str(w);
1523 self.advance();
1524 }
1525 Some(tokens::Token::Semicolon) => {
1526 expr.push(';');
1527 self.advance();
1528 }
1529 Some(tokens::Token::Newline) => {
1530 self.advance();
1531 }
1532 Some(tokens::Token::RedirectIn) => {
1534 expr.push('<');
1535 self.advance();
1536 }
1537 Some(tokens::Token::RedirectOut) => {
1538 expr.push('>');
1539 self.advance();
1540 }
1541 Some(tokens::Token::And) => {
1542 expr.push_str("&&");
1543 self.advance();
1544 }
1545 Some(tokens::Token::Or) => {
1546 expr.push_str("||");
1547 self.advance();
1548 }
1549 Some(tokens::Token::Pipe) => {
1550 expr.push('|');
1551 self.advance();
1552 }
1553 Some(tokens::Token::Background) => {
1554 expr.push('&');
1555 self.advance();
1556 }
1557 None => {
1558 return Err(Error::parse(
1559 "unexpected end of input in arithmetic command".to_string(),
1560 ));
1561 }
1562 _ => {
1563 self.advance();
1564 }
1565 }
1566 }
1567
1568 Ok(CompoundCommand::Arithmetic(expr.trim().to_string()))
1569 }
1570
1571 fn parse_function_keyword(&mut self) -> Result<Command> {
1573 let start_span = self.current_span;
1574 self.advance(); self.skip_newlines()?;
1576
1577 let name = match &self.current_token {
1579 Some(tokens::Token::Word(w)) => w.clone(),
1580 _ => return Err(self.error("expected function name")),
1581 };
1582 self.advance();
1583 self.skip_newlines()?;
1584
1585 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1587 self.advance(); if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1589 return Err(Error::parse(
1590 "expected ')' in function definition".to_string(),
1591 ));
1592 }
1593 self.advance(); self.skip_newlines()?;
1595 }
1596
1597 if !matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
1599 return Err(Error::parse("expected '{' for function body".to_string()));
1600 }
1601
1602 let body = self.parse_brace_group()?;
1604
1605 Ok(Command::Function(FunctionDef {
1606 name,
1607 body: Box::new(Command::Compound(body, Vec::new())),
1608 span: start_span.merge(self.current_span),
1609 }))
1610 }
1611
1612 fn parse_function_posix(&mut self) -> Result<Command> {
1614 let start_span = self.current_span;
1615 let name = match &self.current_token {
1617 Some(tokens::Token::Word(w)) => w.clone(),
1618 _ => return Err(self.error("expected function name")),
1619 };
1620 self.advance();
1621
1622 if !matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1624 return Err(self.error("expected '(' in function definition"));
1625 }
1626 self.advance(); if !matches!(self.current_token, Some(tokens::Token::RightParen)) {
1629 return Err(self.error("expected ')' in function definition"));
1630 }
1631 self.advance(); self.skip_newlines()?;
1633
1634 if !matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
1636 return Err(self.error("expected '{' for function body"));
1637 }
1638
1639 let body = self.parse_brace_group()?;
1641
1642 Ok(Command::Function(FunctionDef {
1643 name,
1644 body: Box::new(Command::Compound(body, Vec::new())),
1645 span: start_span.merge(self.current_span),
1646 }))
1647 }
1648
1649 fn parse_compound_list(&mut self, terminator: &str) -> Result<Vec<Command>> {
1651 self.parse_compound_list_until(&[terminator])
1652 }
1653
1654 fn parse_compound_list_until(&mut self, terminators: &[&str]) -> Result<Vec<Command>> {
1656 let mut commands = Vec::new();
1657
1658 loop {
1659 self.skip_newlines()?;
1660
1661 if let Some(tokens::Token::Word(w)) = &self.current_token
1663 && terminators.contains(&w.as_str())
1664 {
1665 break;
1666 }
1667
1668 if self.current_token.is_none() {
1669 break;
1670 }
1671
1672 if let Some(cmd) = self.parse_command_list()? {
1673 commands.push(cmd);
1674 } else {
1675 break;
1676 }
1677 }
1678
1679 Ok(commands)
1680 }
1681
1682 const NON_COMMAND_WORDS: &'static [&'static str] =
1685 &["then", "else", "elif", "fi", "do", "done", "esac", "in"];
1686
1687 fn is_non_command_word(word: &str) -> bool {
1689 Self::NON_COMMAND_WORDS.contains(&word)
1690 }
1691
1692 fn is_keyword(&self, keyword: &str) -> bool {
1694 matches!(&self.current_token, Some(tokens::Token::Word(w)) if w == keyword)
1695 }
1696
1697 fn expect_keyword(&mut self, keyword: &str) -> Result<()> {
1699 if self.is_keyword(keyword) {
1700 self.advance();
1701 Ok(())
1702 } else {
1703 Err(self.error(format!("expected '{}'", keyword)))
1704 }
1705 }
1706
1707 fn strip_quotes(s: &str) -> &str {
1709 if s.len() >= 2
1710 && ((s.starts_with('"') && s.ends_with('"'))
1711 || (s.starts_with('\'') && s.ends_with('\'')))
1712 {
1713 return &s[1..s.len() - 1];
1714 }
1715 s
1716 }
1717
1718 fn is_assignment(word: &str) -> Option<(&str, Option<&str>, &str, bool)> {
1721 let (eq_pos, is_append) = if let Some(pos) = word.find("+=") {
1723 (pos, true)
1724 } else if let Some(pos) = word.find('=') {
1725 (pos, false)
1726 } else {
1727 return None;
1728 };
1729
1730 let lhs = &word[..eq_pos];
1731 let value = &word[eq_pos + if is_append { 2 } else { 1 }..];
1732
1733 if let Some(bracket_pos) = lhs.find('[') {
1735 let name = &lhs[..bracket_pos];
1736 if name.is_empty() {
1738 return None;
1739 }
1740 let mut chars = name.chars();
1741 let first = chars.next().unwrap();
1742 if !first.is_ascii_alphabetic() && first != '_' {
1743 return None;
1744 }
1745 for c in chars {
1746 if !c.is_ascii_alphanumeric() && c != '_' {
1747 return None;
1748 }
1749 }
1750 if lhs.ends_with(']') {
1752 let index = &lhs[bracket_pos + 1..lhs.len() - 1];
1753 return Some((name, Some(index), value, is_append));
1754 }
1755 } else {
1756 if lhs.is_empty() {
1758 return None;
1759 }
1760 let mut chars = lhs.chars();
1761 let first = chars.next().unwrap();
1762 if !first.is_ascii_alphabetic() && first != '_' {
1763 return None;
1764 }
1765 for c in chars {
1766 if !c.is_ascii_alphanumeric() && c != '_' {
1767 return None;
1768 }
1769 }
1770 return Some((lhs, None, value, is_append));
1771 }
1772 None
1773 }
1774
1775 fn collect_array_elements(&mut self) -> Vec<Word> {
1778 let mut elements = Vec::new();
1779 loop {
1780 match &self.current_token {
1781 Some(tokens::Token::RightParen) => {
1782 self.advance();
1783 break;
1784 }
1785 Some(tokens::Token::Word(elem))
1786 | Some(tokens::Token::LiteralWord(elem))
1787 | Some(tokens::Token::QuotedWord(elem)) => {
1788 let elem_clone = elem.clone();
1789 let word = if matches!(&self.current_token, Some(tokens::Token::LiteralWord(_)))
1790 {
1791 Word {
1792 parts: vec![WordPart::Literal(elem_clone)],
1793 quoted: true,
1794 }
1795 } else {
1796 self.parse_word(elem_clone)
1797 };
1798 elements.push(word);
1799 self.advance();
1800 }
1801 None => break,
1802 _ => {
1803 self.advance();
1804 }
1805 }
1806 }
1807 elements
1808 }
1809
1810 fn try_parse_assignment(&mut self, w: &str) -> Option<(Assignment, bool)> {
1814 let (name, index, value, is_append) = Self::is_assignment(w)?;
1815 let name = name.to_string();
1816 let index = index.map(|s| s.to_string());
1817 let value_str = value.to_string();
1818
1819 if value_str.starts_with('(') && value_str.ends_with(')') {
1821 let inner = &value_str[1..value_str.len() - 1];
1822 let elements: Vec<Word> = inner
1823 .split_whitespace()
1824 .map(|s| self.parse_word(s.to_string()))
1825 .collect();
1826 return Some((
1827 Assignment {
1828 name,
1829 index,
1830 value: AssignmentValue::Array(elements),
1831 append: is_append,
1832 },
1833 true,
1834 ));
1835 }
1836
1837 if value_str.is_empty() {
1839 self.advance();
1840 if matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1841 self.advance(); let elements = self.collect_array_elements();
1843 return Some((
1844 Assignment {
1845 name,
1846 index,
1847 value: AssignmentValue::Array(elements),
1848 append: is_append,
1849 },
1850 false,
1851 ));
1852 }
1853 return Some((
1855 Assignment {
1856 name,
1857 index,
1858 value: AssignmentValue::Scalar(Word::literal("")),
1859 append: is_append,
1860 },
1861 false,
1862 ));
1863 }
1864
1865 let value_word = if value_str.starts_with('"') && value_str.ends_with('"') {
1867 let inner = Self::strip_quotes(&value_str);
1868 let mut w = self.parse_word(inner.to_string());
1869 w.quoted = true;
1870 w
1871 } else if value_str.starts_with('\'') && value_str.ends_with('\'') {
1872 let inner = Self::strip_quotes(&value_str);
1873 Word {
1874 parts: vec![WordPart::Literal(inner.to_string())],
1875 quoted: true,
1876 }
1877 } else {
1878 self.parse_word(value_str)
1879 };
1880 Some((
1881 Assignment {
1882 name,
1883 index,
1884 value: AssignmentValue::Scalar(value_word),
1885 append: is_append,
1886 },
1887 true,
1888 ))
1889 }
1890
1891 fn try_parse_compound_array_arg(&mut self, saved_w: String) -> Option<Word> {
1895 if !matches!(self.current_token, Some(tokens::Token::LeftParen)) {
1896 return None;
1897 }
1898 self.advance(); let mut compound = saved_w;
1900 compound.push('(');
1901 loop {
1902 match &self.current_token {
1903 Some(tokens::Token::RightParen) => {
1904 compound.push(')');
1905 self.advance();
1906 break;
1907 }
1908 Some(tokens::Token::Word(elem))
1909 | Some(tokens::Token::LiteralWord(elem))
1910 | Some(tokens::Token::QuotedWord(elem)) => {
1911 if !compound.ends_with('(') {
1912 compound.push(' ');
1913 }
1914 compound.push_str(elem);
1915 self.advance();
1916 }
1917 None => break,
1918 _ => {
1919 self.advance();
1920 }
1921 }
1922 }
1923 Some(self.parse_word(compound))
1924 }
1925
1926 fn parse_heredoc_redirect(
1928 &mut self,
1929 strip_tabs: bool,
1930 redirects: &mut Vec<Redirect>,
1931 ) -> Result<()> {
1932 self.advance();
1933 let (delimiter, quoted) = match &self.current_token {
1935 Some(tokens::Token::Word(w)) => (w.clone(), false),
1936 Some(tokens::Token::LiteralWord(w)) => (w.clone(), true),
1937 Some(tokens::Token::QuotedWord(w)) => (w.clone(), true),
1938 _ => return Err(Error::parse("expected delimiter after <<".to_string())),
1939 };
1940
1941 let content = self.lexer.read_heredoc(&delimiter);
1942
1943 let content = if strip_tabs {
1945 let had_trailing_newline = content.ends_with('\n');
1946 let mut stripped: String = content
1947 .lines()
1948 .map(|l: &str| l.trim_start_matches('\t'))
1949 .collect::<Vec<_>>()
1950 .join("\n");
1951 if had_trailing_newline {
1952 stripped.push('\n');
1953 }
1954 stripped
1955 } else {
1956 content
1957 };
1958
1959 let target = if quoted {
1960 Word::quoted_literal(content)
1961 } else {
1962 self.parse_word(content)
1963 };
1964
1965 let kind = if strip_tabs {
1966 RedirectKind::HereDocStrip
1967 } else {
1968 RedirectKind::HereDoc
1969 };
1970
1971 redirects.push(Redirect {
1972 fd: None,
1973 kind,
1974 target,
1975 });
1976
1977 self.advance();
1979
1980 self.collect_trailing_redirects(redirects);
1982 Ok(())
1983 }
1984
1985 fn collect_trailing_redirects(&mut self, redirects: &mut Vec<Redirect>) {
1987 while let Some(tok) = &self.current_token {
1988 match tok {
1989 tokens::Token::RedirectOut | tokens::Token::Clobber => {
1990 let kind = if matches!(&self.current_token, Some(tokens::Token::Clobber)) {
1991 RedirectKind::Clobber
1992 } else {
1993 RedirectKind::Output
1994 };
1995 self.advance();
1996 if let Ok(target) = self.expect_word() {
1997 redirects.push(Redirect {
1998 fd: None,
1999 kind,
2000 target,
2001 });
2002 }
2003 }
2004 tokens::Token::RedirectAppend => {
2005 self.advance();
2006 if let Ok(target) = self.expect_word() {
2007 redirects.push(Redirect {
2008 fd: None,
2009 kind: RedirectKind::Append,
2010 target,
2011 });
2012 }
2013 }
2014 tokens::Token::RedirectFd(fd) => {
2015 let fd = *fd;
2016 self.advance();
2017 if let Ok(target) = self.expect_word() {
2018 redirects.push(Redirect {
2019 fd: Some(fd),
2020 kind: RedirectKind::Output,
2021 target,
2022 });
2023 }
2024 }
2025 tokens::Token::DupInput => {
2026 self.advance();
2027 if let Ok(target) = self.expect_word() {
2028 redirects.push(Redirect {
2029 fd: Some(0),
2030 kind: RedirectKind::DupInput,
2031 target,
2032 });
2033 }
2034 }
2035 tokens::Token::DupFdIn(src_fd, dst_fd) => {
2036 let src_fd = *src_fd;
2037 let dst_fd = *dst_fd;
2038 self.advance();
2039 redirects.push(Redirect {
2040 fd: Some(src_fd),
2041 kind: RedirectKind::DupInput,
2042 target: Word::literal(dst_fd.to_string()),
2043 });
2044 }
2045 tokens::Token::DupFdClose(fd) => {
2046 let fd = *fd;
2047 self.advance();
2048 redirects.push(Redirect {
2049 fd: Some(fd),
2050 kind: RedirectKind::DupInput,
2051 target: Word::literal("-"),
2052 });
2053 }
2054 tokens::Token::RedirectFdIn(fd) => {
2055 let fd = *fd;
2056 self.advance();
2057 if let Ok(target) = self.expect_word() {
2058 redirects.push(Redirect {
2059 fd: Some(fd),
2060 kind: RedirectKind::Input,
2061 target,
2062 });
2063 }
2064 }
2065 _ => break,
2066 }
2067 }
2068 }
2069
2070 fn parse_simple_command(&mut self) -> Result<Option<SimpleCommand>> {
2071 self.tick()?;
2072 self.skip_newlines()?;
2073 self.check_error_token()?;
2074 let start_span = self.current_span;
2075
2076 let mut assignments = Vec::new();
2077 let mut words = Vec::new();
2078 let mut redirects = Vec::new();
2079
2080 loop {
2081 match &self.current_token {
2082 Some(tokens::Token::Word(w))
2083 | Some(tokens::Token::LiteralWord(w))
2084 | Some(tokens::Token::QuotedWord(w)) => {
2085 let is_literal =
2086 matches!(&self.current_token, Some(tokens::Token::LiteralWord(_)));
2087 let is_quoted =
2088 matches!(&self.current_token, Some(tokens::Token::QuotedWord(_)));
2089 let w = w.clone();
2091
2092 if words.is_empty() && Self::is_non_command_word(&w) {
2094 break;
2095 }
2096
2097 if words.is_empty()
2099 && !is_literal
2100 && let Some((assignment, needs_advance)) = self.try_parse_assignment(&w)
2101 {
2102 if needs_advance {
2103 self.advance();
2104 }
2105 assignments.push(assignment);
2106 continue;
2107 }
2108
2109 if w.ends_with('=') && !words.is_empty() {
2112 self.advance();
2113 if let Some(word) = self.try_parse_compound_array_arg(w.clone()) {
2114 words.push(word);
2115 continue;
2116 }
2117 let word = if is_literal {
2119 Word {
2120 parts: vec![WordPart::Literal(w)],
2121 quoted: true,
2122 }
2123 } else {
2124 let mut word = self.parse_word(w);
2125 if is_quoted {
2126 word.quoted = true;
2127 }
2128 word
2129 };
2130 words.push(word);
2131 continue;
2132 }
2133
2134 let word = if is_literal {
2135 Word {
2136 parts: vec![WordPart::Literal(w)],
2137 quoted: true,
2138 }
2139 } else {
2140 let mut word = self.parse_word(w);
2141 if is_quoted {
2142 word.quoted = true;
2143 }
2144 word
2145 };
2146 words.push(word);
2147 self.advance();
2148 }
2149 Some(tokens::Token::RedirectOut) | Some(tokens::Token::Clobber) => {
2150 let kind = if matches!(&self.current_token, Some(tokens::Token::Clobber)) {
2151 RedirectKind::Clobber
2152 } else {
2153 RedirectKind::Output
2154 };
2155 self.advance();
2156 let target = self.expect_word()?;
2157 redirects.push(Redirect {
2158 fd: None,
2159 kind,
2160 target,
2161 });
2162 }
2163 Some(tokens::Token::RedirectAppend) => {
2164 self.advance();
2165 let target = self.expect_word()?;
2166 redirects.push(Redirect {
2167 fd: None,
2168 kind: RedirectKind::Append,
2169 target,
2170 });
2171 }
2172 Some(tokens::Token::RedirectIn) => {
2173 self.advance();
2174 let target = self.expect_word()?;
2175 redirects.push(Redirect {
2176 fd: None,
2177 kind: RedirectKind::Input,
2178 target,
2179 });
2180 }
2181 Some(tokens::Token::HereString) => {
2182 self.advance();
2183 let target = self.expect_word()?;
2184 redirects.push(Redirect {
2185 fd: None,
2186 kind: RedirectKind::HereString,
2187 target,
2188 });
2189 }
2190 Some(tokens::Token::HereDoc) | Some(tokens::Token::HereDocStrip) => {
2191 let strip_tabs =
2192 matches!(self.current_token, Some(tokens::Token::HereDocStrip));
2193 self.parse_heredoc_redirect(strip_tabs, &mut redirects)?;
2194 break;
2195 }
2196 Some(tokens::Token::ProcessSubIn) | Some(tokens::Token::ProcessSubOut) => {
2197 let word = self.expect_word()?;
2198 words.push(word);
2199 }
2200 Some(tokens::Token::RedirectBoth) => {
2201 self.advance();
2202 let target = self.expect_word()?;
2203 redirects.push(Redirect {
2204 fd: None,
2205 kind: RedirectKind::OutputBoth,
2206 target,
2207 });
2208 }
2209 Some(tokens::Token::DupOutput) => {
2210 self.advance();
2211 let target = self.expect_word()?;
2212 redirects.push(Redirect {
2213 fd: Some(1),
2214 kind: RedirectKind::DupOutput,
2215 target,
2216 });
2217 }
2218 Some(tokens::Token::RedirectFd(fd)) => {
2219 let fd = *fd;
2220 self.advance();
2221 let target = self.expect_word()?;
2222 redirects.push(Redirect {
2223 fd: Some(fd),
2224 kind: RedirectKind::Output,
2225 target,
2226 });
2227 }
2228 Some(tokens::Token::RedirectFdAppend(fd)) => {
2229 let fd = *fd;
2230 self.advance();
2231 let target = self.expect_word()?;
2232 redirects.push(Redirect {
2233 fd: Some(fd),
2234 kind: RedirectKind::Append,
2235 target,
2236 });
2237 }
2238 Some(tokens::Token::DupFd(src_fd, dst_fd)) => {
2239 let src_fd = *src_fd;
2240 let dst_fd = *dst_fd;
2241 self.advance();
2242 redirects.push(Redirect {
2243 fd: Some(src_fd),
2244 kind: RedirectKind::DupOutput,
2245 target: Word::literal(dst_fd.to_string()),
2246 });
2247 }
2248 Some(tokens::Token::DupInput) => {
2249 self.advance();
2250 let target = self.expect_word()?;
2251 redirects.push(Redirect {
2252 fd: Some(0),
2253 kind: RedirectKind::DupInput,
2254 target,
2255 });
2256 }
2257 Some(tokens::Token::DupFdIn(src_fd, dst_fd)) => {
2258 let src_fd = *src_fd;
2259 let dst_fd = *dst_fd;
2260 self.advance();
2261 redirects.push(Redirect {
2262 fd: Some(src_fd),
2263 kind: RedirectKind::DupInput,
2264 target: Word::literal(dst_fd.to_string()),
2265 });
2266 }
2267 Some(tokens::Token::DupFdClose(fd)) => {
2268 let fd = *fd;
2269 self.advance();
2270 redirects.push(Redirect {
2271 fd: Some(fd),
2272 kind: RedirectKind::DupInput,
2273 target: Word::literal("-"),
2274 });
2275 }
2276 Some(tokens::Token::RedirectFdIn(fd)) => {
2277 let fd = *fd;
2278 self.advance();
2279 let target = self.expect_word()?;
2280 redirects.push(Redirect {
2281 fd: Some(fd),
2282 kind: RedirectKind::Input,
2283 target,
2284 });
2285 }
2286 Some(tokens::Token::LeftBrace) | Some(tokens::Token::RightBrace)
2288 if !words.is_empty() =>
2289 {
2290 let sym = if matches!(self.current_token, Some(tokens::Token::LeftBrace)) {
2291 "{"
2292 } else {
2293 "}"
2294 };
2295 words.push(Word::literal(sym));
2296 self.advance();
2297 }
2298 Some(tokens::Token::Newline)
2299 | Some(tokens::Token::Semicolon)
2300 | Some(tokens::Token::Pipe)
2301 | Some(tokens::Token::And)
2302 | Some(tokens::Token::Or)
2303 | None => break,
2304 _ => break,
2305 }
2306 }
2307
2308 if words.is_empty() && !assignments.is_empty() {
2310 return Ok(Some(SimpleCommand {
2311 name: Word::literal(""),
2312 args: Vec::new(),
2313 redirects,
2314 assignments,
2315 span: start_span.merge(self.current_span),
2316 }));
2317 }
2318
2319 if words.is_empty() {
2320 return Ok(None);
2321 }
2322
2323 let name = words.remove(0);
2324 let args = words;
2325
2326 Ok(Some(SimpleCommand {
2327 name,
2328 args,
2329 redirects,
2330 assignments,
2331 span: start_span.merge(self.current_span),
2332 }))
2333 }
2334
2335 fn expect_word(&mut self) -> Result<Word> {
2337 match &self.current_token {
2338 Some(tokens::Token::Word(w)) => {
2339 let word = self.parse_word(w.clone());
2340 self.advance();
2341 Ok(word)
2342 }
2343 Some(tokens::Token::LiteralWord(w)) => {
2344 let word = Word {
2346 parts: vec![WordPart::Literal(w.clone())],
2347 quoted: true,
2348 };
2349 self.advance();
2350 Ok(word)
2351 }
2352 Some(tokens::Token::QuotedWord(w)) => {
2353 let word = self.parse_word(w.clone());
2355 self.advance();
2356 Ok(word)
2357 }
2358 Some(tokens::Token::ProcessSubIn) | Some(tokens::Token::ProcessSubOut) => {
2359 let is_input = matches!(self.current_token, Some(tokens::Token::ProcessSubIn));
2361 self.advance();
2362
2363 let mut cmd_str = String::new();
2365 let mut depth = 1;
2366 loop {
2367 match &self.current_token {
2368 Some(tokens::Token::LeftParen) => {
2369 depth += 1;
2370 cmd_str.push('(');
2371 self.advance();
2372 }
2373 Some(tokens::Token::RightParen) => {
2374 depth -= 1;
2375 if depth == 0 {
2376 self.advance();
2377 break;
2378 }
2379 cmd_str.push(')');
2380 self.advance();
2381 }
2382 Some(tokens::Token::Word(w)) => {
2383 if !cmd_str.is_empty() {
2384 cmd_str.push(' ');
2385 }
2386 cmd_str.push_str(w);
2387 self.advance();
2388 }
2389 Some(tokens::Token::QuotedWord(w)) => {
2390 if !cmd_str.is_empty() {
2391 cmd_str.push(' ');
2392 }
2393 cmd_str.push('"');
2394 cmd_str.push_str(w);
2395 cmd_str.push('"');
2396 self.advance();
2397 }
2398 Some(tokens::Token::LiteralWord(w)) => {
2399 if !cmd_str.is_empty() {
2400 cmd_str.push(' ');
2401 }
2402 cmd_str.push('\'');
2403 cmd_str.push_str(w);
2404 cmd_str.push('\'');
2405 self.advance();
2406 }
2407 Some(tokens::Token::Pipe) => {
2408 cmd_str.push_str(" | ");
2409 self.advance();
2410 }
2411 Some(tokens::Token::Semicolon) => {
2412 cmd_str.push_str("; ");
2413 self.advance();
2414 }
2415 Some(tokens::Token::And) => {
2416 cmd_str.push_str(" && ");
2417 self.advance();
2418 }
2419 Some(tokens::Token::Or) => {
2420 cmd_str.push_str(" || ");
2421 self.advance();
2422 }
2423 Some(tokens::Token::Background) => {
2424 cmd_str.push_str(" & ");
2425 self.advance();
2426 }
2427 Some(tokens::Token::RedirectOut) => {
2428 cmd_str.push_str(" > ");
2429 self.advance();
2430 }
2431 Some(tokens::Token::RedirectAppend) => {
2432 cmd_str.push_str(" >> ");
2433 self.advance();
2434 }
2435 Some(tokens::Token::RedirectIn) => {
2436 cmd_str.push_str(" < ");
2437 self.advance();
2438 }
2439 Some(tokens::Token::HereString) => {
2440 cmd_str.push_str(" <<< ");
2441 self.advance();
2442 }
2443 Some(tokens::Token::DupOutput) => {
2444 cmd_str.push_str(" >&");
2445 self.advance();
2446 }
2447 Some(tokens::Token::RedirectFd(fd)) => {
2448 cmd_str.push_str(&format!(" {}> ", fd));
2449 self.advance();
2450 }
2451 Some(tokens::Token::Newline) => {
2452 cmd_str.push('\n');
2453 self.advance();
2454 }
2455 None => {
2456 return Err(Error::parse(
2457 "unexpected end of input in process substitution".to_string(),
2458 ));
2459 }
2460 _ => {
2461 self.advance();
2463 }
2464 }
2465 }
2466
2467 let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
2471 let inner_parser = Parser::with_limits(&cmd_str, remaining_depth, self.fuel);
2472 let commands = match inner_parser.parse() {
2473 Ok(script) => script.commands,
2474 Err(_) => Vec::new(),
2475 };
2476
2477 Ok(Word {
2478 parts: vec![WordPart::ProcessSubstitution { commands, is_input }],
2479 quoted: false,
2480 })
2481 }
2482 _ => Err(self.error("expected word")),
2483 }
2484 }
2485
2486 #[allow(dead_code)]
2488 fn current_word_to_word(&self) -> Option<Word> {
2490 match &self.current_token {
2491 Some(tokens::Token::Word(w)) | Some(tokens::Token::QuotedWord(w)) => {
2492 Some(self.parse_word(w.clone()))
2493 }
2494 Some(tokens::Token::LiteralWord(w)) => Some(Word {
2495 parts: vec![WordPart::Literal(w.clone())],
2496 quoted: true,
2497 }),
2498 _ => None,
2499 }
2500 }
2501
2502 #[allow(dead_code)]
2503 fn is_current_word(&self) -> bool {
2505 matches!(
2506 &self.current_token,
2507 Some(tokens::Token::Word(_))
2508 | Some(tokens::Token::LiteralWord(_))
2509 | Some(tokens::Token::QuotedWord(_))
2510 )
2511 }
2512
2513 #[allow(dead_code)]
2514 fn current_word_str(&self) -> Option<String> {
2516 match &self.current_token {
2517 Some(tokens::Token::Word(w))
2518 | Some(tokens::Token::LiteralWord(w))
2519 | Some(tokens::Token::QuotedWord(w)) => Some(w.clone()),
2520 _ => None,
2521 }
2522 }
2523
2524 fn parse_word(&self, s: String) -> Word {
2526 let mut parts = Vec::new();
2527 let mut chars = s.chars().peekable();
2528 let mut current = String::new();
2529
2530 while let Some(ch) = chars.next() {
2531 if ch == '$' {
2532 if !current.is_empty() {
2534 parts.push(WordPart::Literal(std::mem::take(&mut current)));
2535 }
2536
2537 if chars.peek() == Some(&'(') {
2539 chars.next(); if chars.peek() == Some(&'(') {
2543 chars.next(); let mut expr = String::new();
2545 let mut depth = 2;
2546 for c in chars.by_ref() {
2547 if c == '(' {
2548 depth += 1;
2549 expr.push(c);
2550 } else if c == ')' {
2551 depth -= 1;
2552 if depth == 0 {
2553 break;
2554 }
2555 expr.push(c);
2556 } else {
2557 expr.push(c);
2558 }
2559 }
2560 if expr.ends_with(')') {
2562 expr.pop();
2563 }
2564 parts.push(WordPart::ArithmeticExpansion(expr));
2565 } else {
2566 let mut cmd_str = String::new();
2568 let mut depth = 1;
2569 for c in chars.by_ref() {
2570 if c == '(' {
2571 depth += 1;
2572 cmd_str.push(c);
2573 } else if c == ')' {
2574 depth -= 1;
2575 if depth == 0 {
2576 break;
2577 }
2578 cmd_str.push(c);
2579 } else {
2580 cmd_str.push(c);
2581 }
2582 }
2583 let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
2586 let inner_parser =
2587 Parser::with_limits(&cmd_str, remaining_depth, self.fuel);
2588 if let Ok(script) = inner_parser.parse() {
2589 parts.push(WordPart::CommandSubstitution(script.commands));
2590 }
2591 }
2592 } else if chars.peek() == Some(&'{') {
2593 chars.next(); if chars.peek() == Some(&'#') {
2598 chars.next(); let mut var_name = String::new();
2600 while let Some(&c) = chars.peek() {
2601 if c == '}' || c == '[' {
2602 break;
2603 }
2604 var_name.push(chars.next().unwrap());
2605 }
2606 if chars.peek() == Some(&'[') {
2608 chars.next(); let mut index = String::new();
2610 while let Some(&c) = chars.peek() {
2611 if c == ']' {
2612 chars.next();
2613 break;
2614 }
2615 index.push(chars.next().unwrap());
2616 }
2617 if chars.peek() == Some(&'}') {
2619 chars.next();
2620 }
2621 if index == "@" || index == "*" {
2622 parts.push(WordPart::ArrayLength(var_name));
2623 } else {
2624 parts.push(WordPart::Length(format!("{}[{}]", var_name, index)));
2626 }
2627 } else {
2628 if chars.peek() == Some(&'}') {
2630 chars.next();
2631 }
2632 parts.push(WordPart::Length(var_name));
2633 }
2634 } else if chars.peek() == Some(&'!') {
2635 chars.next(); let mut var_name = String::new();
2639 while let Some(&c) = chars.peek() {
2640 if c == '}' || c == '[' || c == '*' || c == '@' {
2641 break;
2642 }
2643 var_name.push(chars.next().unwrap());
2644 }
2645 if chars.peek() == Some(&'[') {
2647 chars.next(); let mut index = String::new();
2649 while let Some(&c) = chars.peek() {
2650 if c == ']' {
2651 chars.next();
2652 break;
2653 }
2654 index.push(chars.next().unwrap());
2655 }
2656 if chars.peek() == Some(&'}') {
2658 chars.next();
2659 }
2660 if index == "@" || index == "*" {
2661 parts.push(WordPart::ArrayIndices(var_name));
2662 } else {
2663 parts.push(WordPart::Variable(format!("!{}[{}]", var_name, index)));
2665 }
2666 } else if chars.peek() == Some(&'}') {
2667 chars.next(); parts.push(WordPart::IndirectExpansion(var_name));
2670 } else {
2671 let mut suffix = String::new();
2673 while let Some(&c) = chars.peek() {
2674 if c == '}' {
2675 chars.next();
2676 break;
2677 }
2678 suffix.push(chars.next().unwrap());
2679 }
2680 if suffix.ends_with('*') || suffix.ends_with('@') {
2682 let full_prefix =
2683 format!("{}{}", var_name, &suffix[..suffix.len() - 1]);
2684 parts.push(WordPart::PrefixMatch(full_prefix));
2685 } else {
2686 parts.push(WordPart::Variable(format!("!{}{}", var_name, suffix)));
2687 }
2688 }
2689 } else {
2690 let mut var_name = String::new();
2692 while let Some(&c) = chars.peek() {
2693 if c.is_ascii_alphanumeric() || c == '_' {
2694 var_name.push(chars.next().unwrap());
2695 } else {
2696 break;
2697 }
2698 }
2699
2700 if var_name.is_empty()
2702 && let Some(&c) = chars.peek()
2703 && matches!(c, '@' | '*')
2704 {
2705 var_name.push(chars.next().unwrap());
2706 }
2707
2708 if chars.peek() == Some(&'[') {
2710 chars.next(); let mut index = String::new();
2712 let mut bracket_depth: i32 = 0;
2716 let mut brace_depth: i32 = 0;
2717 while let Some(&c) = chars.peek() {
2718 if c == ']' && bracket_depth == 0 && brace_depth == 0 {
2719 chars.next();
2720 break;
2721 }
2722 match c {
2723 '[' => bracket_depth += 1,
2724 ']' => bracket_depth -= 1,
2725 '$' => {
2726 index.push(chars.next().unwrap());
2727 if chars.peek() == Some(&'{') {
2728 brace_depth += 1;
2729 index.push(chars.next().unwrap());
2730 continue;
2731 }
2732 continue;
2733 }
2734 '{' => brace_depth += 1,
2735 '}' => {
2736 if brace_depth > 0 {
2737 brace_depth -= 1;
2738 }
2739 }
2740 _ => {}
2741 }
2742 index.push(chars.next().unwrap());
2743 }
2744 if index.len() >= 2
2746 && ((index.starts_with('"') && index.ends_with('"'))
2747 || (index.starts_with('\'') && index.ends_with('\'')))
2748 {
2749 index = index[1..index.len() - 1].to_string();
2750 }
2751 if let Some(&next_c) = chars.peek() {
2753 if next_c == ':' {
2754 let mut lookahead = chars.clone();
2756 lookahead.next(); let is_param_op = matches!(
2758 lookahead.peek(),
2759 Some(&'-') | Some(&'=') | Some(&'+') | Some(&'?')
2760 );
2761 if is_param_op {
2762 chars.next(); let arr_name = format!("{}[{}]", var_name, index);
2764 let op_char = chars.next().unwrap();
2765 let operand = self.read_brace_operand(&mut chars);
2766 let operator = match op_char {
2767 '-' => ParameterOp::UseDefault,
2768 '=' => ParameterOp::AssignDefault,
2769 '+' => ParameterOp::UseReplacement,
2770 '?' => ParameterOp::Error,
2771 _ => unreachable!(),
2772 };
2773 parts.push(WordPart::ParameterExpansion {
2774 name: arr_name,
2775 operator,
2776 operand,
2777 colon_variant: true,
2778 });
2779 } else {
2780 chars.next(); let mut offset = String::new();
2783 while let Some(&c) = chars.peek() {
2784 if c == ':' || c == '}' {
2785 break;
2786 }
2787 offset.push(chars.next().unwrap());
2788 }
2789 let length = if chars.peek() == Some(&':') {
2790 chars.next();
2791 let mut len = String::new();
2792 while let Some(&c) = chars.peek() {
2793 if c == '}' {
2794 break;
2795 }
2796 len.push(chars.next().unwrap());
2797 }
2798 Some(len)
2799 } else {
2800 None
2801 };
2802 if chars.peek() == Some(&'}') {
2803 chars.next();
2804 }
2805 parts.push(WordPart::ArraySlice {
2806 name: var_name,
2807 offset,
2808 length,
2809 });
2810 }
2811 } else if matches!(next_c, '-' | '+' | '=' | '?') {
2812 let arr_name = format!("{}[{}]", var_name, index);
2814 let op_char = chars.next().unwrap();
2815 let operand = self.read_brace_operand(&mut chars);
2816 let operator = match op_char {
2817 '-' => ParameterOp::UseDefault,
2818 '=' => ParameterOp::AssignDefault,
2819 '+' => ParameterOp::UseReplacement,
2820 '?' => ParameterOp::Error,
2821 _ => unreachable!(),
2822 };
2823 parts.push(WordPart::ParameterExpansion {
2824 name: arr_name,
2825 operator,
2826 operand,
2827 colon_variant: false,
2828 });
2829 } else {
2830 if chars.peek() == Some(&'}') {
2832 chars.next();
2833 }
2834 parts.push(WordPart::ArrayAccess {
2835 name: var_name,
2836 index,
2837 });
2838 }
2839 } else {
2840 parts.push(WordPart::ArrayAccess {
2841 name: var_name,
2842 index,
2843 });
2844 }
2845 } else if let Some(&c) = chars.peek() {
2846 match c {
2848 ':' => {
2849 chars.next(); match chars.peek() {
2851 Some(&'-') | Some(&'=') | Some(&'+') | Some(&'?') => {
2852 let op_char = chars.next().unwrap();
2853 let operand = self.read_brace_operand(&mut chars);
2854 let operator = match op_char {
2855 '-' => ParameterOp::UseDefault,
2856 '=' => ParameterOp::AssignDefault,
2857 '+' => ParameterOp::UseReplacement,
2858 '?' => ParameterOp::Error,
2859 _ => unreachable!(),
2860 };
2861 parts.push(WordPart::ParameterExpansion {
2862 name: var_name,
2863 operator,
2864 operand,
2865 colon_variant: true,
2866 });
2867 }
2868 _ => {
2869 let mut offset = String::new();
2871 while let Some(&ch) = chars.peek() {
2872 if ch == ':' || ch == '}' {
2873 break;
2874 }
2875 offset.push(chars.next().unwrap());
2876 }
2877 let length = if chars.peek() == Some(&':') {
2878 chars.next(); let mut len = String::new();
2880 while let Some(&ch) = chars.peek() {
2881 if ch == '}' {
2882 break;
2883 }
2884 len.push(chars.next().unwrap());
2885 }
2886 Some(len)
2887 } else {
2888 None
2889 };
2890 if chars.peek() == Some(&'}') {
2891 chars.next();
2892 }
2893 parts.push(WordPart::Substring {
2894 name: var_name,
2895 offset,
2896 length,
2897 });
2898 }
2899 }
2900 }
2901 '-' | '=' | '+' | '?' => {
2903 let op_char = chars.next().unwrap();
2904 let operand = self.read_brace_operand(&mut chars);
2905 let operator = match op_char {
2906 '-' => ParameterOp::UseDefault,
2907 '=' => ParameterOp::AssignDefault,
2908 '+' => ParameterOp::UseReplacement,
2909 '?' => ParameterOp::Error,
2910 _ => unreachable!(),
2911 };
2912 parts.push(WordPart::ParameterExpansion {
2913 name: var_name,
2914 operator,
2915 operand,
2916 colon_variant: false,
2917 });
2918 }
2919 '#' => {
2920 chars.next();
2921 if chars.peek() == Some(&'#') {
2922 chars.next();
2923 let op = self.read_brace_operand(&mut chars);
2924 parts.push(WordPart::ParameterExpansion {
2925 name: var_name,
2926 operator: ParameterOp::RemovePrefixLong,
2927 operand: op,
2928 colon_variant: false,
2929 });
2930 } else {
2931 let op = self.read_brace_operand(&mut chars);
2932 parts.push(WordPart::ParameterExpansion {
2933 name: var_name,
2934 operator: ParameterOp::RemovePrefixShort,
2935 operand: op,
2936 colon_variant: false,
2937 });
2938 }
2939 }
2940 '%' => {
2941 chars.next();
2942 if chars.peek() == Some(&'%') {
2943 chars.next();
2944 let op = self.read_brace_operand(&mut chars);
2945 parts.push(WordPart::ParameterExpansion {
2946 name: var_name,
2947 operator: ParameterOp::RemoveSuffixLong,
2948 operand: op,
2949 colon_variant: false,
2950 });
2951 } else {
2952 let op = self.read_brace_operand(&mut chars);
2953 parts.push(WordPart::ParameterExpansion {
2954 name: var_name,
2955 operator: ParameterOp::RemoveSuffixShort,
2956 operand: op,
2957 colon_variant: false,
2958 });
2959 }
2960 }
2961 '/' => {
2962 chars.next();
2963 let replace_all = if chars.peek() == Some(&'/') {
2964 chars.next();
2965 true
2966 } else {
2967 false
2968 };
2969 let mut pattern = String::new();
2970 while let Some(&ch) = chars.peek() {
2971 if ch == '/' || ch == '}' {
2972 break;
2973 }
2974 if ch == '\\' {
2975 chars.next();
2976 if let Some(&next) = chars.peek()
2977 && next == '/'
2978 {
2979 pattern.push(chars.next().unwrap());
2980 continue;
2981 }
2982 pattern.push('\\');
2983 continue;
2984 }
2985 pattern.push(chars.next().unwrap());
2986 }
2987 let replacement = if chars.peek() == Some(&'/') {
2988 chars.next();
2989 let mut repl = String::new();
2990 while let Some(&ch) = chars.peek() {
2991 if ch == '}' {
2992 break;
2993 }
2994 repl.push(chars.next().unwrap());
2995 }
2996 repl
2997 } else {
2998 String::new()
2999 };
3000 if chars.peek() == Some(&'}') {
3001 chars.next();
3002 }
3003 let op = if replace_all {
3004 ParameterOp::ReplaceAll {
3005 pattern,
3006 replacement,
3007 }
3008 } else {
3009 ParameterOp::ReplaceFirst {
3010 pattern,
3011 replacement,
3012 }
3013 };
3014 parts.push(WordPart::ParameterExpansion {
3015 name: var_name,
3016 operator: op,
3017 operand: String::new(),
3018 colon_variant: false,
3019 });
3020 }
3021 '^' => {
3022 chars.next();
3023 let op = if chars.peek() == Some(&'^') {
3024 chars.next();
3025 ParameterOp::UpperAll
3026 } else {
3027 ParameterOp::UpperFirst
3028 };
3029 if chars.peek() == Some(&'}') {
3030 chars.next();
3031 }
3032 parts.push(WordPart::ParameterExpansion {
3033 name: var_name,
3034 operator: op,
3035 operand: String::new(),
3036 colon_variant: false,
3037 });
3038 }
3039 ',' => {
3040 chars.next();
3041 let op = if chars.peek() == Some(&',') {
3042 chars.next();
3043 ParameterOp::LowerAll
3044 } else {
3045 ParameterOp::LowerFirst
3046 };
3047 if chars.peek() == Some(&'}') {
3048 chars.next();
3049 }
3050 parts.push(WordPart::ParameterExpansion {
3051 name: var_name,
3052 operator: op,
3053 operand: String::new(),
3054 colon_variant: false,
3055 });
3056 }
3057 '@' => {
3058 chars.next();
3059 if let Some(&op) = chars.peek() {
3060 chars.next();
3061 if chars.peek() == Some(&'}') {
3062 chars.next();
3063 }
3064 parts.push(WordPart::Transformation {
3065 name: var_name,
3066 operator: op,
3067 });
3068 } else {
3069 if chars.peek() == Some(&'}') {
3070 chars.next();
3071 }
3072 parts.push(WordPart::Variable(var_name));
3073 }
3074 }
3075 '}' => {
3076 chars.next();
3077 if !var_name.is_empty() {
3078 parts.push(WordPart::Variable(var_name));
3079 }
3080 }
3081 _ => {
3082 while let Some(&ch) = chars.peek() {
3083 if ch == '}' {
3084 chars.next();
3085 break;
3086 }
3087 chars.next();
3088 }
3089 if !var_name.is_empty() {
3090 parts.push(WordPart::Variable(var_name));
3091 }
3092 }
3093 }
3094 } else if !var_name.is_empty() {
3095 parts.push(WordPart::Variable(var_name));
3096 }
3097 }
3098 } else if let Some(&c) = chars.peek() {
3099 if matches!(c, '?' | '#' | '@' | '*' | '!' | '$' | '-') || c.is_ascii_digit() {
3101 parts.push(WordPart::Variable(chars.next().unwrap().to_string()));
3102 } else {
3103 let mut var_name = String::new();
3105 while let Some(&c) = chars.peek() {
3106 if c.is_ascii_alphanumeric() || c == '_' {
3107 var_name.push(chars.next().unwrap());
3108 } else {
3109 break;
3110 }
3111 }
3112 if !var_name.is_empty() {
3113 parts.push(WordPart::Variable(var_name));
3114 } else {
3115 current.push('$');
3117 }
3118 }
3119 } else {
3120 current.push('$');
3122 }
3123 } else {
3124 current.push(ch);
3125 }
3126 }
3127
3128 if !current.is_empty() {
3130 parts.push(WordPart::Literal(current));
3131 }
3132
3133 if parts.is_empty() {
3135 parts.push(WordPart::Literal(String::new()));
3136 }
3137
3138 Word {
3139 parts,
3140 quoted: false,
3141 }
3142 }
3143
3144 fn read_brace_operand(&self, chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
3146 let mut operand = String::new();
3147 let mut depth = 1; while let Some(&c) = chars.peek() {
3149 if c == '{' {
3150 depth += 1;
3151 operand.push(chars.next().unwrap());
3152 } else if c == '}' {
3153 depth -= 1;
3154 if depth == 0 {
3155 chars.next(); break;
3157 }
3158 operand.push(chars.next().unwrap());
3159 } else {
3160 operand.push(chars.next().unwrap());
3161 }
3162 }
3163 operand
3164 }
3165}
3166
3167#[cfg(test)]
3168mod tests {
3169 use super::*;
3170
3171 #[test]
3172 fn test_parse_simple_command() {
3173 let parser = Parser::new("echo hello");
3174 let script = parser.parse().unwrap();
3175
3176 assert_eq!(script.commands.len(), 1);
3177
3178 if let Command::Simple(cmd) = &script.commands[0] {
3179 assert_eq!(cmd.name.to_string(), "echo");
3180 assert_eq!(cmd.args.len(), 1);
3181 assert_eq!(cmd.args[0].to_string(), "hello");
3182 } else {
3183 panic!("expected simple command");
3184 }
3185 }
3186
3187 #[test]
3188 fn test_parse_multiple_args() {
3189 let parser = Parser::new("echo hello world");
3190 let script = parser.parse().unwrap();
3191
3192 if let Command::Simple(cmd) = &script.commands[0] {
3193 assert_eq!(cmd.name.to_string(), "echo");
3194 assert_eq!(cmd.args.len(), 2);
3195 assert_eq!(cmd.args[0].to_string(), "hello");
3196 assert_eq!(cmd.args[1].to_string(), "world");
3197 } else {
3198 panic!("expected simple command");
3199 }
3200 }
3201
3202 #[test]
3203 fn test_parse_variable() {
3204 let parser = Parser::new("echo $HOME");
3205 let script = parser.parse().unwrap();
3206
3207 if let Command::Simple(cmd) = &script.commands[0] {
3208 assert_eq!(cmd.args.len(), 1);
3209 assert_eq!(cmd.args[0].parts.len(), 1);
3210 assert!(matches!(&cmd.args[0].parts[0], WordPart::Variable(v) if v == "HOME"));
3211 } else {
3212 panic!("expected simple command");
3213 }
3214 }
3215
3216 #[test]
3217 fn test_parse_pipeline() {
3218 let parser = Parser::new("echo hello | cat");
3219 let script = parser.parse().unwrap();
3220
3221 assert_eq!(script.commands.len(), 1);
3222 assert!(matches!(&script.commands[0], Command::Pipeline(_)));
3223
3224 if let Command::Pipeline(pipeline) = &script.commands[0] {
3225 assert_eq!(pipeline.commands.len(), 2);
3226 }
3227 }
3228
3229 #[test]
3230 fn test_parse_redirect_out() {
3231 let parser = Parser::new("echo hello > /tmp/out");
3232 let script = parser.parse().unwrap();
3233
3234 if let Command::Simple(cmd) = &script.commands[0] {
3235 assert_eq!(cmd.redirects.len(), 1);
3236 assert_eq!(cmd.redirects[0].kind, RedirectKind::Output);
3237 assert_eq!(cmd.redirects[0].target.to_string(), "/tmp/out");
3238 } else {
3239 panic!("expected simple command");
3240 }
3241 }
3242
3243 #[test]
3244 fn test_parse_redirect_append() {
3245 let parser = Parser::new("echo hello >> /tmp/out");
3246 let script = parser.parse().unwrap();
3247
3248 if let Command::Simple(cmd) = &script.commands[0] {
3249 assert_eq!(cmd.redirects.len(), 1);
3250 assert_eq!(cmd.redirects[0].kind, RedirectKind::Append);
3251 } else {
3252 panic!("expected simple command");
3253 }
3254 }
3255
3256 #[test]
3257 fn test_parse_redirect_in() {
3258 let parser = Parser::new("cat < /tmp/in");
3259 let script = parser.parse().unwrap();
3260
3261 if let Command::Simple(cmd) = &script.commands[0] {
3262 assert_eq!(cmd.redirects.len(), 1);
3263 assert_eq!(cmd.redirects[0].kind, RedirectKind::Input);
3264 } else {
3265 panic!("expected simple command");
3266 }
3267 }
3268
3269 #[test]
3270 fn test_parse_command_list_and() {
3271 let parser = Parser::new("true && echo success");
3272 let script = parser.parse().unwrap();
3273
3274 assert!(matches!(&script.commands[0], Command::List(_)));
3275 }
3276
3277 #[test]
3278 fn test_parse_command_list_or() {
3279 let parser = Parser::new("false || echo fallback");
3280 let script = parser.parse().unwrap();
3281
3282 assert!(matches!(&script.commands[0], Command::List(_)));
3283 }
3284
3285 #[test]
3286 fn test_heredoc_pipe() {
3287 let parser = Parser::new("cat <<EOF | sort\nc\na\nb\nEOF\n");
3288 let script = parser.parse().unwrap();
3289 assert!(
3290 matches!(&script.commands[0], Command::Pipeline(_)),
3291 "heredoc with pipe should parse as Pipeline"
3292 );
3293 }
3294
3295 #[test]
3296 fn test_heredoc_multiple_on_line() {
3297 let input = "while cat <<E1 && cat <<E2; do cat <<E3; break; done\n1\nE1\n2\nE2\n3\nE3\n";
3298 let parser = Parser::new(input);
3299 let script = parser.parse().unwrap();
3300 assert_eq!(script.commands.len(), 1);
3301 if let Command::Compound(comp, _) = &script.commands[0] {
3302 if let CompoundCommand::While(w) = comp {
3303 assert!(
3304 !w.condition.is_empty(),
3305 "while condition should be non-empty"
3306 );
3307 assert!(!w.body.is_empty(), "while body should be non-empty");
3308 } else {
3309 panic!("expected While compound command");
3310 }
3311 } else {
3312 panic!("expected Compound command");
3313 }
3314 }
3315
3316 #[test]
3317 fn test_empty_function_body_rejected() {
3318 let parser = Parser::new("f() { }");
3319 assert!(
3320 parser.parse().is_err(),
3321 "empty function body should be rejected"
3322 );
3323 }
3324
3325 #[test]
3326 fn test_empty_while_body_rejected() {
3327 let parser = Parser::new("while true; do\ndone");
3328 assert!(
3329 parser.parse().is_err(),
3330 "empty while body should be rejected"
3331 );
3332 }
3333
3334 #[test]
3335 fn test_empty_for_body_rejected() {
3336 let parser = Parser::new("for i in 1 2 3; do\ndone");
3337 assert!(parser.parse().is_err(), "empty for body should be rejected");
3338 }
3339
3340 #[test]
3341 fn test_empty_if_then_rejected() {
3342 let parser = Parser::new("if true; then\nfi");
3343 assert!(
3344 parser.parse().is_err(),
3345 "empty then clause should be rejected"
3346 );
3347 }
3348
3349 #[test]
3350 fn test_empty_else_rejected() {
3351 let parser = Parser::new("if false; then echo yes; else\nfi");
3352 assert!(
3353 parser.parse().is_err(),
3354 "empty else clause should be rejected"
3355 );
3356 }
3357
3358 #[test]
3359 fn test_unterminated_single_quote_rejected() {
3360 let parser = Parser::new("echo 'unterminated");
3361 assert!(
3362 parser.parse().is_err(),
3363 "unterminated single quote should be rejected"
3364 );
3365 }
3366
3367 #[test]
3368 fn test_unterminated_double_quote_rejected() {
3369 let parser = Parser::new("echo \"unterminated");
3370 assert!(
3371 parser.parse().is_err(),
3372 "unterminated double quote should be rejected"
3373 );
3374 }
3375
3376 #[test]
3377 fn test_nonempty_function_body_accepted() {
3378 let parser = Parser::new("f() { echo hi; }");
3379 assert!(
3380 parser.parse().is_ok(),
3381 "non-empty function body should be accepted"
3382 );
3383 }
3384
3385 #[test]
3386 fn test_nonempty_while_body_accepted() {
3387 let parser = Parser::new("while true; do echo hi; done");
3388 assert!(
3389 parser.parse().is_ok(),
3390 "non-empty while body should be accepted"
3391 );
3392 }
3393
3394 #[test]
3396 fn test_nested_expansion_in_array_subscript() {
3397 let parser = Parser::new("echo ${arr[$RANDOM % ${#arr[@]}]}");
3400 let script = parser.parse().unwrap();
3401 assert_eq!(script.commands.len(), 1);
3402 if let Command::Simple(cmd) = &script.commands[0] {
3403 assert_eq!(cmd.name.to_string(), "echo");
3404 assert_eq!(cmd.args.len(), 1);
3405 let arg = &cmd.args[0];
3407 let has_array_access = arg.parts.iter().any(|p| {
3408 matches!(
3409 p,
3410 WordPart::ArrayAccess { name, index }
3411 if name == "arr" && index.contains("${#arr[@]}")
3412 )
3413 });
3414 assert!(
3415 has_array_access,
3416 "expected ArrayAccess with nested index, got: {:?}",
3417 arg.parts
3418 );
3419 } else {
3420 panic!("expected simple command");
3421 }
3422 }
3423
3424 #[test]
3426 fn test_assignment_nested_subscript_parses() {
3427 let parser = Parser::new("x=${arr[$RANDOM % ${#arr[@]}]}");
3428 assert!(
3429 parser.parse().is_ok(),
3430 "assignment with nested subscript should parse"
3431 );
3432 }
3433}