1use crate::ast::*;
13use compact_str::CompactString;
14use frost_lexer::{Span, Token, TokenKind};
15
16#[derive(Debug, thiserror::Error)]
18pub enum ParseError {
19 #[error("unexpected token {kind:?} at position {pos}, expected {expected}")]
20 Unexpected {
21 kind: TokenKind,
22 pos: usize,
23 expected: String,
24 },
25
26 #[error("unexpected end of input, expected {expected}")]
27 UnexpectedEof { expected: String },
28}
29
30pub struct Parser<'a> {
31 tokens: &'a [Token],
32 pos: usize,
33 syntax_errors: Vec<String>,
35}
36
37#[derive(Clone, Copy, PartialEq, Eq, Debug)]
42enum WordPos {
43 First,
44 Merged,
45}
46
47impl<'a> Parser<'a> {
48 pub fn new(tokens: &'a [Token]) -> Self {
49 Self {
50 tokens,
51 pos: 0,
52 syntax_errors: Vec::new(),
53 }
54 }
55
56 pub fn parse(&mut self) -> Program {
57 let commands = self.parse_program();
58 Program {
59 commands,
60 syntax_errors: std::mem::take(&mut self.syntax_errors),
61 }
62 }
63
64 fn peek(&self) -> &Token {
67 self.tokens
68 .get(self.pos)
69 .unwrap_or(&self.tokens[self.tokens.len() - 1])
70 }
71
72 fn kind(&self) -> TokenKind {
73 self.peek().kind
74 }
75
76 fn advance(&mut self) -> &Token {
77 let tok = &self.tokens[self.pos.min(self.tokens.len() - 1)];
78 if self.pos < self.tokens.len() {
79 self.pos += 1;
80 }
81 tok
82 }
83
84 fn at(&self, kind: TokenKind) -> bool {
85 self.kind() == kind || self.word_matches_keyword(kind)
86 }
87
88 fn eat(&mut self, kind: TokenKind) -> bool {
89 if self.at(kind) {
90 self.advance();
91 true
92 } else {
93 false
94 }
95 }
96
97 fn word_matches_keyword(&self, kind: TokenKind) -> bool {
100 if self.kind() != TokenKind::Word {
101 return false;
102 }
103 let text = self.peek().text.as_str();
104 matches!(
105 (text, kind),
106 ("if", TokenKind::If)
107 | ("then", TokenKind::Then)
108 | ("elif", TokenKind::Elif)
109 | ("else", TokenKind::Else)
110 | ("fi", TokenKind::Fi)
111 | ("for", TokenKind::For)
112 | ("in", TokenKind::In)
113 | ("while", TokenKind::While)
114 | ("until", TokenKind::Until)
115 | ("do", TokenKind::Do)
116 | ("done", TokenKind::Done)
117 | ("case", TokenKind::Case)
118 | ("esac", TokenKind::Esac)
119 | ("select", TokenKind::Select)
120 | ("function", TokenKind::Function)
121 | ("time", TokenKind::Time)
122 | ("coproc", TokenKind::Coproc)
123 )
124 }
125
126 fn expect(&mut self, kind: TokenKind) {
133 if !self.eat(kind) {
134 let found = self.kind();
135 self.syntax_errors
136 .push(format!("expected {kind:?}, found {found:?}"));
137 self.advance();
138 }
139 }
140
141 fn skip_newlines(&mut self) {
142 while self.at(TokenKind::Newline) {
143 self.advance();
144 }
145 }
146
147 fn at_eof(&self) -> bool {
148 self.at(TokenKind::Eof)
149 }
150
151 fn span(&self) -> Span {
152 self.peek().span
153 }
154
155 fn at_command_start(&self) -> bool {
157 if self.kind() == TokenKind::Word {
159 let text = self.peek().text.as_str();
160 if matches!(
161 text,
162 "if" | "for"
163 | "while"
164 | "until"
165 | "case"
166 | "select"
167 | "function"
168 | "time"
169 | "coproc"
170 | "[["
171 | "repeat"
172 ) {
173 return true;
174 }
175 }
176 matches!(
177 self.kind(),
178 TokenKind::Word
179 | TokenKind::SingleQuoted
180 | TokenKind::DoubleQuoted
181 | TokenKind::DollarSingleQuoted
182 | TokenKind::Dollar
183 | TokenKind::DollarParam
184 | TokenKind::DollarBrace
185 | TokenKind::DollarParen
186 | TokenKind::DollarDoubleParen
187 | TokenKind::Backtick
188 | TokenKind::Tilde
189 | TokenKind::Star
190 | TokenKind::Question
191 | TokenKind::At
192 | TokenKind::Bang
193 | TokenKind::Less
194 | TokenKind::Greater
195 | TokenKind::DoubleGreater
196 | TokenKind::AmpGreater
197 | TokenKind::AmpDoubleGreater
198 | TokenKind::GreaterPipe
199 | TokenKind::GreaterBang
200 | TokenKind::DoubleLess
201 | TokenKind::TripleLess
202 | TokenKind::LessGreater
203 | TokenKind::FdGreater
204 | TokenKind::FdLess
205 | TokenKind::FdDoubleGreater
206 | TokenKind::FdDup
207 | TokenKind::Number
208 | TokenKind::LeftParen
209 | TokenKind::LeftBrace
210 | TokenKind::If
211 | TokenKind::For
212 | TokenKind::While
213 | TokenKind::Until
214 | TokenKind::Case
215 | TokenKind::Select
216 | TokenKind::Function
217 | TokenKind::Time
218 | TokenKind::Coproc
219 )
220 }
221
222 fn at_word(&self) -> bool {
224 matches!(
225 self.kind(),
226 TokenKind::Word
227 | TokenKind::SingleQuoted
228 | TokenKind::DoubleQuoted
229 | TokenKind::DollarSingleQuoted
230 | TokenKind::Dollar
231 | TokenKind::DollarParam
232 | TokenKind::DollarBrace
233 | TokenKind::DollarParen
234 | TokenKind::DollarDoubleParen
235 | TokenKind::Backtick
236 | TokenKind::Tilde
237 | TokenKind::Star
238 | TokenKind::Question
239 | TokenKind::At
240 | TokenKind::Number
241 | TokenKind::Equals
242 | TokenKind::ProcessSubIn
244 | TokenKind::ProcessSubOut
245 )
246 }
247
248 #[allow(dead_code)] fn looks_like_brace_expansion(&self) -> bool {
254 let mut i = self.pos + 1; let mut saw_comma_or_dots = false;
258 let mut depth = 1u32;
259 while i < self.tokens.len() && i < self.pos + 20 {
260 let tok = &self.tokens[i];
261 match tok.kind {
262 TokenKind::LeftBrace => depth += 1,
263 TokenKind::RightBrace => {
264 depth -= 1;
265 if depth == 0 {
266 return saw_comma_or_dots;
268 }
269 }
270 TokenKind::Newline
271 | TokenKind::Semi
272 | TokenKind::Pipe
273 | TokenKind::AndAnd
274 | TokenKind::OrOr
275 | TokenKind::Eof => {
276 return false; }
278 TokenKind::Word => {
279 let text = tok.text.as_str();
280 if text.contains(',') || text.contains("..") {
281 saw_comma_or_dots = true;
282 }
283 }
284 _ => {}
285 }
286 i += 1;
287 }
288 false
289 }
290
291 fn at_brace_in_word(&self) -> bool {
294 matches!(self.kind(), TokenKind::LeftBrace | TokenKind::RightBrace)
295 }
296
297 fn at_reserved_word_form(&self) -> bool {
300 self.at(TokenKind::Then)
301 || self.at(TokenKind::Elif)
302 || self.at(TokenKind::Else)
303 || self.at(TokenKind::Fi)
304 || self.at(TokenKind::Do)
305 || self.at(TokenKind::Done)
306 || self.at(TokenKind::Esac)
307 }
308
309 fn at_redirect(&self) -> bool {
310 matches!(
311 self.kind(),
312 TokenKind::Less
313 | TokenKind::Greater
314 | TokenKind::DoubleGreater
315 | TokenKind::GreaterPipe
316 | TokenKind::GreaterBang
317 | TokenKind::AmpGreater
318 | TokenKind::AmpDoubleGreater
319 | TokenKind::DoubleLess
320 | TokenKind::TripleLess
321 | TokenKind::DoubleLessDash
322 | TokenKind::LessGreater
323 | TokenKind::FdGreater
324 | TokenKind::FdLess
325 | TokenKind::FdDoubleGreater
326 | TokenKind::FdDup
327 )
328 }
329
330 fn parse_program(&mut self) -> Vec<CompleteCommand> {
333 let mut commands = Vec::new();
334 self.skip_newlines();
335
336 while !self.at_eof() {
337 let pos_before = self.pos;
338
339 if self.at_command_start() {
340 commands.push(self.parse_complete_command());
341 }
342 if !self.eat(TokenKind::Semi) && !self.eat(TokenKind::Newline) {
344 if !self.at_eof() {
345 self.skip_newlines();
346 }
347 }
348 self.skip_newlines();
349
350 if self.pos == pos_before {
363 self.advance();
364 if self.pos == pos_before {
365 break; }
367 }
368 }
369 commands
370 }
371
372 fn parse_complete_command(&mut self) -> CompleteCommand {
375 let list = self.parse_list();
376 let is_async = self.eat(TokenKind::Ampersand);
377 CompleteCommand { list, is_async }
378 }
379
380 fn parse_list(&mut self) -> List {
383 let first = self.parse_pipeline();
384 let mut rest = Vec::new();
385
386 loop {
387 let op = if self.eat(TokenKind::AndAnd) {
388 Some(ListOp::And)
389 } else if self.eat(TokenKind::OrOr) {
390 Some(ListOp::Or)
391 } else {
392 None
393 };
394
395 match op {
396 Some(op) => {
397 self.skip_newlines();
398 rest.push((op, self.parse_pipeline()));
399 }
400 None => break,
401 }
402 }
403
404 List { first, rest }
405 }
406
407 fn parse_pipeline(&mut self) -> Pipeline {
410 let bang = self.eat(TokenKind::Bang);
411 let first = self.parse_command();
412 let mut commands = vec![first];
413 let mut pipe_stderr = Vec::new();
414
415 loop {
416 if self.eat(TokenKind::Pipe) {
417 pipe_stderr.push(false);
418 self.skip_newlines();
419 commands.push(self.parse_command());
420 } else if self.eat(TokenKind::PipeAmpersand) {
421 pipe_stderr.push(true);
422 self.skip_newlines();
423 commands.push(self.parse_command());
424 } else {
425 break;
426 }
427 }
428
429 Pipeline {
430 bang,
431 commands,
432 pipe_stderr,
433 }
434 }
435
436 fn parse_command(&mut self) -> Command {
439 if self.kind() == TokenKind::Word && self.peek().text.as_str() == "[[" {
441 return self.parse_cond_command();
442 }
443 if self.at(TokenKind::LeftParen) && self.is_arith_cmd_ahead() {
445 return self.parse_arith_cmd();
446 }
447 if self.at(TokenKind::For) && self.is_c_for_ahead() {
449 return self.parse_c_for();
450 }
451 if self.kind() == TokenKind::Word && self.peek().text.as_str() == "repeat" {
453 return self.parse_repeat();
454 }
455 if self.at(TokenKind::LeftParen) {
457 return self.parse_subshell();
458 }
459 if self.at(TokenKind::LeftBrace) {
460 return self.parse_brace_group();
461 }
462 if self.at(TokenKind::If) {
463 return self.parse_if();
464 }
465 if self.at(TokenKind::For) {
466 return self.parse_for();
467 }
468 if self.at(TokenKind::While) {
469 return self.parse_while();
470 }
471 if self.at(TokenKind::Until) {
472 return self.parse_until();
473 }
474 if self.at(TokenKind::Case) {
475 return self.parse_case();
476 }
477 if self.at(TokenKind::Select) {
478 return self.parse_select();
479 }
480 if self.at(TokenKind::Function) {
481 return self.parse_function_def();
482 }
483 if self.at(TokenKind::Time) {
484 return self.parse_time();
485 }
486 if self.at(TokenKind::Coproc) {
487 return self.parse_coproc();
488 }
489
490 match self.kind() {
491 _ => {
492 if self.is_function_def_ahead() {
494 return self.parse_function_def_short();
495 }
496 Command::Simple(self.parse_simple_command())
497 }
498 }
499 }
500
501 fn is_function_def_ahead(&self) -> bool {
502 if self.kind() == TokenKind::Word {
504 if let Some(next) = self.tokens.get(self.pos + 1) {
505 if next.kind == TokenKind::LeftParen {
506 if let Some(after) = self.tokens.get(self.pos + 2) {
507 return after.kind == TokenKind::RightParen;
508 }
509 }
510 }
511 }
512 false
513 }
514
515 fn parse_simple_command(&mut self) -> SimpleCommand {
518 let mut assignments = Vec::new();
519 let mut words = Vec::new();
520 let mut redirects = Vec::new();
521
522 while self.is_assignment() && words.is_empty() {
524 assignments.push(self.parse_assignment());
525 }
526
527 while !self.at_eof()
529 && !self.kind().is_separator()
530 && !matches!(
531 self.kind(),
532 TokenKind::RightParen
533 | TokenKind::RightBrace
534 | TokenKind::DoubleSemi
535 | TokenKind::SemiAnd
536 | TokenKind::SemiPipe
537 | TokenKind::Then
538 | TokenKind::Elif
539 | TokenKind::Else
540 | TokenKind::Fi
541 | TokenKind::Do
542 | TokenKind::Done
543 | TokenKind::Esac
544 )
545 && !(words.is_empty() && self.at_reserved_word_form())
561 {
562 if self.at_redirect() {
563 redirects.push(self.parse_redirect());
564 } else if self.at_word() {
565 words.push(self.parse_word());
566 } else if self.kind() == TokenKind::Bang
567 && !(words.is_empty() && assignments.is_empty())
568 {
569 words.push(self.parse_word());
577 } else {
578 break;
579 }
580 }
581
582 SimpleCommand {
583 assignments,
584 words,
585 redirects,
586 }
587 }
588
589 fn is_assignment(&self) -> bool {
590 if self.kind() != TokenKind::Word {
594 return false;
595 }
596 let name = &self.peek().text;
597 let check_name = name.strip_suffix('+').unwrap_or(name);
599 let ident_part = if let Some(bracket) = check_name.find('[') {
601 &check_name[..bracket]
602 } else {
603 check_name
604 };
605 let is_ident = !ident_part.is_empty()
606 && ident_part
607 .bytes()
608 .all(|b| b.is_ascii_alphanumeric() || b == b'_')
609 && !ident_part.bytes().next().unwrap_or(b'0').is_ascii_digit();
610 if !is_ident {
611 return false;
612 }
613 self.tokens
615 .get(self.pos + 1)
616 .is_some_and(|t| t.kind == TokenKind::Equals)
617 }
618
619 fn parse_assignment(&mut self) -> Assignment {
620 let name_tok = self.advance().clone(); let eq_span = self.peek().span;
622 self.expect(TokenKind::Equals); let raw = name_tok.text.as_str();
626 let (base, is_append) = if let Some(stripped) = raw.strip_suffix('+') {
627 (stripped, true)
628 } else {
629 (raw, false)
630 };
631
632 let (name, subscript) = if let Some(bracket) = base.find('[') {
633 let close = base.rfind(']').unwrap_or(base.len());
634 let name_part = &base[..bracket];
635 let sub_part = &base[bracket + 1..close];
636 (
637 CompactString::from(name_part),
638 Some(CompactString::from(sub_part)),
639 )
640 } else {
641 (CompactString::from(base), None)
642 };
643
644 let op = if is_append {
645 AssignOp::Append
646 } else {
647 AssignOp::Assign
648 };
649
650 if self.at(TokenKind::LeftParen) {
652 self.advance(); let mut words = Vec::new();
654 while !self.at(TokenKind::RightParen) && !self.at(TokenKind::Eof) {
655 self.skip_newlines();
656 if self.at(TokenKind::RightParen) {
657 break;
658 }
659 if self.at_word() {
660 words.push(self.parse_word());
661 } else {
662 break;
663 }
664 }
665 if self.at(TokenKind::RightParen) {
666 self.advance(); }
668 return Assignment {
669 name,
670 subscript: subscript.clone(),
671 op,
672 value: None,
673 array_value: Some(words),
674 span: Span::new(name_tok.span.start, eq_span.end),
675 };
676 }
677
678 let value = if self.at_word() {
680 Some(self.parse_word())
681 } else {
682 None
683 };
684
685 Assignment {
686 name,
687 subscript,
688 op,
689 value,
690 array_value: None,
691 span: Span::new(name_tok.span.start, eq_span.end),
692 }
693 }
694
695 pub fn parse_word_for_expansion(&mut self) -> Word {
704 self.parse_word()
705 }
706
707 fn parse_word(&mut self) -> Word {
727 let start_span = self.span();
728 let mut parts = Vec::new();
729
730 self.parse_word_piece(&mut parts, WordPos::First);
731 let mut end_pos = self.prev_end();
732
733 while self.pos < self.tokens.len()
737 && self.peek().span.start == end_pos
738 && (self.at_word() || self.at_brace_in_word() || self.kind() == TokenKind::Bang)
739 {
740 let pos_before = self.pos;
741 self.parse_word_piece(&mut parts, WordPos::Merged);
742 debug_assert!(self.pos > pos_before, "parse_word_piece must consume");
743 if self.pos == pos_before {
744 break; }
746 end_pos = self.prev_end();
747 }
748
749 Word {
750 parts,
751 span: start_span,
752 }
753 }
754
755 fn prev_end(&self) -> u32 {
760 self.tokens
761 .get(self.pos.saturating_sub(1))
762 .map_or(0, |t| t.span.end)
763 }
764
765 fn parse_word_piece(&mut self, parts: &mut Vec<WordPart>, pos: WordPos) {
770 let tok = self.advance().clone();
771 match tok.kind {
772 TokenKind::Word | TokenKind::Number => {
773 parts.push(WordPart::Literal(tok.text.clone()));
774 }
775 TokenKind::SingleQuoted => {
776 let inner = strip_quotes(&tok.text, '\'');
778 parts.push(WordPart::SingleQuoted(inner));
779 }
780 TokenKind::DoubleQuoted => {
781 let inner = strip_quotes(&tok.text, '"');
787 parts.push(WordPart::DoubleQuoted(parse_double_quoted_parts(&inner)));
788 }
789 TokenKind::DollarSingleQuoted => {
790 parts.push(WordPart::AnsiCQuoted(strip_ansi_c_quotes(&tok.text)));
798 }
799 TokenKind::DollarParam => {
800 let name = tok.text.strip_prefix('$').unwrap_or(&tok.text);
803 parts.push(WordPart::DollarVar(CompactString::from(name)));
804 }
805 TokenKind::Dollar => {
806 let adjacent =
809 self.pos < self.tokens.len() && self.peek().span.start == tok.span.end;
810 if adjacent && (self.kind() == TokenKind::Word || self.kind() == TokenKind::Number)
811 {
812 let name_tok = self.advance().clone();
822 let text = name_tok.text.as_str();
823 let split = param_name_len(text);
824 if split == 0 {
825 parts.push(WordPart::Literal(CompactString::from("$")));
826 parts.push(WordPart::Literal(name_tok.text.clone()));
827 } else {
828 parts.push(WordPart::DollarVar(CompactString::from(&text[..split])));
829 if split < text.len() {
830 parts.push(WordPart::Literal(CompactString::from(&text[split..])));
831 }
832 }
833 } else if adjacent
834 && matches!(
835 self.kind(),
836 TokenKind::Question | TokenKind::Bang | TokenKind::At | TokenKind::Star
837 )
838 {
839 let special_tok = self.advance();
842 let name = match special_tok.kind {
843 TokenKind::Question => "?",
844 TokenKind::Bang => "!",
845 TokenKind::At => "@",
846 TokenKind::Star => "*",
847 _ => unreachable!(),
848 };
849 parts.push(WordPart::DollarVar(CompactString::from(name)));
850 } else if adjacent && self.at(TokenKind::Dollar) {
851 self.advance();
853 parts.push(WordPart::DollarVar(CompactString::from("$")));
854 } else {
855 parts.push(WordPart::Literal(CompactString::from("$")));
856 }
857 }
858 TokenKind::DollarBrace => {
859 let mut raw = String::new();
861 let mut depth = 1u32;
862 while !self.at_eof() {
863 if self.at(TokenKind::RightBrace) {
864 depth -= 1;
865 if depth == 0 {
866 self.advance(); break;
868 }
869 raw.push('}');
870 self.advance();
871 } else if self.at(TokenKind::LeftBrace) || self.at(TokenKind::DollarBrace) {
872 depth += 1;
873 raw.push_str(&self.advance().text);
874 } else {
875 raw.push_str(&self.advance().text);
876 }
877 }
878 parts.push(WordPart::DollarBrace {
879 param: CompactString::from(raw.trim()),
880 operator: None,
881 arg: None,
882 });
883 }
884 TokenKind::DollarParen => {
885 let mut inner_tokens = Vec::new();
887 let mut depth = 1u32;
888 while !self.at_eof() && depth > 0 {
889 if self.at(TokenKind::LeftParen) || self.at(TokenKind::DollarParen) {
890 depth += 1;
891 inner_tokens.push(self.advance().clone());
892 } else if self.at(TokenKind::RightParen) {
893 depth -= 1;
894 if depth == 0 {
895 self.advance(); break;
897 }
898 inner_tokens.push(self.advance().clone());
899 } else {
900 inner_tokens.push(self.advance().clone());
901 }
902 }
903 inner_tokens.push(Token {
905 kind: TokenKind::Eof,
906 span: self.span(),
907 text: CompactString::default(),
908 });
909 let mut sub_parser = Parser::new(&inner_tokens);
911 let sub_program = sub_parser.parse();
912 parts.push(WordPart::CommandSub(Box::new(sub_program)));
913 }
914 TokenKind::DollarDoubleParen => {
915 let mut expr = String::new();
917 while !self.at_eof() {
918 if self.at(TokenKind::RightParen) {
920 self.advance();
921 if self.eat(TokenKind::RightParen) {
922 break;
923 }
924 expr.push(')');
925 } else {
926 expr.push_str(&self.advance().text);
927 }
928 }
929 parts.push(WordPart::ArithSub(CompactString::from(expr)));
930 }
931 TokenKind::Backtick => {
932 parts.push(WordPart::CommandSub(Box::new(Program::default())));
933 }
934 TokenKind::Star => parts.push(WordPart::Glob(GlobKind::Star)),
939 TokenKind::Question => parts.push(WordPart::Glob(GlobKind::Question)),
940 TokenKind::At => parts.push(WordPart::Glob(GlobKind::At)),
941 TokenKind::ProcessSubIn | TokenKind::ProcessSubOut => {
946 let kind = if tok.kind == TokenKind::ProcessSubIn {
947 ProcessSubKind::Input
948 } else {
949 ProcessSubKind::Output
950 };
951 let mut inner_tokens = Vec::new();
952 let mut depth = 1u32;
953 while !self.at_eof() && depth > 0 {
954 if self.at(TokenKind::LeftParen)
955 || self.at(TokenKind::DollarParen)
956 || self.at(TokenKind::ProcessSubIn)
957 || self.at(TokenKind::ProcessSubOut)
958 {
959 depth += 1;
960 inner_tokens.push(self.advance().clone());
961 } else if self.at(TokenKind::RightParen) {
962 depth -= 1;
963 if depth == 0 {
964 self.advance();
965 break;
966 }
967 inner_tokens.push(self.advance().clone());
968 } else {
969 inner_tokens.push(self.advance().clone());
970 }
971 }
972 inner_tokens.push(Token {
973 kind: TokenKind::Eof,
974 span: self.span(),
975 text: CompactString::default(),
976 });
977 let mut sub_parser = Parser::new(&inner_tokens);
978 let body = sub_parser.parse();
979 parts.push(WordPart::ProcessSub {
980 kind,
981 body: Box::new(body),
982 });
983 }
984 TokenKind::Tilde if pos == WordPos::First => {
991 let adjacent =
992 self.pos < self.tokens.len() && self.peek().span.start == tok.span.end;
993 let user = if adjacent && self.kind() == TokenKind::Word {
994 self.advance().text.clone()
995 } else {
996 CompactString::default()
997 };
998 parts.push(WordPart::Tilde(user));
999 }
1000 TokenKind::Equals => {
1001 parts.push(WordPart::Literal(CompactString::from("=")));
1002 }
1003 _ => {
1004 parts.push(WordPart::Literal(tok.text.clone()));
1006 }
1007 }
1008 }
1009 fn parse_redirect(&mut self) -> Redirect {
1012 let redir_tok = self.advance().clone();
1013 let (fd, op) = match redir_tok.kind {
1014 TokenKind::Less => (None, RedirectOp::Less),
1015 TokenKind::Greater => (None, RedirectOp::Greater),
1016 TokenKind::DoubleGreater => (None, RedirectOp::DoubleGreater),
1017 TokenKind::GreaterPipe => (None, RedirectOp::GreaterPipe),
1018 TokenKind::GreaterBang => (None, RedirectOp::GreaterBang),
1019 TokenKind::AmpGreater => (None, RedirectOp::AmpGreater),
1020 TokenKind::AmpDoubleGreater => (None, RedirectOp::AmpDoubleGreater),
1021 TokenKind::DoubleLess => (None, RedirectOp::DoubleLess),
1022 TokenKind::TripleLess => (None, RedirectOp::TripleLess),
1023 TokenKind::DoubleLessDash => (None, RedirectOp::DoubleLessDash),
1024 TokenKind::LessGreater => (None, RedirectOp::LessGreater),
1025 TokenKind::FdGreater => {
1026 let fd_num = parse_fd_prefix(&redir_tok.text);
1027 (Some(fd_num), RedirectOp::Greater)
1028 }
1029 TokenKind::FdLess => {
1030 let fd_num = parse_fd_prefix(&redir_tok.text);
1031 (Some(fd_num), RedirectOp::Less)
1032 }
1033 TokenKind::FdDoubleGreater => {
1034 let fd_num = parse_fd_prefix(&redir_tok.text);
1035 (Some(fd_num), RedirectOp::DoubleGreater)
1036 }
1037 TokenKind::FdDup => {
1038 let fd_num = parse_fd_prefix(&redir_tok.text);
1039 (Some(fd_num), RedirectOp::FdDup)
1040 }
1041 _ => (None, RedirectOp::Greater),
1042 };
1043
1044 let target = if self.at_word() {
1046 self.parse_word()
1047 } else {
1048 Word {
1050 parts: vec![],
1051 span: self.span(),
1052 }
1053 };
1054
1055 Redirect {
1056 fd,
1057 op,
1058 target,
1059 span: redir_tok.span,
1060 }
1061 }
1062
1063 fn is_arith_cmd_ahead(&self) -> bool {
1067 self.at(TokenKind::LeftParen)
1068 && self
1069 .tokens
1070 .get(self.pos + 1)
1071 .is_some_and(|t| t.kind == TokenKind::LeftParen)
1072 }
1073
1074 fn parse_arith_cmd(&mut self) -> Command {
1076 self.expect(TokenKind::LeftParen);
1077 self.expect(TokenKind::LeftParen);
1078 let mut expr = String::new();
1080 let mut depth = 0;
1081 loop {
1082 if self.at(TokenKind::Eof) {
1083 break;
1084 }
1085 if self.at(TokenKind::RightParen) {
1086 if depth == 0 {
1087 if self
1089 .tokens
1090 .get(self.pos + 1)
1091 .is_some_and(|t| t.kind == TokenKind::RightParen)
1092 {
1093 self.advance(); self.advance(); break;
1096 }
1097 }
1098 depth -= 1;
1099 expr.push(')');
1100 self.advance();
1101 continue;
1102 }
1103 if self.at(TokenKind::LeftParen) {
1104 depth += 1;
1105 expr.push('(');
1106 self.advance();
1107 continue;
1108 }
1109 let tok = self.advance().clone();
1111 expr.push_str(&tok.text);
1112 if !self.at(TokenKind::RightParen) && !self.at(TokenKind::Eof) {
1114 expr.push(' ');
1115 }
1116 }
1117 Command::ArithCmd(CompactString::new(&expr.trim()))
1118 }
1119
1120 fn parse_subshell(&mut self) -> Command {
1121 self.expect(TokenKind::LeftParen);
1122 self.skip_newlines();
1123 let body = self.parse_compound_body(&[TokenKind::RightParen]);
1124 self.expect(TokenKind::RightParen);
1125 let redirects = self.parse_trailing_redirects();
1126 Command::Subshell(Subshell { body, redirects })
1127 }
1128
1129 fn parse_brace_group(&mut self) -> Command {
1130 self.expect(TokenKind::LeftBrace);
1131 self.skip_newlines();
1132 let body = self.parse_compound_body(&[TokenKind::RightBrace]);
1133 self.expect(TokenKind::RightBrace);
1134
1135 if self.kind() == TokenKind::Word && self.peek().text.as_str() == "always" {
1137 self.advance(); self.expect(TokenKind::LeftBrace);
1139 self.skip_newlines();
1140 let always_body = self.parse_compound_body(&[TokenKind::RightBrace]);
1141 self.expect(TokenKind::RightBrace);
1142 return Command::TryAlways(Box::new(TryAlwaysClause {
1143 try_body: body,
1144 always_body,
1145 }));
1146 }
1147
1148 let redirects = self.parse_trailing_redirects();
1149 Command::BraceGroup(BraceGroup { body, redirects })
1150 }
1151
1152 fn parse_if(&mut self) -> Command {
1153 self.expect(TokenKind::If);
1154 self.skip_newlines();
1155 let condition = self.parse_compound_body(&[TokenKind::Then]);
1156 self.expect(TokenKind::Then);
1157 self.skip_newlines();
1158 let then_body =
1159 self.parse_compound_body(&[TokenKind::Elif, TokenKind::Else, TokenKind::Fi]);
1160
1161 let mut elifs = Vec::new();
1162 while self.eat(TokenKind::Elif) {
1163 self.skip_newlines();
1164 let elif_cond = self.parse_compound_body(&[TokenKind::Then]);
1165 self.expect(TokenKind::Then);
1166 self.skip_newlines();
1167 let elif_body =
1168 self.parse_compound_body(&[TokenKind::Elif, TokenKind::Else, TokenKind::Fi]);
1169 elifs.push((elif_cond, elif_body));
1170 }
1171
1172 let else_body = if self.eat(TokenKind::Else) {
1173 self.skip_newlines();
1174 Some(self.parse_compound_body(&[TokenKind::Fi]))
1175 } else {
1176 None
1177 };
1178
1179 self.expect(TokenKind::Fi);
1180 let redirects = self.parse_trailing_redirects();
1181 Command::If(Box::new(IfClause {
1182 condition,
1183 then_body,
1184 elifs,
1185 else_body,
1186 redirects,
1187 }))
1188 }
1189
1190 fn parse_for(&mut self) -> Command {
1191 self.expect(TokenKind::For);
1192 let var = self.advance().text.clone();
1193
1194 let words = if self.eat(TokenKind::In) {
1195 let mut ws = Vec::new();
1196 while self.at_word() {
1197 ws.push(self.parse_word());
1198 }
1199 let _ = self.eat(TokenKind::Semi) || self.eat(TokenKind::Newline);
1201 Some(ws)
1202 } else {
1203 let _ = self.eat(TokenKind::Semi) || self.eat(TokenKind::Newline);
1204 None
1205 };
1206
1207 self.skip_newlines();
1208 let (body, redirects) = if self.at(TokenKind::LeftBrace) {
1210 self.expect(TokenKind::LeftBrace);
1211 self.skip_newlines();
1212 let body = self.parse_compound_body(&[TokenKind::RightBrace]);
1213 self.expect(TokenKind::RightBrace);
1214 (body, self.parse_trailing_redirects())
1215 } else {
1216 self.expect(TokenKind::Do);
1217 self.skip_newlines();
1218 let body = self.parse_compound_body(&[TokenKind::Done]);
1219 self.expect(TokenKind::Done);
1220 (body, self.parse_trailing_redirects())
1221 };
1222 Command::For(Box::new(ForClause {
1223 var,
1224 words,
1225 body,
1226 redirects,
1227 }))
1228 }
1229
1230 fn parse_while(&mut self) -> Command {
1231 self.expect(TokenKind::While);
1232 self.skip_newlines();
1233 let condition = self.parse_compound_body(&[TokenKind::Do, TokenKind::LeftBrace]);
1234 let (body, redirects) = if self.at(TokenKind::LeftBrace) {
1235 self.expect(TokenKind::LeftBrace);
1236 self.skip_newlines();
1237 let body = self.parse_compound_body(&[TokenKind::RightBrace]);
1238 self.expect(TokenKind::RightBrace);
1239 (body, self.parse_trailing_redirects())
1240 } else {
1241 self.expect(TokenKind::Do);
1242 self.skip_newlines();
1243 let body = self.parse_compound_body(&[TokenKind::Done]);
1244 self.expect(TokenKind::Done);
1245 (body, self.parse_trailing_redirects())
1246 };
1247 Command::While(Box::new(WhileClause {
1248 condition,
1249 body,
1250 redirects,
1251 }))
1252 }
1253
1254 fn parse_until(&mut self) -> Command {
1255 self.expect(TokenKind::Until);
1256 self.skip_newlines();
1257 let condition = self.parse_compound_body(&[TokenKind::Do, TokenKind::LeftBrace]);
1258 let (body, redirects) = if self.at(TokenKind::LeftBrace) {
1259 self.expect(TokenKind::LeftBrace);
1260 self.skip_newlines();
1261 let body = self.parse_compound_body(&[TokenKind::RightBrace]);
1262 self.expect(TokenKind::RightBrace);
1263 (body, self.parse_trailing_redirects())
1264 } else {
1265 self.expect(TokenKind::Do);
1266 self.skip_newlines();
1267 let body = self.parse_compound_body(&[TokenKind::Done]);
1268 self.expect(TokenKind::Done);
1269 (body, self.parse_trailing_redirects())
1270 };
1271 Command::Until(Box::new(UntilClause {
1272 condition,
1273 body,
1274 redirects,
1275 }))
1276 }
1277
1278 fn parse_case(&mut self) -> Command {
1279 self.expect(TokenKind::Case);
1280 let word = self.parse_word();
1281 self.skip_newlines();
1282 self.expect(TokenKind::In);
1283 self.skip_newlines();
1284
1285 let mut items = Vec::new();
1286 while !self.at(TokenKind::Esac) && !self.at_eof() {
1287 self.eat(TokenKind::LeftParen);
1289
1290 let mut patterns = Vec::new();
1292 if self.at_word() {
1293 patterns.push(self.parse_word());
1294 while self.eat(TokenKind::Pipe) {
1295 if self.at_word() {
1296 patterns.push(self.parse_word());
1297 }
1298 }
1299 }
1300 self.expect(TokenKind::RightParen);
1301 self.skip_newlines();
1302
1303 let body = self.parse_compound_body(&[
1305 TokenKind::DoubleSemi,
1306 TokenKind::SemiAnd,
1307 TokenKind::SemiPipe,
1308 TokenKind::Esac,
1309 ]);
1310
1311 let terminator = if self.eat(TokenKind::SemiAnd) {
1312 CaseTerminator::SemiAnd
1313 } else if self.eat(TokenKind::SemiPipe) {
1314 CaseTerminator::SemiPipe
1315 } else {
1316 self.eat(TokenKind::DoubleSemi);
1317 CaseTerminator::DoubleSemi
1318 };
1319 self.skip_newlines();
1320
1321 if !patterns.is_empty() {
1322 items.push(CaseItem {
1323 patterns,
1324 body,
1325 terminator,
1326 });
1327 }
1328 }
1329
1330 self.expect(TokenKind::Esac);
1331 let redirects = self.parse_trailing_redirects();
1332 Command::Case(Box::new(CaseClause {
1333 word,
1334 items,
1335 redirects,
1336 }))
1337 }
1338
1339 fn parse_select(&mut self) -> Command {
1340 self.expect(TokenKind::Select);
1341 let var = self.advance().text.clone();
1342
1343 let words = if self.eat(TokenKind::In) {
1344 let mut ws = Vec::new();
1345 while self.at_word() {
1346 ws.push(self.parse_word());
1347 }
1348 let _ = self.eat(TokenKind::Semi) || self.eat(TokenKind::Newline);
1349 Some(ws)
1350 } else {
1351 let _ = self.eat(TokenKind::Semi) || self.eat(TokenKind::Newline);
1352 None
1353 };
1354
1355 self.skip_newlines();
1356 self.expect(TokenKind::Do);
1357 self.skip_newlines();
1358 let body = self.parse_compound_body(&[TokenKind::Done]);
1359 self.expect(TokenKind::Done);
1360 let redirects = self.parse_trailing_redirects();
1361 Command::Select(Box::new(SelectClause {
1362 var,
1363 words,
1364 body,
1365 redirects,
1366 }))
1367 }
1368
1369 fn parse_function_def(&mut self) -> Command {
1370 self.expect(TokenKind::Function);
1371 let name = self.advance().text.clone();
1372 if self.eat(TokenKind::LeftParen) {
1374 self.eat(TokenKind::RightParen);
1375 }
1376 self.skip_newlines();
1377 let body = self.parse_command();
1378 let redirects = self.parse_trailing_redirects();
1379 Command::FunctionDef(Box::new(FunctionDef {
1380 name,
1381 body,
1382 redirects,
1383 }))
1384 }
1385
1386 fn parse_function_def_short(&mut self) -> Command {
1387 let name = self.advance().text.clone();
1389 self.expect(TokenKind::LeftParen);
1390 self.expect(TokenKind::RightParen);
1391 self.skip_newlines();
1392 let body = self.parse_command();
1393 let redirects = self.parse_trailing_redirects();
1394 Command::FunctionDef(Box::new(FunctionDef {
1395 name,
1396 body,
1397 redirects,
1398 }))
1399 }
1400
1401 fn parse_time(&mut self) -> Command {
1402 self.expect(TokenKind::Time);
1403 let pipeline = self.parse_pipeline();
1404 Command::Time(Box::new(TimeClause { pipeline }))
1405 }
1406
1407 fn parse_coproc(&mut self) -> Command {
1408 self.expect(TokenKind::Coproc);
1409 let name = if self.kind() == TokenKind::Word
1410 && !self
1411 .tokens
1412 .get(self.pos + 1)
1413 .is_some_and(|t| t.kind == TokenKind::LeftParen || t.kind == TokenKind::LeftBrace)
1414 {
1415 None
1416 } else {
1417 Some(self.advance().text.clone())
1418 };
1419 let command = self.parse_command();
1420 Command::Coproc(Box::new(Coproc { name, command }))
1421 }
1422
1423 fn at_any(&self, kinds: &[TokenKind]) -> bool {
1427 kinds.iter().any(|k| self.at(*k))
1428 }
1429
1430 fn parse_compound_body(&mut self, stop: &[TokenKind]) -> Vec<CompleteCommand> {
1432 let mut commands = Vec::new();
1433 loop {
1434 self.skip_newlines();
1435 if self.at_eof() || self.at_any(stop) {
1436 break;
1437 }
1438 if self.at_command_start() {
1439 commands.push(self.parse_complete_command());
1440 }
1441 if !self.eat(TokenKind::Semi) && !self.eat(TokenKind::Newline) {
1443 if self.at_eof() || self.at_any(stop) {
1444 break;
1445 }
1446 let pos_before = self.pos;
1455 self.advance();
1456 if self.pos == pos_before {
1457 break;
1458 }
1459 }
1460 }
1461 commands
1462 }
1463
1464 fn parse_trailing_redirects(&mut self) -> Vec<Redirect> {
1465 let mut redirects = Vec::new();
1466 while self.at_redirect() {
1467 redirects.push(self.parse_redirect());
1468 }
1469 redirects
1470 }
1471
1472 fn parse_cond_command(&mut self) -> Command {
1475 self.advance(); let expr = self.parse_cond_or();
1477 if self.kind() == TokenKind::Word && self.peek().text.as_str() == "]]" {
1479 self.advance();
1480 }
1481 Command::Cond(Box::new(expr))
1482 }
1483
1484 fn parse_cond_or(&mut self) -> CondExpr {
1485 let mut left = self.parse_cond_and();
1486 while self.kind() == TokenKind::OrOr {
1487 self.advance();
1488 let right = self.parse_cond_and();
1489 left = CondExpr::Or(Box::new(left), Box::new(right));
1490 }
1491 left
1492 }
1493
1494 fn parse_cond_and(&mut self) -> CondExpr {
1495 let mut left = self.parse_cond_not();
1496 while self.kind() == TokenKind::AndAnd {
1497 self.advance();
1498 let right = self.parse_cond_not();
1499 left = CondExpr::And(Box::new(left), Box::new(right));
1500 }
1501 left
1502 }
1503
1504 fn parse_cond_not(&mut self) -> CondExpr {
1505 if self.kind() == TokenKind::Bang {
1506 self.advance();
1507 let expr = self.parse_cond_not();
1508 return CondExpr::Not(Box::new(expr));
1509 }
1510 self.parse_cond_primary()
1511 }
1512
1513 fn parse_cond_primary(&mut self) -> CondExpr {
1514 if self.at(TokenKind::LeftParen) {
1516 self.advance();
1517 let expr = self.parse_cond_or();
1518 if self.at(TokenKind::RightParen) {
1519 self.advance();
1520 }
1521 return expr;
1522 }
1523
1524 if self.kind() == TokenKind::Word || self.kind() == TokenKind::Number {
1526 let text = self.peek().text.clone();
1527 if let Some(op) = parse_unary_cond_op(text.as_str()) {
1528 self.advance(); let word = self.parse_cond_word();
1530 return CondExpr::Unary(op, word);
1531 }
1532 }
1533
1534 let left = self.parse_cond_word();
1539
1540 if self.at_cond_end() {
1542 return CondExpr::Unary(CondOp::StrNonEmpty, left);
1544 }
1545
1546 if self.kind() == TokenKind::Equals {
1548 self.advance();
1549 if self.kind() == TokenKind::Equals {
1551 self.advance();
1552 }
1553 let right = self.parse_cond_word();
1554 return CondExpr::Binary(left, CondOp::StrEq, right);
1555 }
1556
1557 if self.kind() == TokenKind::Bang {
1559 if self
1560 .tokens
1561 .get(self.pos + 1)
1562 .is_some_and(|t| t.kind == TokenKind::Equals)
1563 {
1564 self.advance(); self.advance(); let right = self.parse_cond_word();
1567 return CondExpr::Binary(left, CondOp::StrNeq, right);
1568 }
1569 }
1570
1571 if self.kind() == TokenKind::Less {
1573 self.advance();
1574 let right = self.parse_cond_word();
1575 return CondExpr::Binary(left, CondOp::StrLt, right);
1576 }
1577 if self.kind() == TokenKind::Greater {
1578 self.advance();
1579 let right = self.parse_cond_word();
1580 return CondExpr::Binary(left, CondOp::StrGt, right);
1581 }
1582
1583 let text = self.peek().text.clone();
1585 if let Some(op) = parse_binary_cond_op(text.as_str()) {
1586 self.advance(); let right = self.parse_cond_word();
1588 return CondExpr::Binary(left, op, right);
1589 }
1590
1591 CondExpr::Unary(CondOp::StrNonEmpty, left)
1593 }
1594
1595 fn at_cond_end(&self) -> bool {
1597 (self.kind() == TokenKind::Word && self.peek().text.as_str() == "]]")
1598 || self.at(TokenKind::AndAnd)
1599 || self.at(TokenKind::OrOr)
1600 || self.at(TokenKind::RightParen)
1601 || self.at_eof()
1602 }
1603
1604 fn parse_cond_word(&mut self) -> Word {
1606 if self.at_word() || self.kind() == TokenKind::Bang {
1607 self.parse_word()
1608 } else {
1609 Word {
1610 parts: vec![WordPart::Literal(CompactString::default())],
1611 span: self.span(),
1612 }
1613 }
1614 }
1615
1616 fn is_c_for_ahead(&self) -> bool {
1619 if let Some(next) = self.tokens.get(self.pos + 1) {
1621 if next.kind == TokenKind::LeftParen {
1622 if let Some(after) = self.tokens.get(self.pos + 2) {
1623 return after.kind == TokenKind::LeftParen;
1624 }
1625 }
1626 }
1627 false
1628 }
1629
1630 fn parse_c_for(&mut self) -> Command {
1631 self.expect(TokenKind::For); self.expect(TokenKind::LeftParen); self.expect(TokenKind::LeftParen); let mut exprs = [String::new(), String::new(), String::new()];
1637 let mut idx = 0;
1638 loop {
1639 if self.at_eof() {
1640 break;
1641 }
1642 if self.at(TokenKind::RightParen) {
1643 if self
1644 .tokens
1645 .get(self.pos + 1)
1646 .is_some_and(|t| t.kind == TokenKind::RightParen)
1647 {
1648 self.advance(); self.advance(); break;
1651 }
1652 exprs[idx.min(2)].push(')');
1653 self.advance();
1654 continue;
1655 }
1656 if self.at(TokenKind::LeftParen) {
1657 exprs[idx.min(2)].push('(');
1658 self.advance();
1659 continue;
1660 }
1661 if self.at(TokenKind::Semi) {
1662 self.advance();
1663 if idx < 2 {
1664 idx += 1;
1665 }
1666 continue;
1667 }
1668 let tok = self.advance().clone();
1669 if !exprs[idx.min(2)].is_empty() {
1670 exprs[idx.min(2)].push(' ');
1671 }
1672 exprs[idx.min(2)].push_str(&tok.text);
1673 }
1674
1675 self.skip_newlines();
1676 let (body, redirects) = if self.at(TokenKind::LeftBrace) {
1678 self.expect(TokenKind::LeftBrace);
1679 self.skip_newlines();
1680 let body = self.parse_compound_body(&[TokenKind::RightBrace]);
1681 self.expect(TokenKind::RightBrace);
1682 (body, self.parse_trailing_redirects())
1683 } else {
1684 self.expect(TokenKind::Do);
1685 self.skip_newlines();
1686 let body = self.parse_compound_body(&[TokenKind::Done]);
1687 self.expect(TokenKind::Done);
1688 (body, self.parse_trailing_redirects())
1689 };
1690
1691 Command::CFor(Box::new(CForClause {
1692 init: CompactString::new(&exprs[0]),
1693 condition: CompactString::new(&exprs[1]),
1694 step: CompactString::new(&exprs[2]),
1695 body,
1696 redirects,
1697 }))
1698 }
1699
1700 fn parse_repeat(&mut self) -> Command {
1703 self.advance(); let count = self.parse_word();
1705 self.skip_newlines();
1706 let (body, redirects) = if self.at(TokenKind::LeftBrace) {
1708 self.expect(TokenKind::LeftBrace);
1709 self.skip_newlines();
1710 let body = self.parse_compound_body(&[TokenKind::RightBrace]);
1711 self.expect(TokenKind::RightBrace);
1712 (body, self.parse_trailing_redirects())
1713 } else if self.at(TokenKind::Do) {
1714 self.expect(TokenKind::Do);
1715 self.skip_newlines();
1716 let body = self.parse_compound_body(&[TokenKind::Done]);
1717 self.expect(TokenKind::Done);
1718 (body, self.parse_trailing_redirects())
1719 } else {
1720 let cmd = self.parse_complete_command();
1721 (vec![cmd], vec![])
1722 };
1723 Command::Repeat(Box::new(RepeatClause {
1724 count,
1725 body,
1726 redirects,
1727 }))
1728 }
1729}
1730
1731fn parse_unary_cond_op(s: &str) -> Option<CondOp> {
1735 Some(match s {
1736 "-e" | "-a" => CondOp::FileExists,
1737 "-f" => CondOp::IsFile,
1738 "-d" => CondOp::IsDir,
1739 "-L" | "-h" => CondOp::IsSymlink,
1740 "-r" => CondOp::IsReadable,
1741 "-w" => CondOp::IsWritable,
1742 "-x" => CondOp::IsExecutable,
1743 "-s" => CondOp::IsNonEmpty,
1744 "-b" => CondOp::IsBlockDev,
1745 "-c" => CondOp::IsCharDev,
1746 "-p" => CondOp::IsFifo,
1747 "-S" => CondOp::IsSocket,
1748 "-u" => CondOp::IsSetuid,
1749 "-g" => CondOp::IsSetgid,
1750 "-k" => CondOp::IsSticky,
1751 "-O" => CondOp::OwnedByUser,
1752 "-G" => CondOp::OwnedByGroup,
1753 "-N" => CondOp::ModifiedSinceRead,
1754 "-t" => CondOp::IsTty,
1755 "-o" => CondOp::OptionSet,
1756 "-v" => CondOp::VarIsSet,
1757 "-z" => CondOp::StrEmpty,
1758 "-n" => CondOp::StrNonEmpty,
1759 _ => return None,
1760 })
1761}
1762
1763fn parse_binary_cond_op(s: &str) -> Option<CondOp> {
1765 Some(match s {
1766 "==" | "=" => CondOp::StrEq,
1767 "!=" => CondOp::StrNeq,
1768 "<" => CondOp::StrLt,
1769 ">" => CondOp::StrGt,
1770 "=~" => CondOp::StrMatch,
1771 "-eq" => CondOp::IntEq,
1772 "-ne" => CondOp::IntNe,
1773 "-lt" => CondOp::IntLt,
1774 "-le" => CondOp::IntLe,
1775 "-gt" => CondOp::IntGt,
1776 "-ge" => CondOp::IntGe,
1777 "-nt" => CondOp::NewerThan,
1778 "-ot" => CondOp::OlderThan,
1779 "-ef" => CondOp::SameFile,
1780 _ => return None,
1781 })
1782}
1783
1784fn strip_quotes(text: &str, quote: char) -> CompactString {
1785 let s = text.strip_prefix(quote).unwrap_or(text);
1786 let s = s.strip_suffix(quote).unwrap_or(s);
1787 CompactString::from(s)
1788}
1789
1790fn param_name_len(text: &str) -> usize {
1795 let bytes = text.as_bytes();
1796 match bytes.first() {
1797 Some(b) if b.is_ascii_digit() => 1,
1798 Some(b) if b.is_ascii_alphabetic() || *b == b'_' => bytes
1799 .iter()
1800 .position(|b| !(b.is_ascii_alphanumeric() || *b == b'_'))
1801 .unwrap_or(bytes.len()),
1802 _ => 0,
1803 }
1804}
1805
1806fn strip_ansi_c_quotes(text: &str) -> CompactString {
1809 let s = text.strip_prefix("$'").unwrap_or(text);
1810 let s = s.strip_suffix('\'').unwrap_or(s);
1811 CompactString::from(s)
1812}
1813
1814fn parse_fragment(src: &str) -> Program {
1821 let tokens = frost_lexer::tokenize_str(src);
1822 Parser::new(&tokens).parse()
1823}
1824
1825fn parse_double_quoted_parts(content: &str) -> Vec<WordPart> {
1842 let mut parts = Vec::new();
1843 let bytes = content.as_bytes();
1844 let mut i = 0;
1845 let mut literal_start = 0;
1846
1847 macro_rules! flush {
1849 ($upto:expr) => {
1850 if $upto > literal_start {
1851 parts.push(WordPart::Literal(CompactString::from(
1852 &content[literal_start..$upto],
1853 )));
1854 }
1855 };
1856 }
1857
1858 while i < bytes.len() {
1859 if bytes[i] == b'\\' && i + 1 < bytes.len() {
1860 let esc = bytes[i + 1];
1866 if matches!(esc, b'$' | b'`' | b'"' | b'\\' | b'\n') {
1867 flush!(i);
1868 if esc != b'\n' {
1869 parts.push(WordPart::Literal(CompactString::from(
1871 &content[i + 1..i + 2],
1872 )));
1873 }
1874 i += 2;
1875 literal_start = i;
1876 } else {
1877 i += 2;
1878 }
1879 continue;
1880 }
1881
1882 if bytes[i] != b'$' || i + 1 >= bytes.len() {
1883 i += 1;
1884 continue;
1885 }
1886
1887 match bytes[i + 1] {
1888 b'(' if bytes.get(i + 2) == Some(&b'(') => {
1890 let end = frost_lexer::matching_close(bytes, i + 1);
1891 let body_end = end.saturating_sub(2).max(i + 3);
1894 flush!(i);
1895 parts.push(WordPart::ArithSub(CompactString::from(
1896 &content[i + 3..body_end],
1897 )));
1898 i = end;
1899 literal_start = i;
1900 }
1901 b'(' => {
1903 let end = frost_lexer::matching_close(bytes, i + 1);
1904 let body_end = end.saturating_sub(1).max(i + 2);
1905 flush!(i);
1906 parts.push(WordPart::CommandSub(Box::new(parse_fragment(
1907 &content[i + 2..body_end],
1908 ))));
1909 i = end;
1910 literal_start = i;
1911 }
1912 b'{' => {
1915 let end = frost_lexer::matching_close(bytes, i + 1);
1916 let body_end = end.saturating_sub(1).max(i + 2);
1917 flush!(i);
1918 parts.push(WordPart::DollarBrace {
1919 param: CompactString::from(&content[i + 2..body_end]),
1920 operator: None,
1921 arg: None,
1922 });
1923 i = end;
1924 literal_start = i;
1925 }
1926 c if c.is_ascii_alphabetic() || c == b'_' => {
1928 let start = i + 1;
1929 let mut end = start;
1930 while end < bytes.len()
1931 && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_')
1932 {
1933 end += 1;
1934 }
1935 flush!(i);
1936 parts.push(WordPart::DollarVar(CompactString::from(
1937 &content[start..end],
1938 )));
1939 i = end;
1940 literal_start = i;
1941 }
1942 c @ (b'?' | b'!' | b'$' | b'#' | b'*' | b'@' | b'-' | b'0'..=b'9') => {
1944 let _ = c;
1945 flush!(i);
1946 parts.push(WordPart::DollarVar(CompactString::from(
1947 &content[i + 1..i + 2],
1948 )));
1949 i += 2;
1950 literal_start = i;
1951 }
1952 _ => i += 1,
1955 }
1956 }
1957
1958 flush!(bytes.len());
1959
1960 if parts.is_empty() {
1961 parts.push(WordPart::Literal(CompactString::default()));
1962 }
1963
1964 parts
1965}
1966
1967fn parse_fd_prefix(text: &str) -> u32 {
1968 text.bytes()
1969 .take_while(u8::is_ascii_digit)
1970 .fold(0u32, |acc, b| acc * 10 + u32::from(b - b'0'))
1971}
1972
1973#[cfg(test)]
1976mod tests {
1977 use super::*;
1978
1979 fn tokenize(input: &str) -> Vec<Token> {
1980 frost_lexer::tokenize_str(input)
1981 }
1982
1983 fn parse(input: &str) -> Program {
1984 let tokens = tokenize(input);
1985 Parser::new(&tokens).parse()
1986 }
1987
1988 fn first_simple(program: &Program) -> &SimpleCommand {
1989 match &program.commands[0].list.first.commands[0] {
1990 Command::Simple(s) => s,
1991 other => panic!("expected Simple, got {other:?}"),
1992 }
1993 }
1994
1995 #[test]
1996 fn parse_simple_command() {
1997 let p = parse("echo hello world");
1998 assert_eq!(p.commands.len(), 1);
1999 let cmd = first_simple(&p);
2000 assert_eq!(cmd.words.len(), 3);
2001 }
2002
2003 #[test]
2004 fn parse_empty_program() {
2005 let p = parse("");
2006 assert_eq!(p.commands.len(), 0);
2007 }
2008
2009 #[test]
2010 fn parse_newlines_only() {
2011 let p = parse("\n\n\n");
2012 assert_eq!(p.commands.len(), 0);
2013 }
2014
2015 #[test]
2016 fn parse_semicolons() {
2017 let p = parse("echo a; echo b; echo c");
2018 assert_eq!(p.commands.len(), 3);
2019 }
2020
2021 #[test]
2022 fn parse_pipe() {
2023 let p = parse("cat file | grep pattern | wc -l");
2024 let pipeline = &p.commands[0].list.first;
2025 assert_eq!(pipeline.commands.len(), 3);
2026 }
2027
2028 #[test]
2029 fn parse_and_or_list() {
2030 let p = parse("test -f file && cat file || echo missing");
2031 let list = &p.commands[0].list;
2032 assert_eq!(list.rest.len(), 2);
2033 assert_eq!(list.rest[0].0, ListOp::And);
2034 assert_eq!(list.rest[1].0, ListOp::Or);
2035 }
2036
2037 #[test]
2038 fn parse_background() {
2039 let p = parse("sleep 10 &");
2040 assert!(p.commands[0].is_async);
2041 }
2042
2043 #[test]
2044 fn parse_bang() {
2045 let p = parse("! false");
2046 assert!(p.commands[0].list.first.bang);
2047 }
2048
2049 #[test]
2050 fn bang_is_literal_word_mid_command() {
2051 fn literal(word: &Word) -> String {
2057 word.parts
2058 .iter()
2059 .map(|p| match p {
2060 WordPart::Literal(s) => s.as_str(),
2061 other => panic!("expected Literal, got {other:?}"),
2062 })
2063 .collect()
2064 }
2065 let rows: &[(&str, &[&str])] = &[
2066 (
2067 "test ! -d /nonexistent",
2068 &["test", "!", "-d", "/nonexistent"],
2069 ),
2070 ("test a != b", &["test", "a", "!=", "b"]),
2071 ("test a!=b", &["test", "a!=b"]),
2072 ];
2073 let mut failures: Vec<String> = Vec::new();
2074 for (src, expect) in rows {
2075 let p = parse(src);
2076 let got: Vec<String> = first_simple(&p).words.iter().map(literal).collect();
2077 if got != *expect {
2078 failures.push(format!("{src:?} → {got:?}, expected {expect:?}"));
2079 }
2080 }
2081 assert!(
2082 failures.is_empty(),
2083 "{} bang-word rows failed:\n - {}",
2084 failures.len(),
2085 failures.join("\n - ")
2086 );
2087 }
2088
2089 #[test]
2090 fn parse_redirect_output() {
2091 let p = parse("echo hello > file.txt");
2092 let cmd = first_simple(&p);
2093 assert_eq!(cmd.redirects.len(), 1);
2094 assert_eq!(cmd.redirects[0].op, RedirectOp::Greater);
2095 }
2096
2097 #[test]
2098 fn parse_redirect_append() {
2099 let p = parse("echo hello >> file.txt");
2100 let cmd = first_simple(&p);
2101 assert_eq!(cmd.redirects[0].op, RedirectOp::DoubleGreater);
2102 }
2103
2104 #[test]
2105 fn parse_redirect_input() {
2106 let p = parse("cat < input.txt");
2107 let cmd = first_simple(&p);
2108 assert_eq!(cmd.redirects[0].op, RedirectOp::Less);
2109 }
2110
2111 #[test]
2112 fn parse_assignment() {
2113 let p = parse("FOO=bar");
2114 let cmd = first_simple(&p);
2115 assert_eq!(cmd.assignments.len(), 1);
2116 assert_eq!(cmd.assignments[0].name.as_str(), "FOO");
2117 assert_eq!(cmd.assignments[0].op, AssignOp::Assign);
2118 }
2119
2120 #[test]
2121 fn parse_assignment_before_command() {
2122 let p = parse("FOO=bar echo hello");
2123 let cmd = first_simple(&p);
2124 assert_eq!(cmd.assignments.len(), 1);
2125 assert_eq!(cmd.words.len(), 2);
2126 }
2127
2128 #[test]
2129 fn parse_single_quoted() {
2130 let p = parse("echo 'hello world'");
2131 let cmd = first_simple(&p);
2132 assert_eq!(cmd.words.len(), 2);
2133 match &cmd.words[1].parts[0] {
2134 WordPart::SingleQuoted(s) => assert_eq!(s.as_str(), "hello world"),
2135 other => panic!("expected SingleQuoted, got {other:?}"),
2136 }
2137 }
2138
2139 #[test]
2140 fn parse_double_quoted_with_var() {
2141 let p = parse(r#"echo "hello $name""#);
2142 let cmd = first_simple(&p);
2143 assert_eq!(cmd.words.len(), 2);
2144 let parts = &cmd.words[1].parts;
2149 assert_eq!(
2150 parts.len(),
2151 1,
2152 "outer word should have one DoubleQuoted part"
2153 );
2154 match &parts[0] {
2155 WordPart::DoubleQuoted(inner) => {
2156 assert!(
2157 inner
2158 .iter()
2159 .any(|p| matches!(p, WordPart::Literal(s) if s.contains("hello"))),
2160 "inner should carry the literal",
2161 );
2162 assert!(
2163 inner
2164 .iter()
2165 .any(|p| matches!(p, WordPart::DollarVar(n) if n.as_str() == "name")),
2166 "inner should carry the DollarVar",
2167 );
2168 }
2169 other => panic!("expected DoubleQuoted wrapper, got {other:?}"),
2170 }
2171 }
2172
2173 fn dq_inner(p: &Program, idx: usize) -> Vec<WordPart> {
2181 let parts = &first_simple(p).words[idx].parts;
2182 assert_eq!(parts.len(), 1, "expected one DoubleQuoted part: {parts:?}");
2183 match &parts[0] {
2184 WordPart::DoubleQuoted(inner) => inner.clone(),
2185 other => panic!("expected DoubleQuoted, got {other:?}"),
2186 }
2187 }
2188
2189 #[test]
2190 fn double_quoted_command_substitution_is_parsed() {
2191 let inner = dq_inner(&parse(r#"echo "$(echo hi)""#), 1);
2192 assert!(
2193 matches!(inner.as_slice(), [WordPart::CommandSub(_)]),
2194 "expected a CommandSub, got {inner:?}"
2195 );
2196 }
2197
2198 #[test]
2199 fn double_quoted_command_substitution_keeps_surrounding_literals() {
2200 let inner = dq_inner(&parse(r#"echo "x$(echo hi)y""#), 1);
2201 assert!(
2202 matches!(
2203 inner.as_slice(),
2204 [
2205 WordPart::Literal(a),
2206 WordPart::CommandSub(_),
2207 WordPart::Literal(b)
2208 ] if a == "x" && b == "y"
2209 ),
2210 "got {inner:?}"
2211 );
2212 }
2213
2214 #[test]
2215 fn double_quoted_command_substitution_spans_inner_double_quotes() {
2216 let inner = dq_inner(&parse(r#"echo "$(printf %s "export D=1")""#), 1);
2220 match inner.as_slice() {
2221 [WordPart::CommandSub(prog)] => {
2222 let sub = match &prog.commands[0].list.first.commands[0] {
2223 Command::Simple(s) => s,
2224 other => panic!("expected Simple, got {other:?}"),
2225 };
2226 assert_eq!(sub.words.len(), 3, "printf %s \"export D=1\"");
2227 }
2228 other => panic!("expected one CommandSub, got {other:?}"),
2229 }
2230 }
2231
2232 #[test]
2233 fn double_quoted_arithmetic_substitution_is_parsed() {
2234 let inner = dq_inner(&parse(r#"echo "$((2+2))""#), 1);
2235 assert!(
2236 matches!(inner.as_slice(), [WordPart::ArithSub(e)] if e == "2+2"),
2237 "got {inner:?}"
2238 );
2239 let inner = dq_inner(&parse(r#"echo "$(( (1+2) * 3 ))""#), 1);
2241 assert!(
2242 matches!(inner.as_slice(), [WordPart::ArithSub(e)] if e == " (1+2) * 3 "),
2243 "got {inner:?}"
2244 );
2245 }
2246
2247 #[test]
2248 fn double_quoted_backslash_escapes_are_removed() {
2249 let inner = dq_inner(&parse(r#"echo "a\"b""#), 1);
2251 let joined: String = inner
2252 .iter()
2253 .map(|p| match p {
2254 WordPart::Literal(s) => s.to_string(),
2255 other => panic!("expected literals, got {other:?}"),
2256 })
2257 .collect();
2258 assert_eq!(joined, "a\"b");
2259 }
2260
2261 #[test]
2262 fn ansi_c_quoting_gets_its_own_part() {
2263 let p = parse(r"echo $'a\nb'");
2265 let parts = &first_simple(&p).words[1].parts;
2266 assert!(
2267 matches!(parts.as_slice(), [WordPart::AnsiCQuoted(raw)] if raw == r"a\nb"),
2268 "got {parts:?}"
2269 );
2270 }
2271
2272 #[test]
2278 fn adjacent_brace_expansions_stay_in_one_word() {
2279 let p = parse("x=${a}${b}");
2280 let assign = &first_simple(&p).assignments[0];
2281 let parts = &assign.value.as_ref().expect("value").parts;
2282 assert_eq!(parts.len(), 2, "both ${{…}} must survive: {parts:?}");
2283 assert!(
2284 parts
2285 .iter()
2286 .all(|p| matches!(p, WordPart::DollarBrace { .. }))
2287 );
2288 assert!(
2289 first_simple(&p).words.is_empty(),
2290 "nothing may leak out as a command word"
2291 );
2292 }
2293
2294 #[test]
2295 fn brace_expansion_then_literal_stays_in_one_word() {
2296 let p = parse("y=${a}Z");
2297 let cmd = first_simple(&p);
2298 let parts = &cmd.assignments[0].value.as_ref().expect("value").parts;
2299 assert!(
2300 matches!(
2301 parts.as_slice(),
2302 [WordPart::DollarBrace { .. }, WordPart::Literal(z)] if z == "Z"
2303 ),
2304 "got {parts:?}"
2305 );
2306 assert!(cmd.words.is_empty(), "`Z` must not become a command");
2307 }
2308
2309 #[test]
2310 fn adjacent_command_substitutions_stay_in_one_word() {
2311 let p = parse("x=$(echo A)$(echo B)");
2312 let parts = &first_simple(&p).assignments[0]
2313 .value
2314 .as_ref()
2315 .expect("value")
2316 .parts;
2317 assert_eq!(parts.len(), 2, "got {parts:?}");
2318 assert!(parts.iter().all(|p| matches!(p, WordPart::CommandSub(_))));
2319 }
2320
2321 #[test]
2322 fn dollar_var_stops_at_a_non_name_byte() {
2323 let p = parse("echo $d/xx");
2326 let parts = &first_simple(&p).words[1].parts;
2327 assert!(
2328 matches!(
2329 parts.as_slice(),
2330 [WordPart::DollarVar(n), WordPart::Literal(rest)]
2331 if n == "d" && rest == "/xx"
2332 ),
2333 "got {parts:?}"
2334 );
2335 }
2336
2337 #[test]
2338 fn tilde_expands_only_at_a_words_start() {
2339 let p = parse("echo a~b");
2341 let parts = &first_simple(&p).words[1].parts;
2342 assert!(
2343 !parts.iter().any(|p| matches!(p, WordPart::Tilde(_))),
2344 "mid-word `~` must stay literal: {parts:?}"
2345 );
2346 }
2347
2348 #[test]
2351 fn a_stray_closing_paren_does_not_spin_parse_program() {
2352 let p = parse("echo a ) echo b");
2356 assert!(!p.commands.is_empty());
2357 assert!(parse(")").commands.is_empty() || true);
2358 assert!(parse("}").commands.is_empty() || true);
2359 }
2360
2361 #[test]
2362 fn reserved_word_in_argument_position_is_a_plain_word() {
2363 for kw in ["done", "fi", "then", "else", "elif", "do", "esac"] {
2366 let p = parse(&format!("echo {kw}"));
2367 let cmd = first_simple(&p);
2368 assert_eq!(cmd.words.len(), 2, "`echo {kw}` must be two words");
2369 }
2370 let p = parse("echo a done b");
2371 assert_eq!(first_simple(&p).words.len(), 4);
2372 }
2373
2374 #[test]
2375 fn control_flow_keywords_still_terminate_their_blocks() {
2376 let p = parse("if true; then echo A; fi");
2379 assert_eq!(p.commands.len(), 1);
2380 let p = parse("for i in a b; do echo $i; done");
2381 assert_eq!(p.commands.len(), 1);
2382 let p = parse("while false; do echo x; done");
2383 assert_eq!(p.commands.len(), 1);
2384 }
2385
2386 #[test]
2387 fn parse_dollar_var() {
2388 let p = parse("echo $HOME");
2389 let cmd = first_simple(&p);
2390 assert_eq!(cmd.words.len(), 2);
2391 match &cmd.words[1].parts[0] {
2392 WordPart::DollarVar(name) => assert_eq!(name.as_str(), "HOME"),
2393 other => panic!("expected DollarVar, got {other:?}"),
2394 }
2395 }
2396
2397 #[test]
2398 fn parse_dollar_hash_special_param() {
2399 let p = parse("echo $#");
2405 let cmd = first_simple(&p);
2406 assert_eq!(cmd.words.len(), 2);
2407 match &cmd.words[1].parts[0] {
2408 WordPart::DollarVar(name) => assert_eq!(name.as_str(), "#"),
2409 other => panic!("expected DollarVar(\"#\"), got {other:?}"),
2410 }
2411 }
2412
2413 #[test]
2414 fn parse_dollar_hash_in_compound_word() {
2415 let p = parse("echo n=$#");
2417 let cmd = first_simple(&p);
2418 let parts = &cmd.words[1].parts;
2419 assert!(
2420 parts
2421 .iter()
2422 .any(|p| matches!(p, WordPart::DollarVar(n) if n.as_str() == "#")),
2423 "compound word should carry DollarVar(\"#\"), got {parts:?}",
2424 );
2425 }
2426
2427 #[test]
2428 fn parse_if_then_fi() {
2429 let p = parse("if true; then echo yes; fi");
2430 match &p.commands[0].list.first.commands[0] {
2431 Command::If(clause) => {
2432 assert_eq!(clause.condition.len(), 1);
2433 assert_eq!(clause.then_body.len(), 1);
2434 assert!(clause.else_body.is_none());
2435 }
2436 other => panic!("expected If, got {other:?}"),
2437 }
2438 }
2439
2440 #[test]
2441 fn parse_if_else() {
2442 let p = parse("if false; then echo no; else echo yes; fi");
2443 match &p.commands[0].list.first.commands[0] {
2444 Command::If(clause) => {
2445 assert!(clause.else_body.is_some());
2446 }
2447 other => panic!("expected If, got {other:?}"),
2448 }
2449 }
2450
2451 #[test]
2452 fn parse_if_elif() {
2453 let p = parse("if false; then echo 1; elif true; then echo 2; else echo 3; fi");
2454 match &p.commands[0].list.first.commands[0] {
2455 Command::If(clause) => {
2456 assert_eq!(clause.elifs.len(), 1);
2457 assert!(clause.else_body.is_some());
2458 }
2459 other => panic!("expected If, got {other:?}"),
2460 }
2461 }
2462
2463 #[test]
2464 fn parse_for_loop() {
2465 let p = parse("for x in a b c; do echo $x; done");
2466 match &p.commands[0].list.first.commands[0] {
2467 Command::For(clause) => {
2468 assert_eq!(clause.var.as_str(), "x");
2469 assert_eq!(clause.words.as_ref().unwrap().len(), 3);
2470 assert_eq!(clause.body.len(), 1);
2471 }
2472 other => panic!("expected For, got {other:?}"),
2473 }
2474 }
2475
2476 #[test]
2477 fn parse_while_loop() {
2478 let p = parse("while true; do echo loop; done");
2479 match &p.commands[0].list.first.commands[0] {
2480 Command::While(clause) => {
2481 assert_eq!(clause.condition.len(), 1);
2482 assert_eq!(clause.body.len(), 1);
2483 }
2484 other => panic!("expected While, got {other:?}"),
2485 }
2486 }
2487
2488 #[test]
2489 fn parse_case() {
2490 let p = parse("case $x in\n a) echo A ;;\n b) echo B ;;\nesac");
2491 match &p.commands[0].list.first.commands[0] {
2492 Command::Case(clause) => {
2493 assert_eq!(clause.items.len(), 2);
2494 }
2495 other => panic!("expected Case, got {other:?}"),
2496 }
2497 }
2498
2499 #[test]
2500 fn parse_subshell() {
2501 let p = parse("(echo hello)");
2502 assert!(matches!(
2503 &p.commands[0].list.first.commands[0],
2504 Command::Subshell(_)
2505 ));
2506 }
2507
2508 #[test]
2509 fn parse_brace_group() {
2510 let p = parse("{ echo hello; }");
2511 assert!(matches!(
2512 &p.commands[0].list.first.commands[0],
2513 Command::BraceGroup(_)
2514 ));
2515 }
2516
2517 #[test]
2518 fn parse_function_keyword() {
2519 let p = parse("function greet { echo hello; }");
2520 match &p.commands[0].list.first.commands[0] {
2521 Command::FunctionDef(f) => assert_eq!(f.name.as_str(), "greet"),
2522 other => panic!("expected FunctionDef, got {other:?}"),
2523 }
2524 }
2525
2526 #[test]
2527 fn parse_function_parens() {
2528 let p = parse("greet() { echo hello; }");
2529 match &p.commands[0].list.first.commands[0] {
2530 Command::FunctionDef(f) => assert_eq!(f.name.as_str(), "greet"),
2531 other => panic!("expected FunctionDef, got {other:?}"),
2532 }
2533 }
2534
2535 #[test]
2536 fn parse_tilde() {
2537 let p = parse("cd ~");
2538 let cmd = first_simple(&p);
2539 assert!(matches!(&cmd.words[1].parts[0], WordPart::Tilde(_)));
2540 }
2541
2542 #[test]
2543 fn parse_glob_star() {
2544 let p = parse("ls *");
2545 let cmd = first_simple(&p);
2546 assert!(matches!(
2547 &cmd.words[1].parts[0],
2548 WordPart::Glob(GlobKind::Star)
2549 ));
2550 }
2551
2552 #[test]
2553 fn parse_multiple_commands_newlines() {
2554 let p = parse("echo a\necho b\necho c\n");
2555 assert_eq!(p.commands.len(), 3);
2556 }
2557
2558 #[test]
2559 fn parse_herestring() {
2560 let p = parse("cat <<< 'hello'");
2561 let cmd = first_simple(&p);
2562 assert_eq!(cmd.redirects[0].op, RedirectOp::TripleLess);
2563 }
2564
2565 #[test]
2566 fn parse_time() {
2567 let p = parse("time ls -la");
2568 assert!(matches!(
2569 &p.commands[0].list.first.commands[0],
2570 Command::Time(_)
2571 ));
2572 }
2573
2574 #[test]
2575 fn parse_multiline_if() {
2576 let p = parse("if true\nthen\n echo yes\nfi");
2577 assert!(matches!(
2578 &p.commands[0].list.first.commands[0],
2579 Command::If(_)
2580 ));
2581 }
2582}
2583
2584#[cfg(test)]
2585mod syntax_error_recording {
2586 use super::*;
2587
2588 fn parse_src(src: &str) -> crate::ast::Program {
2589 let toks = frost_lexer::tokenize(src.as_bytes());
2590 Parser::new(&toks).parse()
2591 }
2592
2593 #[test]
2598 fn unterminated_while_records_a_syntax_error() {
2599 let p = parse_src("while true\n");
2600 assert!(
2601 !p.syntax_errors.is_empty(),
2602 "a missing `do` must be recorded, not silently recovered"
2603 );
2604 }
2605
2606 #[test]
2611 fn well_formed_input_records_nothing() {
2612 for src in [
2613 "while true; do echo x; done\n",
2614 "echo done\n",
2615 "if true; then echo y; fi\n",
2616 "for i in 1 2 3; do echo $i; done\n",
2617 "case x in y) echo z ;; esac\n",
2618 "f() { echo hi; }\n",
2619 ] {
2620 let p = parse_src(src);
2621 assert!(
2622 p.syntax_errors.is_empty(),
2623 "well-formed input recorded errors: {src:?} -> {:?}",
2624 p.syntax_errors
2625 );
2626 }
2627 }
2628}
2629
2630#[cfg(test)]
2631mod live_rc_parses_clean {
2632 use super::*;
2633
2634 #[test]
2643 fn the_shipped_frostmourne_rc_has_no_recovered_syntax_errors() {
2644 const RC: &str = "/nix/store/jmvklqqzydxrdiyb8bsjxi1d8i78i8q1-frostmourne-rc.lisp/share/frostmourne/rc.lisp";
2645 let Ok(src) = std::fs::read_to_string(RC) else {
2646 eprintln!("SKIP: {RC} not present on this host");
2647 return;
2648 };
2649 let toks = frost_lexer::tokenize(src.as_bytes());
2650 let p = Parser::new(&toks).parse();
2651 assert!(
2652 p.syntax_errors.is_empty(),
2653 "the shipped rc records {} syntax errors; refusing to execute it \
2654 would brick the shell. First few: {:?}",
2655 p.syntax_errors.len(),
2656 &p.syntax_errors[..p.syntax_errors.len().min(5)]
2657 );
2658 }
2659}