1use crate::ast::{
7 Arg, Assignment, BinaryOp, CaseBranch, CaseStmt, Command, Expr, FileTestOp, ForLoop,
8 HereDocMeta, IfStmt, ListElem, Pipeline, PipelineStage, Program, RecordEntry, RecordKey,
9 Redirect, RedirectKind, SpannedPart, Stmt, StringPart, StringTestOp, TestCmpOp, TestExpr,
10 ToolDef, Value,
11 VarPath, VarSegment, WhileLoop,
12};
13use crate::lexer::{self, HereDocData, Token};
14use chumsky::input::{MappedInput, Stream, ValueInput};
15use chumsky::prelude::*;
16
17pub type Span = SimpleSpan;
19
20type ParserInput = MappedInput<'static, Token, Span, Stream<std::vec::IntoIter<(Token, Span)>>, PairFn>;
33
34type PairFn = fn((Token, Span)) -> (Token, Span);
38
39fn keep_pair(pair: (Token, Span)) -> (Token, Span) {
40 pair
41}
42
43thread_local! {
44 static CACHED_PARSER: Boxed<
56 'static,
57 'static,
58 ParserInput,
59 Program,
60 extra::Err<Rich<'static, Token, Span>>,
61 > = program_parser().boxed();
62}
63
64fn parse_var_expr(raw: &str) -> Expr {
71 if raw == "${?}" {
73 return Expr::LastExitCode;
74 }
75
76 if raw == "${$}" {
78 return Expr::CurrentPid;
79 }
80
81 if let Some(colon_idx) = find_default_separator(raw) {
84 let path = parse_varpath(&format!("${{{}}}", &raw[2..colon_idx]));
86 let default_str = &raw[colon_idx + 2..raw.len() - 1];
89 let default_word = unquote_default_word(default_str);
98 let default = parse_interpolated_string(&default_word)
99 .unwrap_or_else(|_| vec![StringPart::Literal(default_word.clone())]);
100 return Expr::VarWithDefault { path, default };
101 }
102
103 Expr::VarRef(parse_varpath(raw))
105}
106
107pub(crate) fn bash_substring_hint(var_content: &str) -> Option<String> {
119 let mut depth = 0usize;
120 let colon = var_content.char_indices().find_map(|(i, c)| match c {
121 '[' => {
122 depth += 1;
123 None
124 }
125 ']' => {
126 depth = depth.saturating_sub(1);
127 None
128 }
129 ':' if depth == 0 => Some(i),
130 _ => None,
131 })?;
132 let (name, rest) = var_content.split_at(colon);
133 let after_offset = &rest[1..];
135 let suggestion = if let Some(length) = after_offset.strip_prefix(':') {
138 format!("${{{name}[0:{length}]}}")
139 } else if after_offset.contains(':') {
140 format!("${{{name}[{after_offset}]}}")
141 } else {
142 format!("${{{name}[{after_offset}:]}}")
143 };
144 Some(format!(
145 "${{{var_content}}}: kaish slices with brackets, not `:offset:length` — \
146 write {suggestion}. Brackets are start:end and end-exclusive, so \
147 ${{{name}[0:5]}} is the first five characters and ${{{name}[-3:]}} the last three."
148 ))
149}
150
151fn unquote_default_word(word: &str) -> String {
181 let mut out = String::with_capacity(word.len());
182 let mut in_single = false;
183 let mut in_double = false;
184 let chars: Vec<char> = word.chars().collect();
185 let mut i = 0;
186 while i < chars.len() {
187 let ch = chars[i];
188 if ch == '\\' && !in_single {
192 let run_start = i;
193 while i < chars.len() && chars[i] == '\\' {
194 i += 1;
195 }
196 let run_len = i - run_start;
197 let next_is_quote =
200 chars.get(i).is_some_and(|c| *c == '"' || (*c == '\'' && !in_double));
201 if next_is_quote {
202 if run_len / 2 > 0 {
203 out.push_str(&"\\".repeat(run_len / 2));
204 }
205 if run_len % 2 == 1 {
206 out.push(chars[i]);
210 i += 1;
211 }
212 } else {
215 out.push_str(&"\\".repeat(run_len));
216 }
217 continue;
218 }
219 i += 1;
220 match ch {
221 '\'' if !in_double => in_single = !in_single,
224 '"' if !in_single => in_double = !in_double,
225 '$' if in_single => out.push_str("__KAISH_ESCAPED_DOLLAR__"),
227 _ => out.push(ch),
228 }
229 }
230 out
231}
232
233fn find_default_separator(raw: &str) -> Option<usize> {
235 let bytes = raw.as_bytes();
236 let mut depth = 0;
237 let mut bracket_depth = 0;
238 let mut i = 0;
239
240 while i < bytes.len() {
241 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
242 depth += 1;
243 i += 2;
244 continue;
245 }
246 if bytes[i] == b'}' && depth > 0 {
247 depth -= 1;
248 i += 1;
249 continue;
250 }
251 if bytes[i] == b'[' {
254 bracket_depth += 1;
255 } else if bytes[i] == b']' && bracket_depth > 0 {
256 bracket_depth -= 1;
257 }
258 if depth == 1
261 && bracket_depth == 0
262 && i + 1 < bytes.len()
263 && bytes[i] == b':'
264 && bytes[i + 1] == b'-'
265 {
266 return Some(i);
267 }
268 i += 1;
269 }
270 None
271}
272
273fn find_default_separator_in_content(content: &str) -> Option<usize> {
275 let bytes = content.as_bytes();
276 let mut depth = 0;
277 let mut bracket_depth = 0;
278 let mut i = 0;
279
280 while i < bytes.len() {
281 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
282 depth += 1;
283 i += 2;
284 continue;
285 }
286 if bytes[i] == b'}' && depth > 0 {
287 depth -= 1;
288 i += 1;
289 continue;
290 }
291 if bytes[i] == b'[' {
294 bracket_depth += 1;
295 } else if bytes[i] == b']' && bracket_depth > 0 {
296 bracket_depth -= 1;
297 }
298 if depth == 0
300 && bracket_depth == 0
301 && i + 1 < bytes.len()
302 && bytes[i] == b':'
303 && bytes[i + 1] == b'-'
304 {
305 return Some(i);
306 }
307 i += 1;
308 }
309 None
310}
311
312fn is_name_char(c: char) -> bool {
324 c.is_ascii_alphanumeric() || c == '_' || !c.is_ascii()
325}
326
327fn is_name_start(c: char) -> bool {
330 c.is_ascii_alphabetic() || c == '_' || !c.is_ascii()
331}
332
333
334fn name_in_token_kind<'a>(
349 tok: &'a Token,
350 prev: Option<&Token>,
351 next: Option<&Token>,
352) -> Option<(&'a str, bool)> {
353 match tok {
354 Token::SimpleVarRef(name) => Some((name.as_str(), false)),
355 Token::VarLength(inner) => Some((root_of(inner), false)),
356 Token::VarRef(raw) => raw
357 .strip_prefix("${")
358 .and_then(|s| s.strip_suffix('}'))
359 .map(|r| (root_of(r), false)),
360 Token::Ident(name)
366 if matches!(next, Some(Token::Eq))
367 && match prev {
368 None => true,
369 Some(p) => crate::lexer::is_statement_boundary(p) || matches!(p, Token::Local),
370 } =>
371 {
372 Some((name.as_str(), true))
373 }
374 Token::Ident(name) if matches!(prev, Some(Token::For)) => Some((name.as_str(), false)),
378 _ => None,
379 }
380}
381
382fn bad_name_in_parts(parts: &[StringPart]) -> Option<crate::name::NameError> {
387 fn root(path: &VarPath) -> Option<&str> {
388 match path.segments.first() {
389 Some(VarSegment::Field(name)) => Some(name.as_str()),
390 _ => None,
391 }
392 }
393 for part in parts {
394 let bad = match part {
395 StringPart::Var(path) | StringPart::VarLength(path) => {
396 root(path).and_then(|n| crate::name::validate(n).err())
397 }
398 StringPart::VarWithDefault { path, default } => root(path)
399 .and_then(|n| crate::name::validate(n).err())
400 .or_else(|| bad_name_in_parts(default)),
401 _ => None,
404 };
405 if bad.is_some() {
406 return bad;
407 }
408 }
409 None
410}
411
412fn root_of(inner: &str) -> &str {
415 let end = inner.find(['[', '.', ':', '-']).unwrap_or(inner.len());
416 &inner[..end]
417}
418
419pub(crate) fn parse_varpath(raw: &str) -> VarPath {
420 let segment_strs = lexer::parse_var_ref(raw).unwrap_or_default();
421 let segments = segment_strs
422 .into_iter()
423 .enumerate()
424 .map(|(i, s)| {
425 if i == 0 {
426 VarSegment::Field(crate::ast::normalize_name(s))
429 } else if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
430 parse_subscript(inner)
431 } else {
432 VarSegment::Field(s)
435 }
436 })
437 .collect();
438 VarPath { segments }
439}
440
441fn parse_subscript(inner: &str) -> VarSegment {
448 if let Some(var) = inner.strip_prefix('$') {
450 return VarSegment::Dynamic(var.to_string());
451 }
452 if inner.len() >= 2
454 && ((inner.starts_with('"') && inner.ends_with('"'))
455 || (inner.starts_with('\'') && inner.ends_with('\'')))
456 {
457 return VarSegment::Key(inner[1..inner.len() - 1].to_string());
458 }
459 if let Some((lhs, rhs)) = inner.split_once(':') {
463 let bound = |s: &str| -> Option<Option<i64>> {
464 if s.is_empty() {
465 Some(None)
466 } else {
467 s.parse::<i64>().ok().map(Some)
468 }
469 };
470 if let (Some(start), Some(end)) = (bound(lhs), bound(rhs)) {
471 return VarSegment::Slice(start, end);
472 }
473 }
474 if let Ok(i) = inner.parse::<i64>() {
476 return VarSegment::Index(i);
477 }
478 VarSegment::Key(inner.to_string())
480}
481
482fn strip_empty_stmts(statements: Vec<Stmt>) -> Vec<Stmt> {
485 statements
486 .into_iter()
487 .filter(|s| !matches!(s, Stmt::Empty))
488 .collect()
489}
490
491fn parse_interpolated_string_spanned(
510 s: &str,
511 base_offset: usize,
512) -> Result<Vec<SpannedPart>, String> {
513 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
514
515 let chars_vec: Vec<char> = s.chars().collect();
516 let mut i = 0;
517 let mut pos: usize = 0;
518
519 let mut parts: Vec<SpannedPart> = Vec::new();
520 let mut current_text = String::new();
521 let mut current_text_start: usize = pos;
522
523 let push_literal =
524 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
525 if !current_text.is_empty() {
526 parts.push(SpannedPart {
527 part: StringPart::Literal(std::mem::take(current_text)),
528 offset: base_offset + *start,
529 len: end - *start,
530 });
531 *start = end;
532 }
533 };
534
535 while i < chars_vec.len() {
536 let ch = chars_vec[i];
537
538 if ch == '\x00' {
539 let start = pos;
541 i += 1;
542 pos += 1;
543 let mut marker = String::new();
544 while let Some(&c) = chars_vec.get(i) {
545 if c == '\x00' {
546 i += 1;
547 pos += 1;
548 break;
549 }
550 marker.push(c);
551 i += 1;
552 pos += c.len_utf8();
553 }
554 if marker == "DOLLAR" {
555 if current_text.is_empty() {
556 current_text_start = start;
557 }
558 current_text.push('$');
559 }
560 } else if ch == '\\' {
561 let next = chars_vec.get(i + 1).copied();
567 match next {
568 Some('$') => {
569 if current_text.is_empty() {
570 current_text_start = pos;
571 }
572 current_text.push('$');
573 i += 2;
574 pos += 2;
575 }
576 Some('\\') => {
577 if current_text.is_empty() {
578 current_text_start = pos;
579 }
580 current_text.push('\\');
581 i += 2;
582 pos += 2;
583 }
584 Some('\n') => {
585 i += 2;
588 pos += 2;
589 if current_text.is_empty() {
590 current_text_start = pos;
591 }
592 }
593 Some('\r') => {
594 i += 2;
596 pos += 2;
597 if chars_vec.get(i) == Some(&'\n') {
598 i += 1;
599 pos += 1;
600 }
601 if current_text.is_empty() {
602 current_text_start = pos;
603 }
604 }
605 _ => {
606 if current_text.is_empty() {
610 current_text_start = pos;
611 }
612 current_text.push('\\');
613 i += 1;
614 pos += 1;
615 }
616 }
617 } else if ch == '$' {
618 let part_start = pos;
620 let next = chars_vec.get(i + 1).copied();
621
622 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
623 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
625 i += 2; pos += 2;
627 let mut cmd_content = String::new();
628 let mut depth = 1;
629 let mut closed = false;
630 while let Some(&c) = chars_vec.get(i) {
631 i += 1;
632 pos += c.len_utf8();
633 if c == '(' {
634 depth += 1;
635 cmd_content.push(c);
636 } else if c == ')' {
637 depth -= 1;
638 if depth == 0 {
639 closed = true;
640 break;
641 }
642 cmd_content.push(c);
643 } else {
644 cmd_content.push(c);
645 }
646 }
647 if !closed {
648 return Err("unterminated command substitution: missing `)`".to_string());
649 }
650 let inserted = if let Ok(program) = parse(&cmd_content) {
657 let stmts = strip_empty_stmts(program.statements);
660 if stmts.is_empty() {
661 false
662 } else {
663 parts.push(SpannedPart {
664 part: StringPart::CommandSubst(stmts),
665 offset: base_offset + part_start,
666 len: pos - part_start,
667 });
668 true
669 }
670 } else {
671 return Err(format!(
672 "syntax error in command substitution: $({cmd_content})"
673 ));
674 };
675 if inserted {
676 current_text_start = pos;
679 } else {
680 if current_text.is_empty() {
685 current_text_start = part_start;
686 }
687 current_text.push_str("$(");
688 current_text.push_str(&cmd_content);
689 current_text.push(')');
690 }
691 } else if next == Some('{') {
692 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
693 i += 2; pos += 2;
695 let mut var_content = String::new();
696 let mut depth = 1;
697 while let Some(&c) = chars_vec.get(i) {
698 i += 1;
699 pos += c.len_utf8();
700 if c == '{' && var_content.ends_with('$') {
701 depth += 1;
702 var_content.push(c);
703 } else if c == '}' {
704 depth -= 1;
705 if depth == 0 {
706 break;
707 }
708 var_content.push(c);
709 } else {
710 var_content.push(c);
711 }
712 }
713 let part = if let Some(name) = var_content.strip_prefix('#') {
714 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
715 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
716 let expr = var_content
717 .strip_prefix("__ARITH:")
718 .and_then(|s| s.strip_suffix("__"))
719 .unwrap_or("");
720 StringPart::Arithmetic(expr.to_string())
721 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
722 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
723 let default_str = &var_content[colon_idx + 2..];
724 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
735 StringPart::VarWithDefault { path, default }
736 } else if let Some(msg) = bash_substring_hint(&var_content) {
737 return Err(msg);
738 } else {
739 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
740 };
741 parts.push(SpannedPart {
742 part,
743 offset: base_offset + part_start,
744 len: pos - part_start,
745 });
746 current_text_start = pos;
747 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
748 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
749 i += 1; pos += 1;
751 if let Some(&digit) = chars_vec.get(i) {
752 let n = digit.to_digit(10).unwrap_or(0) as usize;
753 i += 1;
754 pos += digit.len_utf8();
755 parts.push(SpannedPart {
756 part: StringPart::Positional(n),
757 offset: base_offset + part_start,
758 len: pos - part_start,
759 });
760 }
761 current_text_start = pos;
762 } else if next == Some('@') {
763 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
764 i += 2; pos += 2;
766 parts.push(SpannedPart {
767 part: StringPart::AllArgs,
768 offset: base_offset + part_start,
769 len: pos - part_start,
770 });
771 current_text_start = pos;
772 } else if next == Some('#') {
773 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
774 i += 2; pos += 2;
776 parts.push(SpannedPart {
777 part: StringPart::ArgCount,
778 offset: base_offset + part_start,
779 len: pos - part_start,
780 });
781 current_text_start = pos;
782 } else if next == Some('?') {
783 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
784 i += 2; pos += 2;
786 parts.push(SpannedPart {
787 part: StringPart::LastExitCode,
788 offset: base_offset + part_start,
789 len: pos - part_start,
790 });
791 current_text_start = pos;
792 } else if next == Some('$') {
793 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
794 i += 2; pos += 2;
796 parts.push(SpannedPart {
797 part: StringPart::CurrentPid,
798 offset: base_offset + part_start,
799 len: pos - part_start,
800 });
801 current_text_start = pos;
802 } else if next.map(is_name_start).unwrap_or(false) {
803 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
804 i += 1; pos += 1;
806 let mut var_name = String::new();
807 while let Some(&c) = chars_vec.get(i) {
808 if is_name_char(c) {
809 var_name.push(c);
810 i += 1;
811 pos += c.len_utf8();
812 } else {
813 break;
814 }
815 }
816 parts.push(SpannedPart {
817 part: StringPart::Var(VarPath::simple(var_name)),
818 offset: base_offset + part_start,
819 len: pos - part_start,
820 });
821 current_text_start = pos;
822 } else {
823 if current_text.is_empty() {
825 current_text_start = pos;
826 }
827 current_text.push(ch);
828 i += 1;
829 pos += 1;
830 }
831 } else {
832 if current_text.is_empty() {
833 current_text_start = pos;
834 }
835 current_text.push(ch);
836 i += 1;
837 pos += ch.len_utf8();
838 }
839 }
840
841 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
842
843 Ok(parts)
844}
845
846fn parse_interpolated_string(s: &str) -> Result<Vec<StringPart>, String> {
847 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
850
851 let mut parts = Vec::new();
852 let mut current_text = String::new();
853 let mut chars = s.chars().peekable();
854
855 while let Some(ch) = chars.next() {
856 if ch == '\x00' {
857 let mut marker = String::new();
859 while let Some(&c) = chars.peek() {
860 if c == '\x00' {
861 chars.next(); break;
863 }
864 if let Some(c) = chars.next() {
865 marker.push(c);
866 }
867 }
868 if marker == "DOLLAR" {
869 current_text.push('$');
870 }
871 } else if ch == '$' {
872 if chars.peek() == Some(&'(') {
874 if !current_text.is_empty() {
876 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
877 }
878
879 chars.next();
881
882 let remainder: String = chars.clone().collect();
892 let close = lexer::tokenize(&remainder).ok().and_then(|toks| {
893 let toks: Vec<(Token, Span)> = toks
894 .into_iter()
895 .map(|sp| (sp.token, (sp.span.start..sp.span.end).into()))
896 .collect();
897 find_cmd_subst_close(&toks).map(|idx| toks[idx].1)
898 });
899 let Some(rparen_span) = close else {
906 return Err("unterminated command substitution: missing `)`".to_string());
907 };
908 let (cmd_content, consume_bytes) =
909 (remainder[..rparen_span.start].to_string(), rparen_span.end);
910 let mut consumed = 0usize;
911 while consumed < consume_bytes {
912 match chars.next() {
913 Some(c) => consumed += c.len_utf8(),
914 None => break,
915 }
916 }
917
918 match parse(&cmd_content) {
921 Ok(program) => {
922 let stmts = strip_empty_stmts(program.statements);
923 if stmts.is_empty() {
924 current_text.push_str("$(");
927 current_text.push_str(&cmd_content);
928 current_text.push(')');
929 } else {
930 parts.push(StringPart::CommandSubst(stmts));
931 }
932 }
933 Err(_) => {
934 return Err(format!(
938 "syntax error in command substitution: $({cmd_content})"
939 ));
940 }
941 }
942 } else if chars.peek() == Some(&'{') {
943 if !current_text.is_empty() {
945 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
946 }
947
948 chars.next();
950
951 let mut var_content = String::new();
953 let mut depth = 1;
954 for c in chars.by_ref() {
955 if c == '{' && var_content.ends_with('$') {
956 depth += 1;
957 var_content.push(c);
958 } else if c == '}' {
959 depth -= 1;
960 if depth == 0 {
961 break;
962 }
963 var_content.push(c);
964 } else {
965 var_content.push(c);
966 }
967 }
968
969 let part = if let Some(name) = var_content.strip_prefix('#') {
971 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
973 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
974 let expr = var_content
976 .strip_prefix("__ARITH:")
977 .and_then(|s| s.strip_suffix("__"))
978 .unwrap_or("");
979 StringPart::Arithmetic(expr.to_string())
980 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
981 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
983 let default_str = &var_content[colon_idx + 2..];
984 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
985 StringPart::VarWithDefault { path, default }
986 } else if let Some(msg) = bash_substring_hint(&var_content) {
987 return Err(msg);
988 } else {
989 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
991 };
992 parts.push(part);
993 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
994 if !current_text.is_empty() {
996 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
997 }
998 if let Some(digit) = chars.next() {
999 let n = digit.to_digit(10).unwrap_or(0) as usize;
1000 parts.push(StringPart::Positional(n));
1001 }
1002 } else if chars.peek() == Some(&'@') {
1003 if !current_text.is_empty() {
1005 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1006 }
1007 chars.next(); parts.push(StringPart::AllArgs);
1009 } else if chars.peek() == Some(&'#') {
1010 if !current_text.is_empty() {
1012 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1013 }
1014 chars.next(); parts.push(StringPart::ArgCount);
1016 } else if chars.peek() == Some(&'?') {
1017 if !current_text.is_empty() {
1019 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1020 }
1021 chars.next(); parts.push(StringPart::LastExitCode);
1023 } else if chars.peek() == Some(&'$') {
1024 if !current_text.is_empty() {
1026 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1027 }
1028 chars.next(); parts.push(StringPart::CurrentPid);
1030 } else if chars.peek().copied().map(is_name_start).unwrap_or(false) {
1031 if !current_text.is_empty() {
1033 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1034 }
1035
1036 let mut var_name = String::new();
1038 while let Some(&c) = chars.peek() {
1039 if is_name_char(c) {
1040 if let Some(c) = chars.next() {
1041 var_name.push(c);
1042 }
1043 } else {
1044 break;
1045 }
1046 }
1047
1048 parts.push(StringPart::Var(VarPath::simple(var_name)));
1049 } else {
1050 current_text.push(ch);
1052 }
1053 } else {
1054 current_text.push(ch);
1055 }
1056 }
1057
1058 if !current_text.is_empty() {
1059 parts.push(StringPart::Literal(current_text));
1060 }
1061
1062 Ok(parts)
1063}
1064
1065#[derive(Debug, Clone)]
1067pub struct ParseError {
1068 pub span: Span,
1069 pub message: String,
1070}
1071
1072impl ParseError {
1073 pub fn format(&self, source: &str) -> String {
1078 let start = self.span.start;
1079 let mut line = 1usize;
1080 let mut col = 1usize;
1081 for (i, ch) in source.char_indices() {
1082 if i >= start {
1083 break;
1084 }
1085 if ch == '\n' {
1086 line += 1;
1087 col = 1;
1088 } else {
1089 col += 1;
1090 }
1091 }
1092 let line_content = {
1093 let line_start = source[..start.min(source.len())]
1094 .rfind('\n')
1095 .map_or(0, |i| i + 1);
1096 let line_end = source[start.min(source.len())..]
1097 .find('\n')
1098 .map_or(source.len(), |i| start + i);
1099 source.get(line_start..line_end).unwrap_or("")
1100 };
1101 if line_content.is_empty() {
1102 format!("{}:{} [parse]: {}", line, col, self.message)
1103 } else {
1104 format!(
1105 "{}:{} [parse]: {}\n | {}",
1106 line, col, self.message, line_content
1107 )
1108 }
1109 }
1110}
1111
1112impl std::fmt::Display for ParseError {
1113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1114 write!(f, "{} at {:?}", self.message, self.span)
1115 }
1116}
1117
1118impl std::error::Error for ParseError {}
1119
1120pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
1122 let tokens = lexer::tokenize(source).map_err(|errs| {
1124 errs.into_iter()
1125 .map(|e| ParseError {
1126 span: (e.span.start..e.span.end).into(),
1127 message: format!("lexer error: {}", e.token),
1128 })
1129 .collect::<Vec<_>>()
1130 })?;
1131
1132 let tokens: Vec<(Token, Span)> = tokens
1134 .into_iter()
1135 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
1136 .collect();
1137
1138 for (i, (tok, span)) in tokens.iter().enumerate() {
1149 let prev = i.checked_sub(1).and_then(|j| tokens.get(j)).map(|(t, _)| t);
1150 if let Some((name, is_target)) =
1151 name_in_token_kind(tok, prev, tokens.get(i + 1).map(|(t, _)| t))
1152 {
1153 if let Err(bad) = crate::name::validate(name) {
1154 let defer = is_target && matches!(bad.ch, '.' | '#');
1163 if !defer {
1164 return Err(vec![ParseError { span: *span, message: bad.to_string() }]);
1165 }
1166 }
1167 }
1168 if let Token::String(s) = tok {
1174 if !s.is_ascii() && s.contains('$') {
1175 if let Ok(parts) = parse_interpolated_string(s) {
1176 if let Some(bad) = bad_name_in_parts(&parts) {
1177 return Err(vec![ParseError { span: *span, message: bad.to_string() }]);
1178 }
1179 }
1180 }
1181 }
1182 let message = match tok {
1183 Token::VarRef(raw) => raw
1184 .strip_prefix("${")
1185 .and_then(|s| s.strip_suffix('}'))
1186 .filter(|_| find_default_separator(raw).is_none())
1187 .and_then(bash_substring_hint),
1188 Token::String(s) if s.contains("${") && s.contains(':') => {
1192 parse_interpolated_string(s).err()
1193 }
1194 _ => None,
1195 };
1196 if let Some(message) = message {
1197 return Err(vec![ParseError {
1198 span: *span,
1199 message,
1200 }]);
1201 }
1202 }
1203
1204 let end_span: Span = (source.len()..source.len()).into();
1206
1207 parse_tokens(tokens, end_span, (0..0).into())
1208}
1209
1210fn parse_tokens(
1223 tokens: Vec<(Token, Span)>,
1224 end_span: Span,
1225 stdin_anchor: Span,
1226) -> Result<Program, Vec<ParseError>> {
1227 let input = Stream::from_iter(tokens.clone()).map(end_span, keep_pair as PairFn);
1240 let result = CACHED_PARSER.with(|parser| parser.parse(input));
1241
1242 let program = result.into_result().map_err(|errs| {
1243 if let Err(specific) = validate_cmd_subst_bodies(&tokens) {
1251 return specific;
1252 }
1253 if let Err(specific) = validate_interpolated_strings(&tokens) {
1257 return specific;
1258 }
1259 if let Err(specific) = validate_heredoc_bodies(&tokens) {
1261 return specific;
1262 }
1263 errs.into_iter()
1264 .map(|e| ParseError {
1265 span: *e.span(),
1266 message: e.to_string(),
1267 })
1268 .collect::<Vec<_>>()
1269 })?;
1270
1271 if first_ambiguous_stdin(&program.statements) {
1276 return Err(vec![ParseError {
1277 span: stdin_anchor,
1281 message: "multiple stdin redirects on one command are ambiguous; \
1282 use exactly one of `<`, `<<`, or `<<<`"
1283 .to_string(),
1284 }]);
1285 }
1286
1287 Ok(program)
1288}
1289
1290pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
1292 let program = parse(source)?;
1293 program
1294 .statements
1295 .into_iter()
1296 .find(|s| !matches!(s, Stmt::Empty))
1297 .ok_or_else(|| {
1298 vec![ParseError {
1299 span: (0..source.len()).into(),
1300 message: "empty input".to_string(),
1301 }]
1302 })
1303}
1304
1305fn program_parser<'tokens, 'src: 'tokens, I>(
1311) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
1312where
1313 I: ValueInput<'tokens, Token = Token, Span = Span>,
1314{
1315 statement_parser()
1316 .repeated()
1317 .collect::<Vec<_>>()
1318 .map(|statements| Program { statements })
1319}
1320
1321fn statement_parser<'tokens, I>(
1324) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1325where
1326 I: ValueInput<'tokens, Token = Token, Span = Span>,
1327{
1328 recursive(|stmt| {
1329 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
1330
1331 let break_stmt = just(Token::Break)
1333 .ignore_then(
1334 select! { Token::Int(n) => n as usize }.or_not()
1335 )
1336 .map(Stmt::Break);
1337
1338 let continue_stmt = just(Token::Continue)
1340 .ignore_then(
1341 select! { Token::Int(n) => n as usize }.or_not()
1342 )
1343 .map(Stmt::Continue);
1344
1345 let return_stmt = just(Token::Return)
1347 .ignore_then(primary_expr_parser().or_not())
1348 .map(|e| Stmt::Return(e.map(Box::new)));
1349
1350 let exit_stmt = just(Token::Exit)
1352 .ignore_then(primary_expr_parser().or_not())
1353 .map(|e| Stmt::Exit(e.map(Box::new)));
1354
1355 let set_flag_arg = choice((
1364 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
1365 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
1366 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
1368 ));
1369
1370 let option_value_str = select! {
1374 Token::NumberIdent(s) => s,
1375 Token::Int(n) => n.to_string(),
1376 Token::Ident(s) => s,
1377 };
1378
1379 let set_option_assign = ident_parser()
1383 .then_ignore(just(Token::Eq))
1384 .then(option_value_str)
1385 .map(|(name, value)| {
1386 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
1387 });
1388
1389 let set_quoted_arg = select! {
1393 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1394 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1395 };
1396
1397 let set_with_flags = just(Token::Set)
1399 .then(set_flag_arg)
1400 .then(
1401 choice((
1402 set_flag_arg,
1403 set_option_assign,
1405 set_quoted_arg,
1406 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
1408 ))
1409 .repeated()
1410 .collect::<Vec<_>>(),
1411 )
1412 .map(|((_, first_arg), mut rest_args)| {
1413 let mut args = vec![first_arg];
1414 args.append(&mut rest_args);
1415 Stmt::Command(Command {
1416 name: "set".to_string(),
1417 args,
1418 redirects: vec![],
1419 })
1420 });
1421
1422 let set_no_args = just(Token::Set)
1425 .then(
1426 choice((
1427 just(Token::Newline).to(()),
1428 just(Token::Semi).to(()),
1429 just(Token::And).to(()),
1430 just(Token::Or).to(()),
1431 end(),
1432 ))
1433 .rewind(),
1434 )
1435 .map(|_| Stmt::Command(Command {
1436 name: "set".to_string(),
1437 args: vec![],
1438 redirects: vec![],
1439 }));
1440
1441 let set_command = set_with_flags.or(set_no_args);
1445
1446 let env_prefix_assign = ident_parser()
1463 .then_ignore(just(Token::Eq))
1464 .then(value_expr_parser())
1465 .map(|(name, value)| Assignment { path: VarPath::simple(name), value, local: false });
1466 let env_scoped = env_prefix_assign
1467 .repeated()
1468 .at_least(1)
1469 .collect::<Vec<_>>()
1470 .then(pipeline_parser(command_stage_parser()).map(pipeline_into_stmt))
1471 .map(|(assignments, body)| Stmt::EnvScoped {
1472 assignments,
1473 body: Box::new(body),
1474 });
1475
1476 let compound = choice((
1484 if_parser(stmt.clone()).map(Stmt::If),
1485 for_parser(stmt.clone()).map(Stmt::For),
1486 while_parser(stmt.clone()).map(Stmt::While),
1487 case_parser(stmt.clone()).map(Stmt::Case),
1488 ))
1489 .boxed();
1490
1491 let base_statement = choice((
1493 just(Token::Newline).to(Stmt::Empty),
1494 set_command,
1495 env_scoped,
1496 assignment_parser().map(Stmt::Assignment),
1497 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), break_stmt,
1501 continue_stmt,
1502 return_stmt,
1503 exit_stmt,
1504 test_expr_stmt_parser().map(Stmt::Test),
1505 pipeline_parser(choice((
1507 compound.map(|s| PipelineStage::Compound(Box::new(s))),
1508 command_stage_parser(),
1509 )))
1510 .map(pipeline_into_stmt),
1511 ))
1512 .boxed();
1513
1514 base_statement
1520 .clone()
1521 .foldl(
1522 choice((
1523 just(Token::And).to(true), just(Token::Or).to(false), ))
1526 .then(base_statement)
1527 .repeated(),
1528 |left, (is_and, right): (bool, Stmt)| {
1529 if is_and {
1530 Stmt::AndChain {
1531 left: Box::new(left),
1532 right: Box::new(right),
1533 }
1534 } else {
1535 Stmt::OrChain {
1536 left: Box::new(left),
1537 right: Box::new(right),
1538 }
1539 }
1540 },
1541 )
1542 .then_ignore(terminator)
1543 })
1544}
1545
1546fn lvalue_subscript_parser<'tokens, I>(
1556) -> impl Parser<'tokens, I, VarSegment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1557where
1558 I: ValueInput<'tokens, Token = Token, Span = Span>,
1559{
1560 let interior = choice((
1561 select! { Token::SimpleVarRef(name) => VarSegment::Dynamic(name) },
1562 select! { Token::String(s) => VarSegment::Key(s) },
1563 select! { Token::SingleString(s) => VarSegment::Key(s) },
1564 select! { Token::Int(n) => VarSegment::Index(n) },
1565 select! { Token::Ident(s) => parse_subscript(&s) },
1566 ));
1567
1568 just(Token::LBracket)
1569 .ignore_then(interior)
1570 .then_ignore(just(Token::RBracket))
1571 .labelled("subscript")
1572}
1573
1574fn lvalue_path_parser<'tokens, I>(
1580) -> impl Parser<'tokens, I, VarPath, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1581where
1582 I: ValueInput<'tokens, Token = Token, Span = Span>,
1583{
1584 ident_parser()
1585 .then(lvalue_subscript_parser().repeated().collect::<Vec<_>>())
1586 .map(|(name, subscripts)| {
1587 let mut segments = vec![VarSegment::Field(crate::ast::normalize_name(name))];
1590 segments.extend(subscripts);
1591 VarPath { segments }
1592 })
1593 .labelled("lvalue path")
1594}
1595
1596fn assignment_parser<'tokens, I>(
1602) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1603where
1604 I: ValueInput<'tokens, Token = Token, Span = Span>,
1605{
1606 let local_assignment = just(Token::Local)
1608 .ignore_then(lvalue_path_parser())
1609 .then_ignore(just(Token::Eq))
1610 .then(value_expr_parser())
1611 .map(|(path, value)| Assignment {
1612 path,
1613 value,
1614 local: true,
1615 });
1616
1617 let bash_assignment = lvalue_path_parser()
1620 .then_ignore(just(Token::Eq))
1621 .then(value_expr_parser())
1622 .map(|(path, value)| Assignment {
1623 path,
1624 value,
1625 local: false,
1626 });
1627
1628 choice((local_assignment, bash_assignment))
1629 .labelled("assignment")
1630 .boxed()
1631}
1632
1633fn posix_function_parser<'tokens, I, S>(
1637 stmt: S,
1638) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1639where
1640 I: ValueInput<'tokens, Token = Token, Span = Span>,
1641 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1642{
1643 ident_parser()
1644 .then_ignore(just(Token::LParen))
1645 .then_ignore(just(Token::RParen))
1646 .then_ignore(just(Token::LBrace))
1647 .then_ignore(just(Token::Newline).repeated())
1648 .then(
1649 stmt.repeated()
1650 .collect::<Vec<_>>()
1651 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1652 )
1653 .then_ignore(just(Token::Newline).repeated())
1654 .then_ignore(just(Token::RBrace))
1655 .map(|(name, body)| ToolDef { name, params: vec![], body })
1656 .labelled("POSIX function")
1657 .boxed()
1658}
1659
1660fn bash_function_parser<'tokens, I, S>(
1664 stmt: S,
1665) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1666where
1667 I: ValueInput<'tokens, Token = Token, Span = Span>,
1668 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1669{
1670 just(Token::Function)
1671 .ignore_then(ident_parser())
1672 .then_ignore(just(Token::LBrace))
1673 .then_ignore(just(Token::Newline).repeated())
1674 .then(
1675 stmt.repeated()
1676 .collect::<Vec<_>>()
1677 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1678 )
1679 .then_ignore(just(Token::Newline).repeated())
1680 .then_ignore(just(Token::RBrace))
1681 .map(|(name, body)| ToolDef { name, params: vec![], body })
1682 .labelled("bash function")
1683 .boxed()
1684}
1685
1686fn if_parser<'tokens, I, S>(
1693 stmt: S,
1694) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1695where
1696 I: ValueInput<'tokens, Token = Token, Span = Span>,
1697 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1698{
1699 let branch = condition_parser()
1701 .then_ignore(just(Token::Semi).or_not())
1702 .then_ignore(just(Token::Newline).repeated())
1703 .then_ignore(just(Token::Then))
1704 .then_ignore(just(Token::Newline).repeated())
1705 .then(
1706 stmt.clone()
1707 .repeated()
1708 .collect::<Vec<_>>()
1709 .map(|stmts: Vec<Stmt>| {
1710 stmts
1711 .into_iter()
1712 .filter(|s| !matches!(s, Stmt::Empty))
1713 .collect::<Vec<_>>()
1714 }),
1715 );
1716
1717 let elif_branch = just(Token::Elif)
1719 .ignore_then(condition_parser())
1720 .then_ignore(just(Token::Semi).or_not())
1721 .then_ignore(just(Token::Newline).repeated())
1722 .then_ignore(just(Token::Then))
1723 .then_ignore(just(Token::Newline).repeated())
1724 .then(
1725 stmt.clone()
1726 .repeated()
1727 .collect::<Vec<_>>()
1728 .map(|stmts: Vec<Stmt>| {
1729 stmts
1730 .into_iter()
1731 .filter(|s| !matches!(s, Stmt::Empty))
1732 .collect::<Vec<_>>()
1733 }),
1734 );
1735
1736 let else_branch = just(Token::Else)
1738 .ignore_then(just(Token::Newline).repeated())
1739 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1740 .map(|stmts: Vec<Stmt>| {
1741 stmts
1742 .into_iter()
1743 .filter(|s| !matches!(s, Stmt::Empty))
1744 .collect::<Vec<_>>()
1745 });
1746
1747 just(Token::If)
1748 .ignore_then(branch)
1749 .then(elif_branch.repeated().collect::<Vec<_>>())
1750 .then(else_branch.or_not())
1751 .then_ignore(just(Token::Fi))
1752 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1753 build_if_chain(condition, then_branch, elif_branches, else_branch)
1755 })
1756 .labelled("if statement")
1757 .boxed()
1758}
1759
1760fn build_if_chain(
1767 condition: Expr,
1768 then_branch: Vec<Stmt>,
1769 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1770 else_branch: Option<Vec<Stmt>>,
1771) -> IfStmt {
1772 if elif_branches.is_empty() {
1773 IfStmt {
1775 condition: Box::new(condition),
1776 then_branch,
1777 else_branch,
1778 }
1779 } else {
1780 let (elif_cond, elif_then) = elif_branches.remove(0);
1782 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1783 IfStmt {
1784 condition: Box::new(condition),
1785 then_branch,
1786 else_branch: Some(vec![Stmt::If(nested_if)]),
1787 }
1788 }
1789}
1790
1791fn for_parser<'tokens, I, S>(
1793 stmt: S,
1794) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1795where
1796 I: ValueInput<'tokens, Token = Token, Span = Span>,
1797 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1798{
1799 just(Token::For)
1800 .ignore_then(ident_parser())
1801 .then_ignore(just(Token::In))
1802 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1803 .then_ignore(just(Token::Semi).or_not())
1804 .then_ignore(just(Token::Newline).repeated())
1805 .then_ignore(just(Token::Do))
1806 .then_ignore(just(Token::Newline).repeated())
1807 .then(
1808 stmt.repeated()
1809 .collect::<Vec<_>>()
1810 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1811 )
1812 .then_ignore(just(Token::Done))
1813 .map(|((variable, items), body)| ForLoop {
1814 variable,
1815 items,
1816 body,
1817 })
1818 .labelled("for loop")
1819 .boxed()
1820}
1821
1822fn while_parser<'tokens, I, S>(
1824 stmt: S,
1825) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1826where
1827 I: ValueInput<'tokens, Token = Token, Span = Span>,
1828 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1829{
1830 just(Token::While)
1831 .ignore_then(condition_parser())
1832 .then_ignore(just(Token::Semi).or_not())
1833 .then_ignore(just(Token::Newline).repeated())
1834 .then_ignore(just(Token::Do))
1835 .then_ignore(just(Token::Newline).repeated())
1836 .then(
1837 stmt.repeated()
1838 .collect::<Vec<_>>()
1839 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1840 )
1841 .then_ignore(just(Token::Done))
1842 .map(|(condition, body)| WhileLoop {
1843 condition: Box::new(condition),
1844 body,
1845 })
1846 .labelled("while loop")
1847 .boxed()
1848}
1849
1850fn case_parser<'tokens, I, S>(
1857 stmt: S,
1858) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1859where
1860 I: ValueInput<'tokens, Token = Token, Span = Span>,
1861 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1862{
1863 let pattern_part = choice((
1866 select! { Token::GlobWord(s) => s },
1867 select! { Token::Ident(s) => s },
1868 select! { Token::NumberIdent(s) => s },
1869 select! { Token::DashNumWord(s) => s },
1870 select! { Token::AtWord(s) => s },
1871 select! { Token::DottedIdent(s) => s },
1872 select! { Token::String(s) => s },
1873 select! { Token::SingleString(s) => s },
1874 select! { Token::Int(n) => n.to_string() },
1875 select! { Token::Star => "*".to_string() },
1876 select! { Token::Question => "?".to_string() },
1877 select! { Token::Dot => ".".to_string() },
1878 select! { Token::DotDot => "..".to_string() },
1879 select! { Token::Tilde => "~".to_string() },
1880 select! { Token::TildePath(s) => s },
1881 select! { Token::RelativePath(s) => s },
1882 select! { Token::DotSlashPath(s) => s },
1883 select! { Token::Path(p) => p },
1884 select! { Token::VarRef(v) => v },
1885 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1886 choice((
1894 select! { Token::DoubleDashBare(s) => s },
1895 select! { Token::PlusBare(s) => s },
1896 select! { Token::MinusBare(s) => s },
1897 select! { Token::MinusAlone => "-".to_string() },
1898 select! { Token::DoubleDash => "--".to_string() },
1899 select! { Token::ShortFlag(s) => format!("-{}", s) },
1900 select! { Token::LongFlag(s) => format!("--{}", s) },
1901 select! { Token::PlusFlag(s) => format!("+{}", s) },
1902 )),
1903 just(Token::LBracket)
1905 .ignore_then(
1906 choice((
1907 select! { Token::Ident(s) => s },
1908 select! { Token::Int(n) => n.to_string() },
1909 just(Token::Colon).to(":".to_string()),
1910 just(Token::Bang).to("!".to_string()),
1912 select! { Token::ShortFlag(s) => format!("-{}", s) },
1914 ))
1915 .repeated()
1916 .at_least(1)
1917 .collect::<Vec<String>>()
1918 )
1919 .then_ignore(just(Token::RBracket))
1920 .map(|parts| format!("[{}]", parts.join(""))),
1921 just(Token::LBrace)
1923 .ignore_then(
1924 choice((
1925 select! { Token::Ident(s) => s },
1926 select! { Token::Int(n) => n.to_string() },
1927 ))
1928 .separated_by(just(Token::Comma))
1929 .at_least(1)
1930 .collect::<Vec<String>>()
1931 )
1932 .then_ignore(just(Token::RBrace))
1933 .map(|parts| format!("{{{}}}", parts.join(","))),
1934 ));
1935
1936 let pattern = pattern_part
1939 .repeated()
1940 .at_least(1)
1941 .collect::<Vec<String>>()
1942 .map(|parts| parts.join(""))
1943 .labelled("case pattern");
1944
1945 let patterns = pattern
1947 .separated_by(just(Token::Pipe))
1948 .at_least(1)
1949 .collect::<Vec<String>>()
1950 .labelled("case patterns");
1951
1952 let branch = just(Token::LParen)
1954 .or_not()
1955 .ignore_then(just(Token::Newline).repeated())
1956 .ignore_then(patterns)
1957 .then_ignore(just(Token::RParen))
1958 .then_ignore(just(Token::Newline).repeated())
1959 .then(
1960 stmt.clone()
1961 .repeated()
1962 .collect::<Vec<_>>()
1963 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1964 )
1965 .then_ignore(just(Token::DoubleSemi))
1966 .then_ignore(just(Token::Newline).repeated())
1967 .map(|(patterns, body)| CaseBranch { patterns, body })
1968 .labelled("case branch");
1969
1970 just(Token::Case)
1971 .ignore_then(expr_parser())
1972 .then_ignore(just(Token::In))
1973 .then_ignore(just(Token::Newline).repeated())
1974 .then(branch.repeated().collect::<Vec<_>>())
1975 .then_ignore(just(Token::Esac))
1976 .map(|(expr, branches)| CaseStmt { expr, branches })
1977 .labelled("case statement")
1978 .boxed()
1979}
1980
1981fn pipeline_parser<'tokens, I, S>(
1990 stage: S,
1991) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1992where
1993 I: ValueInput<'tokens, Token = Token, Span = Span>,
1994 S: Parser<'tokens, I, PipelineStage, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1995{
1996 stage
1997 .separated_by(just(Token::Pipe))
1998 .at_least(1)
1999 .collect::<Vec<_>>()
2000 .then(just(Token::Amp).or_not())
2001 .map(|(stages, bg)| Pipeline {
2002 stages,
2003 background: bg.is_some(),
2004 })
2005 .labelled("pipeline")
2006 .boxed()
2007}
2008
2009fn command_stage_parser<'tokens, I>(
2011) -> impl Parser<'tokens, I, PipelineStage, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2012where
2013 I: ValueInput<'tokens, Token = Token, Span = Span>,
2014{
2015 command_parser().map(PipelineStage::Command)
2016}
2017
2018fn command_parser<'tokens, I>(
2022) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2023where
2024 I: ValueInput<'tokens, Token = Token, Span = Span>,
2025{
2026 let command_name = choice((
2032 ident_parser(),
2033 path_parser(),
2034 select! { Token::DotSlashPath(s) => s },
2035 just(Token::True).to("true".to_string()),
2036 just(Token::False).to("false".to_string()),
2037 just(Token::Colon).to(":".to_string()),
2038 just(Token::Dot).to(".".to_string()),
2039 ));
2040
2041 command_name
2051 .then(args_list_parser())
2052 .then(redirect_parser(primary_expr_parser()).repeated().collect::<Vec<_>>())
2053 .map(|((name, args), redirects)| Command {
2054 name,
2055 args,
2056 redirects,
2057 })
2058 .labelled("command")
2059 .boxed()
2060}
2061
2062fn pipeline_into_stmt(p: Pipeline) -> Stmt {
2067 if p.stages.len() == 1 && !p.background && p.stages[0].redirects().is_empty() {
2068 match p.stages.into_iter().next() {
2069 Some(PipelineStage::Compound(stmt)) => *stmt,
2073 Some(PipelineStage::Command(cmd)) => Stmt::Command(cmd),
2074 None => Stmt::Empty, }
2076 } else {
2077 Stmt::Pipeline(p)
2078 }
2079}
2080
2081fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
2085 cmd.redirects
2086 .iter()
2087 .filter(|r| {
2088 matches!(
2089 r.kind,
2090 RedirectKind::Stdin | RedirectKind::HereDoc(_) | RedirectKind::HereString
2091 )
2092 })
2093 .count()
2094 > 1
2095}
2096
2097fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
2101 stmts.iter().any(stmt_has_ambiguous_stdin)
2102}
2103
2104fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
2105 match stmt {
2106 Stmt::Command(c) => command_has_ambiguous_stdin(c),
2107 Stmt::Pipeline(p) => p.stages.iter().any(|stage| match stage {
2108 PipelineStage::Command(cmd) => command_has_ambiguous_stdin(cmd),
2109 PipelineStage::Compound(inner) => stmt_has_ambiguous_stdin(inner),
2110 }),
2111 Stmt::If(i) => {
2112 first_ambiguous_stdin(&i.then_branch)
2113 || i.else_branch
2114 .as_deref()
2115 .is_some_and(first_ambiguous_stdin)
2116 }
2117 Stmt::For(f) => first_ambiguous_stdin(&f.body),
2118 Stmt::While(w) => first_ambiguous_stdin(&w.body),
2119 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
2120 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
2121 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
2122 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
2123 }
2124 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
2125 Stmt::Assignment(_)
2126 | Stmt::Break(_)
2127 | Stmt::Continue(_)
2128 | Stmt::Return(_)
2129 | Stmt::Exit(_)
2130 | Stmt::Test(_)
2131 | Stmt::Empty => false,
2132 }
2133}
2134
2135fn is_glue_candidate(arg: &Arg) -> bool {
2156 matches!(arg, Arg::Positional(_) | Arg::LongFlag(_))
2157}
2158
2159fn reject_glued_args<'src>(
2177 args: Vec<(Arg, Span)>,
2178) -> Result<Vec<Arg>, Rich<'src, Token, Span>> {
2179 for pair in args.windows(2) {
2180 let (prev, prev_span) = &pair[0];
2181 let (next, next_span) = &pair[1];
2182 if is_glue_candidate(prev) && is_glue_candidate(next) && prev_span.end == next_span.start {
2183 let msg = "adjacent words with no space between them are not joined into one \
2184 argument (kaish does no token pasting); quote the whole word, e.g. \
2185 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\"";
2186 return Err(Rich::custom(*next_span, msg));
2187 }
2188 }
2189 Ok(args.into_iter().map(|(arg, _)| arg).collect())
2190}
2191
2192fn args_list_parser<'tokens, I>(
2196) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2197where
2198 I: ValueInput<'tokens, Token = Token, Span = Span>,
2199{
2200 let pre_dash = arg_before_double_dash_parser()
2208 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
2209 .repeated()
2210 .collect::<Vec<(Arg, Span)>>()
2211 .try_map(|args, _span| reject_glued_args(args));
2212
2213 let double_dash = select! {
2215 Token::DoubleDash => Arg::DoubleDash,
2216 };
2217
2218 let post_dash_arg = choice((
2220 select! {
2222 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
2223 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
2224 },
2225 word_assign_arg_parser(),
2232 test_operator_arg_parser(),
2234 primary_expr_parser().map(Arg::Positional),
2236 ));
2237
2238 let post_dash = post_dash_arg
2242 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
2243 .repeated()
2244 .collect::<Vec<(Arg, Span)>>()
2245 .try_map(|args, _span| reject_glued_args(args));
2246
2247 pre_dash
2249 .then(double_dash.then(post_dash).or_not())
2250 .map(|(mut args, maybe_dd)| {
2251 if let Some((dd, post)) = maybe_dd {
2252 args.push(dd);
2253 args.extend(post);
2254 }
2255 args
2256 })
2257}
2258
2259fn keyword_word<'tokens, I>(
2269) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2270where
2271 I: ValueInput<'tokens, Token = Token, Span = Span>,
2272{
2273 select! {
2274 Token::Set => "set",
2275 Token::Local => "local",
2276 Token::If => "if",
2277 Token::Then => "then",
2278 Token::Else => "else",
2279 Token::Elif => "elif",
2280 Token::Fi => "fi",
2281 Token::For => "for",
2282 Token::While => "while",
2283 Token::In => "in",
2284 Token::Do => "do",
2285 Token::Done => "done",
2286 Token::Case => "case",
2287 Token::Esac => "esac",
2288 Token::Function => "function",
2289 Token::Break => "break",
2290 Token::Continue => "continue",
2291 Token::Return => "return",
2292 Token::Exit => "exit",
2293 }
2294 .map(|s| s.to_string())
2295}
2296
2297fn word_assign_arg_parser<'tokens, I>(
2307) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2308where
2309 I: ValueInput<'tokens, Token = Token, Span = Span>,
2310{
2311 choice((
2312 select! { Token::Ident(s) => s },
2313 keyword_word(),
2314 ))
2315 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
2316 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
2317 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
2318 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
2319 if key_span.end != eq_span.start || eq_span.end != value_span.start {
2321 Err(Rich::custom(
2322 span,
2323 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
2324 ))
2325 } else {
2326 Ok(Arg::WordAssign { key, value })
2327 }
2328 })
2329}
2330
2331fn test_operator_arg_parser<'tokens, I>(
2348) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2349where
2350 I: ValueInput<'tokens, Token = Token, Span = Span>,
2351{
2352 select! {
2353 Token::Eq => "=",
2354 Token::EqEq => "==",
2355 Token::NotEq => "!=",
2356 Token::Bang => "!",
2357 }
2358 .map(|s| Arg::Positional(Expr::Literal(Value::String(s.to_string()))))
2359}
2360
2361fn arg_before_double_dash_parser<'tokens, I>(
2363) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2364where
2365 I: ValueInput<'tokens, Token = Token, Span = Span>,
2366{
2367 let long_flag_with_value = select! {
2369 Token::LongFlag(name) => name,
2370 }
2371 .then_ignore(just(Token::Eq))
2372 .then(primary_expr_parser())
2373 .map(|(key, value)| Arg::Named { key, value });
2374
2375 let long_flag = select! {
2377 Token::LongFlag(name) => Arg::LongFlag(name),
2378 };
2379
2380 let short_flag = select! {
2382 Token::ShortFlag(name) => Arg::ShortFlag(name),
2383 };
2384
2385 let named = word_assign_arg_parser();
2387
2388 let positional = primary_expr_parser().map(Arg::Positional);
2390
2391 let test_operator = test_operator_arg_parser();
2395
2396 choice((
2399 long_flag_with_value,
2400 long_flag,
2401 short_flag,
2402 named,
2403 test_operator,
2404 positional,
2405 ))
2406 .boxed()
2407}
2408
2409fn redirect_parser<'tokens, I, T>(
2423 target: T,
2424) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2425where
2426 I: ValueInput<'tokens, Token = Token, Span = Span>,
2427 T: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2428{
2429 let target = target
2442 .clone()
2443 .map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) })
2444 .then(target.clone().map_with(|_, e| e.span()).rewind().or_not())
2445 .try_map(|((expr, span), glued), _| match glued {
2446 Some(next_span) if next_span.start == span.end => Err(Rich::custom(
2447 next_span,
2448 "adjacent words with no space between them are not joined into the redirect \
2449 target (kaish does no token pasting); quote the whole target, e.g. \
2450 \"/tmp/$(echo x).txt\"",
2451 )),
2452 _ => Ok(expr),
2453 })
2454 .boxed();
2455
2456 let regular_redirect = select! {
2458 Token::GtGt => RedirectKind::StdoutAppend,
2459 Token::Gt => RedirectKind::StdoutOverwrite,
2460 Token::Lt => RedirectKind::Stdin,
2461 Token::Stderr => RedirectKind::Stderr,
2462 Token::Both => RedirectKind::Both,
2463 }
2464 .then(target.clone())
2465 .map(|(kind, target)| Redirect { kind, target });
2466
2467 let heredoc_redirect = just(Token::HereDocStart)
2475 .ignore_then(select! { Token::HereDoc(data) => data })
2476 .try_map(|data: HereDocData, span| {
2477 let meta = HereDocMeta {
2480 delimiter: data.delimiter.clone(),
2481 literal: data.literal,
2482 strip_tabs: data.strip_tabs,
2483 body: data.source_body.clone(),
2484 body_offset: data.body_start_offset,
2485 };
2486 let target = if data.literal {
2487 let body = if data.strip_tabs {
2488 crate::interpreter::strip_leading_tabs(&data.content)
2489 } else {
2490 data.content
2491 };
2492 Expr::Literal(Value::String(body))
2493 } else {
2494 let parts =
2495 parse_interpolated_string_spanned(&data.content, data.body_start_offset)
2496 .map_err(|msg| Rich::custom(span, msg))?;
2497 if parts.len() == 1 && !data.strip_tabs {
2501 if let StringPart::Literal(text) = &parts[0].part {
2502 return Ok(Redirect {
2503 kind: RedirectKind::HereDoc(meta),
2504 target: Expr::Literal(Value::String(text.clone())),
2505 });
2506 }
2507 }
2508 Expr::HereDocBody {
2509 parts,
2510 strip_tabs: data.strip_tabs,
2511 }
2512 };
2513 Ok(Redirect {
2514 kind: RedirectKind::HereDoc(meta),
2515 target,
2516 })
2517 });
2518
2519 let herestring_redirect = just(Token::HereString)
2523 .ignore_then(target.clone())
2524 .map(|target| Redirect {
2525 kind: RedirectKind::HereString,
2526 target,
2527 });
2528
2529 let merge_stderr_redirect = just(Token::StderrToStdout)
2531 .map(|_| Redirect {
2532 kind: RedirectKind::MergeStderr,
2533 target: Expr::Literal(Value::Null),
2535 });
2536
2537 let merge_stdout_redirect = choice((
2539 just(Token::StdoutToStderr),
2540 just(Token::StdoutToStderr2),
2541 ))
2542 .map(|_| Redirect {
2543 kind: RedirectKind::MergeStdout,
2544 target: Expr::Literal(Value::Null),
2546 });
2547
2548 choice((
2549 heredoc_redirect,
2550 herestring_redirect,
2551 merge_stderr_redirect,
2552 merge_stdout_redirect,
2553 regular_redirect,
2554 ))
2555 .labelled("redirect")
2556 .boxed()
2557}
2558
2559fn test_expr_stmt_parser<'tokens, I>(
2571) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2572where
2573 I: ValueInput<'tokens, Token = Token, Span = Span>,
2574{
2575 let file_test_op = select! {
2577 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
2578 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
2579 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
2580 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
2581 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
2582 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
2583 };
2584
2585 let string_test_op = select! {
2589 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
2590 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
2591 Token::ShortFlag(s) if s == "list" => StringTestOp::IsList,
2592 Token::ShortFlag(s) if s == "record" => StringTestOp::IsRecord,
2593 };
2594
2595 let cmp_op = choice((
2598 just(Token::EqEq).to(TestCmpOp::Eq),
2599 just(Token::Eq).to(TestCmpOp::Eq),
2600 just(Token::NotEq).to(TestCmpOp::NotEq),
2601 just(Token::Match).to(TestCmpOp::Match),
2602 just(Token::NotMatch).to(TestCmpOp::NotMatch),
2603 just(Token::Gt).to(TestCmpOp::Gt),
2604 just(Token::Lt).to(TestCmpOp::Lt),
2605 just(Token::GtEq).to(TestCmpOp::GtEq),
2606 just(Token::LtEq).to(TestCmpOp::LtEq),
2607 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
2608 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
2609 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
2610 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
2611 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
2612 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
2613 ));
2614
2615 let file_test = file_test_op
2617 .then(primary_expr_parser())
2618 .map(|(op, path)| TestExpr::FileTest {
2619 op,
2620 path: Box::new(path),
2621 });
2622
2623 let string_test = string_test_op
2625 .then(primary_expr_parser())
2626 .map(|(op, value)| TestExpr::StringTest {
2627 op,
2628 value: Box::new(value),
2629 });
2630
2631 let comparison = primary_expr_parser()
2633 .then(cmp_op)
2634 .then(primary_expr_parser())
2635 .map(|((left, op), right)| TestExpr::Comparison {
2636 left: Box::new(left),
2637 op,
2638 right: Box::new(right),
2639 });
2640
2641 let not_in = primary_expr_parser()
2647 .then_ignore(select! { Token::Ident(s) if s == "not" => () })
2648 .then_ignore(just(Token::In))
2649 .then(value_primary_parser())
2650 .map(|(left, right)| TestExpr::NotIn {
2651 left: Box::new(left),
2652 right: Box::new(right),
2653 });
2654
2655 let in_ = primary_expr_parser()
2656 .then_ignore(just(Token::In))
2657 .then(value_primary_parser())
2658 .map(|(left, right)| TestExpr::In {
2659 left: Box::new(left),
2660 right: Box::new(right),
2661 });
2662
2663 let primary_test = choice((file_test, string_test, not_in, in_, comparison));
2665
2666 let unary = recursive(|unary| {
2680 let not_expr = just(Token::Bang)
2681 .ignore_then(unary)
2682 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
2683 choice((not_expr, primary_test.clone()))
2684 });
2685
2686 let and_expr = unary.clone().foldl(
2688 just(Token::And).ignore_then(unary).repeated(),
2689 |left, right| TestExpr::And {
2690 left: Box::new(left),
2691 right: Box::new(right),
2692 },
2693 );
2694
2695 let compound_test = and_expr.clone().foldl(
2697 just(Token::Or).ignore_then(and_expr).repeated(),
2698 |left, right| TestExpr::Or {
2699 left: Box::new(left),
2700 right: Box::new(right),
2701 },
2702 );
2703
2704 just(Token::LBracket)
2707 .then(just(Token::LBracket))
2708 .ignore_then(compound_test)
2709 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
2710 .labelled("test expression")
2711 .boxed()
2712}
2713
2714fn condition_parser<'tokens, I>(
2729) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2730where
2731 I: ValueInput<'tokens, Token = Token, Span = Span>,
2732{
2733 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
2735
2736 let command_condition = command_parser().map(Expr::Command);
2739
2740 let base = choice((test_expr_condition, command_condition));
2742
2743 let and_expr = base.clone().foldl(
2746 just(Token::And).ignore_then(base).repeated(),
2747 |left, right| Expr::BinaryOp {
2748 left: Box::new(left),
2749 op: BinaryOp::And,
2750 right: Box::new(right),
2751 },
2752 );
2753
2754 and_expr
2756 .clone()
2757 .foldl(
2758 just(Token::Or).ignore_then(and_expr).repeated(),
2759 |left, right| Expr::BinaryOp {
2760 left: Box::new(left),
2761 op: BinaryOp::Or,
2762 right: Box::new(right),
2763 },
2764 )
2765 .labelled("condition")
2766 .boxed()
2767}
2768
2769fn expr_parser<'tokens, I>(
2776) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2777where
2778 I: ValueInput<'tokens, Token = Token, Span = Span>,
2779{
2780 primary_expr_parser()
2782}
2783
2784fn value_expr_parser<'tokens, I>(
2789) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2790where
2791 I: ValueInput<'tokens, Token = Token, Span = Span>,
2792{
2793 value_literal_parser()
2794}
2795
2796fn value_primary_parser<'tokens, I>(
2802) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2803where
2804 I: ValueInput<'tokens, Token = Token, Span = Span>,
2805{
2806 value_literal_parser()
2807}
2808
2809fn value_literal_parser<'tokens, I>(
2821) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2822where
2823 I: ValueInput<'tokens, Token = Token, Span = Span>,
2824{
2825 recursive(|value| {
2826 choice((
2827 list_literal_parser(value.clone()),
2828 record_literal_parser(value.clone()),
2829 primary_expr_parser(),
2830 ))
2831 })
2832 .boxed()
2833}
2834
2835fn list_literal_parser<'tokens, I, V>(
2843 value: V,
2844) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2845where
2846 I: ValueInput<'tokens, Token = Token, Span = Span>,
2847 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2848{
2849 let spread_elem = just(Token::DotDotDot)
2850 .ignore_then(value.clone())
2851 .map(ListElem::Spread);
2852 let item_elem = value.map(ListElem::Item);
2853 let elem = choice((spread_elem, item_elem));
2854
2855 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2856
2857 just(Token::LBracket)
2858 .ignore_then(just(Token::Newline).repeated())
2859 .ignore_then(elem.then_ignore(sep).repeated().collect::<Vec<_>>())
2860 .then_ignore(just(Token::RBracket))
2861 .map(Expr::ListLiteral)
2862 .labelled("list literal")
2863}
2864
2865fn record_literal_parser<'tokens, I, V>(
2873 value: V,
2874) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2875where
2876 I: ValueInput<'tokens, Token = Token, Span = Span>,
2877 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2878{
2879 let bare_key = select! { Token::Ident(s) => RecordKey::Bare(s) };
2880 let double_key = select! { Token::String(s) => s }.try_map(|s, span| {
2886 let parts = parse_interpolated_string(&s)
2887 .map_err(|e| Rich::custom(span, format!("record key: {e}")))?;
2888 Ok(match parts.as_slice() {
2889 [] => RecordKey::Quoted(String::new()),
2890 [StringPart::Literal(lit)] => RecordKey::Quoted(lit.clone()),
2891 _ => RecordKey::Interpolated(parts),
2892 })
2893 });
2894 let single_key = select! { Token::SingleString(s) => RecordKey::Quoted(s) };
2895 let key = choice((double_key, single_key, bare_key)).labelled("record key");
2896
2897 let stray_bareword_after_value = select! { Token::Ident(s) => s }
2909 .then(just(Token::Colon).or_not())
2910 .rewind()
2911 .or_not()
2912 .try_map(|maybe, span| match maybe {
2913 Some((word, None)) => Err(Rich::custom(
2914 span,
2915 format!(
2916 "record value: unexpected word \"{word}\" after the value — a multi-word \
2917 value must be quoted, e.g. {{key: \"hello world\"}}"
2918 ),
2919 )),
2920 _ => Ok(()),
2921 });
2922
2923 let entry = key
2924 .then_ignore(just(Token::Colon))
2925 .then(value)
2926 .then_ignore(stray_bareword_after_value)
2927 .map(|(key, value)| RecordEntry { key, value });
2928
2929 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2930
2931 just(Token::LBrace)
2932 .ignore_then(just(Token::Newline).repeated())
2933 .ignore_then(entry.then_ignore(sep).repeated().collect::<Vec<_>>())
2934 .then_ignore(just(Token::RBrace))
2935 .map(Expr::RecordLiteral)
2936 .labelled("record literal")
2937}
2938
2939fn primary_expr_parser<'tokens, I>(
2943) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2944where
2945 I: ValueInput<'tokens, Token = Token, Span = Span>,
2946{
2947 let positional = select! {
2949 Token::Positional(n) => Expr::Positional(n),
2950 Token::AllArgs => Expr::AllArgs,
2951 Token::ArgCount => Expr::ArgCount,
2952 Token::VarLength(name) => Expr::VarLength(parse_varpath(&format!("${{{name}}}"))),
2953 Token::LastExitCode => Expr::LastExitCode,
2954 Token::CurrentPid => Expr::CurrentPid,
2955 };
2956
2957 let arithmetic = select! {
2959 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
2960 };
2961
2962 let keyword_as_bareword = select! {
2965 Token::Done => "done",
2966 Token::Fi => "fi",
2967 Token::Then => "then",
2968 Token::Else => "else",
2969 Token::Elif => "elif",
2970 Token::In => "in",
2971 Token::Do => "do",
2972 Token::Esac => "esac",
2973 Token::Set => "set",
2978 }
2979 .map(|s| Expr::Literal(Value::String(s.to_string())));
2980
2981 let plus_minus_bare = select! {
2985 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
2986 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
2987 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
2988 Token::DoubleDashBare(s) => Expr::Literal(Value::String(s)),
2989 };
2990
2991 let glob_pattern = select! {
2993 Token::GlobWord(s) => Expr::GlobPattern(s),
2994 Token::Star => Expr::GlobPattern("*".to_string()),
2995 Token::Question => Expr::GlobPattern("?".to_string()),
2996 };
2997
2998 choice((
3005 positional,
3006 arithmetic,
3007 cmd_subst_parser(),
3008 var_expr_parser(),
3009 interpolated_string_parser(),
3010 literal_parser().map(Expr::Literal),
3011 glob_pattern,
3013 ident_parser().map(|s| Expr::Literal(Value::String(s))),
3015 path_parser().map(|s| Expr::Literal(Value::String(s))),
3017 select! {
3020 Token::Dot => Expr::Literal(Value::String(".".into())),
3026 Token::DotDot => Expr::Literal(Value::String("..".into())),
3027 Token::Comma => Expr::Literal(Value::String(",".into())),
3039 Token::Colon => Expr::Literal(Value::String(":".into())),
3046 Token::Tilde => Expr::Literal(Value::String("~".into())),
3047 Token::TildePath(s) => Expr::Literal(Value::String(s)),
3048 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
3049 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
3050 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
3052 Token::DashNumWord(s) => Expr::Literal(Value::String(s)),
3055 Token::AtWord(s) => Expr::Literal(Value::String(s)),
3057 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
3062 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
3065 },
3066 plus_minus_bare,
3067 keyword_as_bareword,
3069 ))
3070 .labelled("expression")
3071 .boxed()
3072}
3073
3074fn var_expr_parser<'tokens, I>(
3077) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3078where
3079 I: ValueInput<'tokens, Token = Token, Span = Span>,
3080{
3081 choice((
3082 select! { Token::VarRef(raw) => raw }.try_map(|raw, span| {
3083 let inner = raw
3087 .strip_prefix("${")
3088 .and_then(|s| s.strip_suffix('}'))
3089 .unwrap_or(&raw);
3090 if !raw.starts_with("${?}")
3091 && !raw.starts_with("${$}")
3092 && find_default_separator(&raw).is_none()
3093 && let Some(msg) = bash_substring_hint(inner)
3094 {
3095 return Err(Rich::custom(span, msg));
3096 }
3097 if let Some(colon) = find_default_separator(&raw)
3105 && raw.len() > colon + 3
3106 && let Err(msg) =
3107 parse_interpolated_string(&unquote_default_word(&raw[colon + 2..raw.len() - 1]))
3108 {
3109 return Err(Rich::custom(span, msg));
3110 }
3111 Ok(parse_var_expr(&raw))
3112 }),
3113 select! { Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)) },
3114 ))
3115 .labelled("variable reference")
3116}
3117
3118type CmdSubstBody = (Vec<(Token, Span)>, Span);
3138
3139fn cmd_subst_body_tokens<'tokens, I>(
3140) -> impl Parser<'tokens, I, CmdSubstBody, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3141where
3142 I: ValueInput<'tokens, Token = Token, Span = Span>,
3143{
3144 custom(|inp| {
3145 let mut tracker = CmdSubstFrames::default();
3146 let mut body: Vec<(Token, Span)> = Vec::new();
3147 loop {
3148 let before = inp.cursor();
3149 match inp.next() {
3150 None => {
3151 let span = inp.span_since(&before);
3152 return Err(Rich::custom(
3153 span,
3154 "unterminated command substitution: missing `)`",
3155 ));
3156 }
3157 Some(tok) => {
3158 let span = inp.span_since(&before);
3159 let next = inp.peek();
3163 if tracker.step(&tok, next.as_ref()) {
3164 return Ok((body, span));
3165 }
3166 body.push((tok, span));
3167 }
3168 }
3169 }
3170 })
3171}
3172
3173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3175enum CmdSubstFrame {
3176 Subst,
3178 Paren,
3184 Case { awaiting_pattern: bool },
3193}
3194
3195#[derive(Default)]
3240struct CmdSubstFrames(Vec<CmdSubstFrame>);
3241
3242impl CmdSubstFrames {
3243 fn step(&mut self, tok: &Token, next: Option<&Token>) -> bool {
3250 match tok {
3251 Token::RParen => match self.0.last_mut() {
3252 None => return true,
3253 Some(CmdSubstFrame::Case { awaiting_pattern }) => {
3254 *awaiting_pattern = false;
3255 }
3256 Some(CmdSubstFrame::Paren) => {
3257 self.0.pop();
3258 if let Some(CmdSubstFrame::Case { awaiting_pattern }) = self.0.last_mut() {
3262 *awaiting_pattern = false;
3263 }
3264 }
3265 Some(CmdSubstFrame::Subst) => {
3266 self.0.pop();
3267 }
3268 },
3269 Token::LParen => self.0.push(CmdSubstFrame::Paren),
3270 Token::CmdSubstStart => self.0.push(CmdSubstFrame::Subst),
3271 Token::Case if !matches!(next, Some(Token::Eq)) => {
3278 self.0.push(CmdSubstFrame::Case { awaiting_pattern: true });
3279 }
3280 Token::Case => {}
3281 Token::DoubleSemi => {
3282 if let Some(CmdSubstFrame::Case { awaiting_pattern }) = self.0.last_mut() {
3283 *awaiting_pattern = true;
3284 }
3285 }
3286 Token::Esac
3287 if matches!(
3288 self.0.last(),
3289 Some(CmdSubstFrame::Case { awaiting_pattern: true })
3290 ) =>
3291 {
3292 self.0.pop();
3293 }
3294 _ => {}
3295 }
3296 false
3297 }
3298}
3299
3300fn find_cmd_subst_close(tokens: &[(Token, Span)]) -> Option<usize> {
3308 let mut tracker = CmdSubstFrames::default();
3309 (0..tokens.len()).find(|&i| {
3310 let next = tokens.get(i + 1).map(|(t, _)| t);
3311 tracker.step(&tokens[i].0, next)
3312 })
3313}
3314
3315fn validate_cmd_subst_bodies(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3336 let mut i = 0;
3337 while i < tokens.len() {
3338 if !matches!(tokens[i].0, Token::CmdSubstStart) {
3339 i += 1;
3340 continue;
3341 }
3342 let start_span = tokens[i].1;
3343 let rest = &tokens[i + 1..];
3344 let Some(close_rel) = find_cmd_subst_close(rest) else {
3345 return Err(vec![ParseError {
3346 span: start_span,
3347 message: "unterminated command substitution: missing `)`".to_string(),
3348 }]);
3349 };
3350 let body = &rest[..close_rel];
3351 let rparen_span = rest[close_rel].1;
3352 let end_span: Span = (rparen_span.start..rparen_span.start).into();
3353 parse_tokens(body.to_vec(), end_span, start_span)?;
3357 i += 1 + close_rel + 1;
3358 }
3359 Ok(())
3360}
3361
3362fn validate_interpolated_strings(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3374 for (tok, span) in tokens {
3375 let owned;
3376 let body = match tok {
3377 Token::String(s) => Some(s.as_str()),
3378 Token::VarRef(raw) => match find_default_separator(raw) {
3382 Some(colon) if raw.len() > colon + 3 => {
3383 owned = unquote_default_word(&raw[colon + 2..raw.len() - 1]);
3384 Some(owned.as_str())
3385 }
3386 _ => None,
3387 },
3388 _ => None,
3389 };
3390 if let Some(body) = body
3391 && let Err(message) = parse_interpolated_string(body)
3392 {
3393 return Err(vec![ParseError { span: *span, message }]);
3394 }
3395 }
3396 Ok(())
3397}
3398
3399fn validate_heredoc_bodies(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3409 for (tok, span) in tokens {
3410 if let Token::HereDoc(d) = tok
3411 && !d.literal
3412 && let Err(message) = parse_interpolated_string_spanned(&d.content, 0)
3413 {
3414 return Err(vec![ParseError { span: *span, message }]);
3415 }
3416 }
3417 Ok(())
3418}
3419
3420fn cmd_subst_parser<'tokens, I>(
3443) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3444where
3445 I: ValueInput<'tokens, Token = Token, Span = Span>,
3446{
3447 just(Token::CmdSubstStart)
3448 .ignore_then(cmd_subst_body_tokens())
3449 .try_map(|(body_tokens, rparen_span), outer_span| {
3450 let end_span: Span = (rparen_span.start..rparen_span.start).into();
3451 parse_tokens(body_tokens, end_span, outer_span)
3452 .map(|program| Expr::CommandSubst(program.statements))
3453 .map_err(|errs| {
3454 let first = errs.into_iter().next().unwrap_or_else(|| ParseError {
3455 span: outer_span,
3456 message: "command substitution failed to parse".to_string(),
3457 });
3458 Rich::custom(first.span, first.message)
3459 })
3460 })
3461 .labelled("command substitution")
3462}
3463
3464fn interpolated_string_parser<'tokens, I>(
3466) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3467where
3468 I: ValueInput<'tokens, Token = Token, Span = Span>,
3469{
3470 let double_quoted = select! {
3472 Token::String(s) => s,
3473 }
3474 .try_map(|s, span| {
3475 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
3477 let parts = parse_interpolated_string(&s)
3480 .map_err(|msg| Rich::custom(span, msg))?;
3481 if parts.len() == 1
3482 && let StringPart::Literal(text) = &parts[0] {
3483 return Ok(Expr::Literal(Value::String(text.clone())));
3484 }
3485 Ok(Expr::Interpolated(parts))
3486 } else {
3487 Ok(Expr::Literal(Value::String(s)))
3488 }
3489 });
3490
3491 let single_quoted = select! {
3493 Token::SingleString(s) => Expr::Literal(Value::String(s)),
3494 };
3495
3496 choice((single_quoted, double_quoted)).labelled("string")
3497}
3498
3499fn literal_parser<'tokens, I>(
3501) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3502where
3503 I: ValueInput<'tokens, Token = Token, Span = Span>,
3504{
3505 choice((
3506 select! {
3507 Token::True => Value::Bool(true),
3508 Token::False => Value::Bool(false),
3509 },
3510 select! {
3511 Token::Int(n) => Value::Int(n),
3512 Token::Float(f) => Value::Float(f),
3513 },
3514 ))
3515 .labelled("literal")
3516 .boxed()
3517}
3518
3519fn ident_parser<'tokens, I>(
3521) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3522where
3523 I: ValueInput<'tokens, Token = Token, Span = Span>,
3524{
3525 select! {
3526 Token::Ident(s) => s,
3527 }
3528 .labelled("identifier")
3529}
3530
3531fn path_parser<'tokens, I>(
3533) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3534where
3535 I: ValueInput<'tokens, Token = Token, Span = Span>,
3536{
3537 select! {
3538 Token::Path(s) => s,
3539 }
3540 .labelled("path")
3541}
3542
3543#[cfg(test)]
3544#[allow(clippy::approx_constant)]
3545mod tests {
3546 use super::*;
3547 use proptest::strategy::Strategy;
3548
3549 fn pipeline_commands(p: &Pipeline) -> Vec<&Command> {
3553 p.stages
3554 .iter()
3555 .map(|stage| stage.as_command().expect("expected a command stage"))
3556 .collect()
3557 }
3558
3559 fn subst_cmd(expr: &Expr) -> &Command {
3561 match expr {
3562 Expr::CommandSubst(stmts) => match stmts.as_slice() {
3563 [Stmt::Command(cmd)] => cmd,
3564 other => panic!("expected a single command in $(), got {other:?}"),
3565 },
3566 other => panic!("expected command subst, got {other:?}"),
3567 }
3568 }
3569
3570 fn subst_pipeline(expr: &Expr) -> &Pipeline {
3572 match expr {
3573 Expr::CommandSubst(stmts) => match stmts.as_slice() {
3574 [Stmt::Pipeline(p)] => p,
3575 other => panic!("expected a single pipeline in $(), got {other:?}"),
3576 },
3577 other => panic!("expected command subst, got {other:?}"),
3578 }
3579 }
3580
3581 #[test]
3582 fn parse_empty() {
3583 let result = parse("");
3584 assert!(result.is_ok());
3585 assert_eq!(result.expect("ok").statements.len(), 0);
3586 }
3587
3588 #[test]
3589 fn parse_newlines_only() {
3590 let result = parse("\n\n\n");
3591 assert!(result.is_ok());
3592 }
3593
3594 #[test]
3595 fn parse_simple_command() {
3596 let result = parse("echo");
3597 assert!(result.is_ok());
3598 let program = result.expect("ok");
3599 assert_eq!(program.statements.len(), 1);
3600 assert!(matches!(&program.statements[0], Stmt::Command(_)));
3601 }
3602
3603 #[test]
3604 fn parse_command_with_string_arg() {
3605 let result = parse(r#"echo "hello""#);
3606 assert!(result.is_ok());
3607 let program = result.expect("ok");
3608 match &program.statements[0] {
3609 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
3610 _ => panic!("expected Command"),
3611 }
3612 }
3613
3614 #[test]
3615 fn parse_assignment() {
3616 let result = parse("X=5");
3617 assert!(result.is_ok());
3618 let program = result.expect("ok");
3619 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
3620 }
3621
3622 #[test]
3623 fn parse_pipeline() {
3624 let result = parse("a | b | c");
3625 assert!(result.is_ok());
3626 let program = result.expect("ok");
3627 match &program.statements[0] {
3628 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 3),
3629 _ => panic!("expected Pipeline"),
3630 }
3631 }
3632
3633 #[test]
3634 fn parse_background_job() {
3635 let result = parse("cmd &");
3636 assert!(result.is_ok());
3637 let program = result.expect("ok");
3638 match &program.statements[0] {
3639 Stmt::Pipeline(p) => assert!(p.background),
3640 _ => panic!("expected Pipeline with background"),
3641 }
3642 }
3643
3644 #[test]
3645 fn parse_if_simple() {
3646 let result = parse("if true; then echo; fi");
3647 assert!(result.is_ok());
3648 let program = result.expect("ok");
3649 assert!(matches!(&program.statements[0], Stmt::If(_)));
3650 }
3651
3652 #[test]
3653 fn parse_if_else() {
3654 let result = parse("if true; then echo; else echo; fi");
3655 assert!(result.is_ok());
3656 let program = result.expect("ok");
3657 match &program.statements[0] {
3658 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
3659 _ => panic!("expected If"),
3660 }
3661 }
3662
3663 #[test]
3664 fn parse_elif_simple() {
3665 let result = parse("if true; then echo a; elif false; then echo b; fi");
3666 assert!(result.is_ok(), "parse failed: {:?}", result);
3667 let program = result.expect("ok");
3668 match &program.statements[0] {
3669 Stmt::If(if_stmt) => {
3670 assert!(if_stmt.else_branch.is_some());
3672 let else_branch = if_stmt.else_branch.as_ref().unwrap();
3673 assert_eq!(else_branch.len(), 1);
3674 assert!(matches!(&else_branch[0], Stmt::If(_)));
3675 }
3676 _ => panic!("expected If"),
3677 }
3678 }
3679
3680 #[test]
3681 fn parse_elif_with_else() {
3682 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
3683 assert!(result.is_ok(), "parse failed: {:?}", result);
3684 let program = result.expect("ok");
3685 match &program.statements[0] {
3686 Stmt::If(outer_if) => {
3687 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
3689 assert_eq!(else_branch.len(), 1);
3690 match &else_branch[0] {
3691 Stmt::If(inner_if) => {
3692 assert!(inner_if.else_branch.is_some());
3694 }
3695 _ => panic!("expected nested If from elif"),
3696 }
3697 }
3698 _ => panic!("expected If"),
3699 }
3700 }
3701
3702 #[test]
3703 fn parse_multiple_elif() {
3704 let result = parse(
3706 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
3707 );
3708 assert!(result.is_ok(), "parse failed: {:?}", result);
3709 }
3710
3711 #[test]
3712 fn parse_for_loop() {
3713 let result = parse("for X in items; do echo; done");
3714 assert!(result.is_ok());
3715 let program = result.expect("ok");
3716 assert!(matches!(&program.statements[0], Stmt::For(_)));
3717 }
3718
3719 #[test]
3720 fn parse_brackets_not_array_literal() {
3721 let result = parse("cmd [1");
3723 let _ = result;
3726 }
3727
3728 #[test]
3729 fn parse_named_arg() {
3730 let result = parse("cmd foo=5");
3734 assert!(result.is_ok());
3735 let program = result.expect("ok");
3736 match &program.statements[0] {
3737 Stmt::Command(cmd) => {
3738 assert_eq!(cmd.args.len(), 1);
3739 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
3740 }
3741 _ => panic!("expected Command"),
3742 }
3743 }
3744
3745 #[test]
3746 fn parse_short_flag() {
3747 let result = parse("ls -l");
3748 assert!(result.is_ok());
3749 let program = result.expect("ok");
3750 match &program.statements[0] {
3751 Stmt::Command(cmd) => {
3752 assert_eq!(cmd.name, "ls");
3753 assert_eq!(cmd.args.len(), 1);
3754 match &cmd.args[0] {
3755 Arg::ShortFlag(name) => assert_eq!(name, "l"),
3756 _ => panic!("expected ShortFlag"),
3757 }
3758 }
3759 _ => panic!("expected Command"),
3760 }
3761 }
3762
3763 #[test]
3764 fn parse_long_flag() {
3765 let result = parse("git push --force");
3766 assert!(result.is_ok());
3767 let program = result.expect("ok");
3768 match &program.statements[0] {
3769 Stmt::Command(cmd) => {
3770 assert_eq!(cmd.name, "git");
3771 assert_eq!(cmd.args.len(), 2);
3772 match &cmd.args[0] {
3773 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
3774 _ => panic!("expected Positional push"),
3775 }
3776 match &cmd.args[1] {
3777 Arg::LongFlag(name) => assert_eq!(name, "force"),
3778 _ => panic!("expected LongFlag"),
3779 }
3780 }
3781 _ => panic!("expected Command"),
3782 }
3783 }
3784
3785 #[test]
3786 fn parse_long_flag_with_value() {
3787 let result = parse(r#"git commit --message="hello""#);
3788 assert!(result.is_ok());
3789 let program = result.expect("ok");
3790 match &program.statements[0] {
3791 Stmt::Command(cmd) => {
3792 assert_eq!(cmd.name, "git");
3793 assert_eq!(cmd.args.len(), 2);
3794 match &cmd.args[1] {
3795 Arg::Named { key, value } => {
3796 assert_eq!(key, "message");
3797 match value {
3798 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
3799 _ => panic!("expected String value"),
3800 }
3801 }
3802 _ => panic!("expected Named from --flag=value"),
3803 }
3804 }
3805 _ => panic!("expected Command"),
3806 }
3807 }
3808
3809 #[test]
3810 fn parse_mixed_flags_and_args() {
3811 let result = parse(r#"git commit -m "message" --amend"#);
3812 assert!(result.is_ok());
3813 let program = result.expect("ok");
3814 match &program.statements[0] {
3815 Stmt::Command(cmd) => {
3816 assert_eq!(cmd.name, "git");
3817 assert_eq!(cmd.args.len(), 4);
3818 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
3820 match &cmd.args[1] {
3822 Arg::ShortFlag(name) => assert_eq!(name, "m"),
3823 _ => panic!("expected ShortFlag -m"),
3824 }
3825 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
3827 match &cmd.args[3] {
3829 Arg::LongFlag(name) => assert_eq!(name, "amend"),
3830 _ => panic!("expected LongFlag --amend"),
3831 }
3832 }
3833 _ => panic!("expected Command"),
3834 }
3835 }
3836
3837 #[test]
3838 fn parse_redirect_stdout() {
3839 let result = parse("cmd > file");
3840 assert!(result.is_ok());
3841 let program = result.expect("ok");
3842 match &program.statements[0] {
3844 Stmt::Pipeline(p) => {
3845 assert_eq!(pipeline_commands(p).len(), 1);
3846 let cmd = pipeline_commands(p)[0];
3847 assert_eq!(cmd.redirects.len(), 1);
3848 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
3849 }
3850 _ => panic!("expected Pipeline"),
3851 }
3852 }
3853
3854 #[test]
3855 fn parse_var_ref() {
3856 let result = parse("echo ${VAR}");
3857 assert!(result.is_ok());
3858 let program = result.expect("ok");
3859 match &program.statements[0] {
3860 Stmt::Command(cmd) => {
3861 assert_eq!(cmd.args.len(), 1);
3862 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
3863 }
3864 _ => panic!("expected Command"),
3865 }
3866 }
3867
3868 #[test]
3869 fn parse_multiple_statements() {
3870 let result = parse("a\nb\nc");
3871 assert!(result.is_ok());
3872 let program = result.expect("ok");
3873 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3874 assert_eq!(non_empty.len(), 3);
3875 }
3876
3877 #[test]
3878 fn parse_semicolon_separated() {
3879 let result = parse("a; b; c");
3880 assert!(result.is_ok());
3881 let program = result.expect("ok");
3882 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3883 assert_eq!(non_empty.len(), 3);
3884 }
3885
3886 #[test]
3887 fn parse_complex_pipeline() {
3888 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
3889 assert!(result.is_ok());
3890 let program = result.expect("ok");
3891 match &program.statements[0] {
3892 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 3),
3893 _ => panic!("expected Pipeline"),
3894 }
3895 }
3896
3897 #[test]
3898 fn parse_json_as_string_arg() {
3899 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
3901 assert!(result.is_ok());
3902 }
3903
3904 #[test]
3905 fn parse_mixed_args() {
3906 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
3907 assert!(result.is_ok());
3908 let program = result.expect("ok");
3909 match &program.statements[0] {
3910 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
3911 _ => panic!("expected Command"),
3912 }
3913 }
3914
3915 #[test]
3916 fn error_unterminated_string() {
3917 let result = parse(r#"echo "hello"#);
3918 assert!(result.is_err());
3919 }
3920
3921 #[test]
3922 fn error_unterminated_var_ref() {
3923 let result = parse("echo ${VAR");
3924 assert!(result.is_err());
3925 }
3926
3927 #[test]
3928 fn error_missing_fi() {
3929 let result = parse("if true; then echo");
3930 assert!(result.is_err());
3931 }
3932
3933 #[test]
3934 fn error_missing_done() {
3935 let result = parse("for X in items; do echo");
3936 assert!(result.is_err());
3937 }
3938
3939 #[test]
3940 fn parse_lvalue_single_index() {
3941 let result = parse("xs[0]=9").unwrap();
3942 match &result.statements[0] {
3943 Stmt::Assignment(a) => {
3944 assert_eq!(a.name(), "xs");
3945 assert_eq!(
3946 a.path.segments,
3947 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
3948 );
3949 assert!(!a.local);
3950 }
3951 other => panic!("expected assignment, got {:?}", other),
3952 }
3953 }
3954
3955 #[test]
3956 fn parse_lvalue_negative_index() {
3957 let result = parse("xs[-1]=7").unwrap();
3958 match &result.statements[0] {
3959 Stmt::Assignment(a) => assert_eq!(
3960 a.path.segments,
3961 vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)]
3962 ),
3963 other => panic!("expected assignment, got {:?}", other),
3964 }
3965 }
3966
3967 #[test]
3968 fn parse_lvalue_bareword_key() {
3969 let result = parse("user[email]=x").unwrap();
3970 match &result.statements[0] {
3971 Stmt::Assignment(a) => assert_eq!(
3972 a.path.segments,
3973 vec![
3974 VarSegment::Field("user".into()),
3975 VarSegment::Key("email".into())
3976 ]
3977 ),
3978 other => panic!("expected assignment, got {:?}", other),
3979 }
3980 }
3981
3982 #[test]
3983 fn parse_lvalue_chained_keys() {
3984 let result = parse("s[web][port]=9000").unwrap();
3985 match &result.statements[0] {
3986 Stmt::Assignment(a) => assert_eq!(
3987 a.path.segments,
3988 vec![
3989 VarSegment::Field("s".into()),
3990 VarSegment::Key("web".into()),
3991 VarSegment::Key("port".into())
3992 ]
3993 ),
3994 other => panic!("expected assignment, got {:?}", other),
3995 }
3996 }
3997
3998 #[test]
3999 fn parse_lvalue_dynamic_key() {
4000 let result = parse("r[$k]=v").unwrap();
4001 match &result.statements[0] {
4002 Stmt::Assignment(a) => assert_eq!(
4003 a.path.segments,
4004 vec![
4005 VarSegment::Field("r".into()),
4006 VarSegment::Dynamic("k".into())
4007 ]
4008 ),
4009 other => panic!("expected assignment, got {:?}", other),
4010 }
4011 }
4012
4013 #[test]
4014 fn parse_local_lvalue_spaced() {
4015 let result = parse("local xs[0] = 9").unwrap();
4016 match &result.statements[0] {
4017 Stmt::Assignment(a) => {
4018 assert!(a.local);
4019 assert_eq!(
4020 a.path.segments,
4021 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
4022 );
4023 }
4024 other => panic!("expected assignment, got {:?}", other),
4025 }
4026 }
4027
4028 #[test]
4029 fn env_prefix_subscripted_target_is_not_captured_as_env_scoped() {
4030 let result = parse("user={}\nuser[email]=x echo hi").unwrap();
4040 for stmt in &result.statements {
4041 assert!(
4042 !matches!(stmt, Stmt::EnvScoped { .. }),
4043 "a subscripted assignment must never be captured into EnvScoped: {stmt:?}"
4044 );
4045 }
4046 assert!(matches!(&result.statements[1], Stmt::Assignment(a) if a.name() == "user"));
4048 assert!(matches!(&result.statements[2], Stmt::Command(c) if c.name == "echo"));
4049 }
4050
4051 #[test]
4052 fn parse_nested_cmd_subst() {
4053 let result = parse("X=$(echo $(date))").unwrap();
4055 match &result.statements[0] {
4056 Stmt::Assignment(a) => {
4057 assert_eq!(a.name(), "X");
4058 let outer = subst_cmd(&a.value);
4059 assert_eq!(outer.name, "echo");
4060 match &outer.args[0] {
4062 Arg::Positional(inner_expr) => {
4063 assert_eq!(subst_cmd(inner_expr).name, "date");
4064 }
4065 other => panic!("expected nested cmd subst arg, got {:?}", other),
4066 }
4067 }
4068 other => panic!("expected assignment, got {:?}", other),
4069 }
4070 }
4071
4072 #[test]
4073 fn parse_deeply_nested_cmd_subst() {
4074 let result = parse("X=$(a $(b $(c)))").unwrap();
4076 match &result.statements[0] {
4077 Stmt::Assignment(a) => {
4078 let level1 = subst_cmd(&a.value);
4079 assert_eq!(level1.name, "a");
4080 match &level1.args[0] {
4081 Arg::Positional(level2_expr) => {
4082 let level2 = subst_cmd(level2_expr);
4083 assert_eq!(level2.name, "b");
4084 match &level2.args[0] {
4085 Arg::Positional(level3_expr) => {
4086 assert_eq!(subst_cmd(level3_expr).name, "c");
4087 }
4088 other => panic!("expected level3 cmd subst, got {:?}", other),
4089 }
4090 }
4091 other => panic!("expected level2 cmd subst, got {:?}", other),
4092 }
4093 }
4094 other => panic!("expected assignment, got {:?}", other),
4095 }
4096 }
4097
4098 #[test]
4103 fn value_int_preserved() {
4104 let result = parse("X=42").unwrap();
4105 match &result.statements[0] {
4106 Stmt::Assignment(a) => {
4107 assert_eq!(a.name(), "X");
4108 match &a.value {
4109 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
4110 other => panic!("expected int literal, got {:?}", other),
4111 }
4112 }
4113 other => panic!("expected assignment, got {:?}", other),
4114 }
4115 }
4116
4117 #[test]
4118 fn value_negative_int_preserved() {
4119 let result = parse("X=-99").unwrap();
4120 match &result.statements[0] {
4121 Stmt::Assignment(a) => match &a.value {
4122 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
4123 other => panic!("expected int, got {:?}", other),
4124 },
4125 other => panic!("expected assignment, got {:?}", other),
4126 }
4127 }
4128
4129 #[test]
4130 fn value_float_preserved() {
4131 let result = parse("PI=3.14").unwrap();
4132 match &result.statements[0] {
4133 Stmt::Assignment(a) => match &a.value {
4134 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
4135 other => panic!("expected float, got {:?}", other),
4136 },
4137 other => panic!("expected assignment, got {:?}", other),
4138 }
4139 }
4140
4141 #[test]
4142 fn value_string_preserved() {
4143 let result = parse(r#"echo "hello world""#).unwrap();
4144 match &result.statements[0] {
4145 Stmt::Command(cmd) => {
4146 assert_eq!(cmd.name, "echo");
4147 match &cmd.args[0] {
4148 Arg::Positional(Expr::Literal(Value::String(s))) => {
4149 assert_eq!(s, "hello world");
4150 }
4151 other => panic!("expected string arg, got {:?}", other),
4152 }
4153 }
4154 other => panic!("expected command, got {:?}", other),
4155 }
4156 }
4157
4158 #[test]
4159 fn value_string_with_escapes_preserved() {
4160 let result = parse(r#"echo "line1\nline2""#).unwrap();
4161 match &result.statements[0] {
4162 Stmt::Command(cmd) => match &cmd.args[0] {
4163 Arg::Positional(Expr::Literal(Value::String(s))) => {
4164 assert_eq!(s, "line1\nline2");
4165 }
4166 other => panic!("expected string, got {:?}", other),
4167 },
4168 other => panic!("expected command, got {:?}", other),
4169 }
4170 }
4171
4172 #[test]
4173 fn value_command_name_preserved() {
4174 let result = parse("my-command").unwrap();
4175 match &result.statements[0] {
4176 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
4177 other => panic!("expected command, got {:?}", other),
4178 }
4179 }
4180
4181 #[test]
4182 fn value_assignment_name_preserved() {
4183 let result = parse("MY_VAR=1").unwrap();
4184 match &result.statements[0] {
4185 Stmt::Assignment(a) => assert_eq!(a.name(), "MY_VAR"),
4186 other => panic!("expected assignment, got {:?}", other),
4187 }
4188 }
4189
4190 #[test]
4191 fn value_for_variable_preserved() {
4192 let result = parse("for ITEM in items; do echo; done").unwrap();
4193 match &result.statements[0] {
4194 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
4195 other => panic!("expected for, got {:?}", other),
4196 }
4197 }
4198
4199 #[test]
4200 fn value_varref_name_preserved() {
4201 let result = parse("echo ${MESSAGE}").unwrap();
4202 match &result.statements[0] {
4203 Stmt::Command(cmd) => match &cmd.args[0] {
4204 Arg::Positional(Expr::VarRef(path)) => {
4205 assert_eq!(path.segments.len(), 1);
4206 let VarSegment::Field(name) = &path.segments[0] else {
4207 panic!("expected root field, got {:?}", path.segments[0]);
4208 };
4209 assert_eq!(name, "MESSAGE");
4210 }
4211 other => panic!("expected varref, got {:?}", other),
4212 },
4213 other => panic!("expected command, got {:?}", other),
4214 }
4215 }
4216
4217 #[test]
4218 fn value_varref_field_access_preserved() {
4219 let result = parse("echo ${RESULT.data}").unwrap();
4220 match &result.statements[0] {
4221 Stmt::Command(cmd) => match &cmd.args[0] {
4222 Arg::Positional(Expr::VarRef(path)) => {
4223 assert_eq!(path.segments.len(), 2);
4227 let VarSegment::Field(a) = &path.segments[0] else {
4228 panic!("expected field, got {:?}", path.segments[0]);
4229 };
4230 let VarSegment::Field(b) = &path.segments[1] else {
4231 panic!("expected field, got {:?}", path.segments[1]);
4232 };
4233 assert_eq!(a, "RESULT");
4234 assert_eq!(b, "data");
4235 }
4236 other => panic!("expected varref, got {:?}", other),
4237 },
4238 other => panic!("expected command, got {:?}", other),
4239 }
4240 }
4241
4242 #[test]
4243 fn value_varref_index_parsed() {
4244 let result = parse("echo ${ITEMS[0]}").unwrap();
4247 match &result.statements[0] {
4248 Stmt::Command(cmd) => match &cmd.args[0] {
4249 Arg::Positional(Expr::VarRef(path)) => {
4250 assert_eq!(path.segments.len(), 2);
4251 let VarSegment::Field(name) = &path.segments[0] else {
4252 panic!("expected root field, got {:?}", path.segments[0]);
4253 };
4254 assert_eq!(name, "ITEMS");
4255 assert_eq!(path.segments[1], VarSegment::Index(0));
4256 }
4257 other => panic!("expected varref, got {:?}", other),
4258 },
4259 other => panic!("expected command, got {:?}", other),
4260 }
4261 }
4262
4263 #[test]
4264 fn value_named_arg_preserved() {
4265 let result = parse("cmd count=42").unwrap();
4269 match &result.statements[0] {
4270 Stmt::Command(cmd) => {
4271 assert_eq!(cmd.name, "cmd");
4272 match &cmd.args[0] {
4273 Arg::WordAssign { key, value } => {
4274 assert_eq!(key, "count");
4275 match value {
4276 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
4277 other => panic!("expected int, got {:?}", other),
4278 }
4279 }
4280 other => panic!("expected WordAssign arg, got {:?}", other),
4281 }
4282 }
4283 other => panic!("expected command, got {:?}", other),
4284 }
4285 }
4286
4287 #[test]
4288 fn value_function_def_name_preserved() {
4289 let result = parse("greet() { echo }").unwrap();
4290 match &result.statements[0] {
4291 Stmt::ToolDef(t) => {
4292 assert_eq!(t.name, "greet");
4293 assert!(t.params.is_empty());
4294 }
4295 other => panic!("expected function def, got {:?}", other),
4296 }
4297 }
4298
4299 #[test]
4304 fn parse_comparison_equals() {
4305 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
4307 match &result.statements[0] {
4308 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4309 Expr::Test(test) => match test.as_ref() {
4310 TestExpr::Comparison { left, op, right } => {
4311 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
4312 assert_eq!(*op, TestCmpOp::Eq);
4313 match right.as_ref() {
4314 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
4315 other => panic!("expected int, got {:?}", other),
4316 }
4317 }
4318 other => panic!("expected comparison, got {:?}", other),
4319 },
4320 other => panic!("expected test expr, got {:?}", other),
4321 },
4322 other => panic!("expected if, got {:?}", other),
4323 }
4324 }
4325
4326 #[test]
4327 fn parse_comparison_not_equals() {
4328 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
4329 match &result.statements[0] {
4330 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4331 Expr::Test(test) => match test.as_ref() {
4332 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
4333 other => panic!("expected comparison, got {:?}", other),
4334 },
4335 other => panic!("expected test expr, got {:?}", other),
4336 },
4337 other => panic!("expected if, got {:?}", other),
4338 }
4339 }
4340
4341 #[test]
4342 fn parse_comparison_less_than() {
4343 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
4344 match &result.statements[0] {
4345 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4346 Expr::Test(test) => match test.as_ref() {
4347 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
4348 other => panic!("expected comparison, got {:?}", other),
4349 },
4350 other => panic!("expected test expr, got {:?}", other),
4351 },
4352 other => panic!("expected if, got {:?}", other),
4353 }
4354 }
4355
4356 #[test]
4357 fn parse_comparison_greater_than() {
4358 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
4359 match &result.statements[0] {
4360 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4361 Expr::Test(test) => match test.as_ref() {
4362 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
4363 other => panic!("expected comparison, got {:?}", other),
4364 },
4365 other => panic!("expected test expr, got {:?}", other),
4366 },
4367 other => panic!("expected if, got {:?}", other),
4368 }
4369 }
4370
4371 #[test]
4372 fn parse_comparison_less_equal() {
4373 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
4374 match &result.statements[0] {
4375 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4376 Expr::Test(test) => match test.as_ref() {
4377 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
4378 other => panic!("expected comparison, got {:?}", other),
4379 },
4380 other => panic!("expected test expr, got {:?}", other),
4381 },
4382 other => panic!("expected if, got {:?}", other),
4383 }
4384 }
4385
4386 #[test]
4387 fn parse_comparison_greater_equal() {
4388 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
4389 match &result.statements[0] {
4390 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4391 Expr::Test(test) => match test.as_ref() {
4392 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
4393 other => panic!("expected comparison, got {:?}", other),
4394 },
4395 other => panic!("expected test expr, got {:?}", other),
4396 },
4397 other => panic!("expected if, got {:?}", other),
4398 }
4399 }
4400
4401 #[test]
4402 fn parse_regex_match() {
4403 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
4404 match &result.statements[0] {
4405 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4406 Expr::Test(test) => match test.as_ref() {
4407 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
4408 other => panic!("expected comparison, got {:?}", other),
4409 },
4410 other => panic!("expected test expr, got {:?}", other),
4411 },
4412 other => panic!("expected if, got {:?}", other),
4413 }
4414 }
4415
4416 #[test]
4417 fn parse_regex_not_match() {
4418 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
4419 match &result.statements[0] {
4420 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4421 Expr::Test(test) => match test.as_ref() {
4422 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
4423 other => panic!("expected comparison, got {:?}", other),
4424 },
4425 other => panic!("expected test expr, got {:?}", other),
4426 },
4427 other => panic!("expected if, got {:?}", other),
4428 }
4429 }
4430
4431 #[test]
4432 fn parse_string_interpolation() {
4433 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
4434 match &result.statements[0] {
4435 Stmt::Command(cmd) => match &cmd.args[0] {
4436 Arg::Positional(Expr::Interpolated(parts)) => {
4437 assert_eq!(parts.len(), 3);
4438 match &parts[0] {
4439 StringPart::Literal(s) => assert_eq!(s, "Hello "),
4440 other => panic!("expected literal, got {:?}", other),
4441 }
4442 match &parts[1] {
4443 StringPart::Var(path) => {
4444 assert_eq!(path.segments.len(), 1);
4445 let VarSegment::Field(name) = &path.segments[0] else {
4446 panic!("expected root field, got {:?}", path.segments[0]);
4447 };
4448 assert_eq!(name, "NAME");
4449 }
4450 other => panic!("expected var, got {:?}", other),
4451 }
4452 match &parts[2] {
4453 StringPart::Literal(s) => assert_eq!(s, "!"),
4454 other => panic!("expected literal, got {:?}", other),
4455 }
4456 }
4457 other => panic!("expected interpolated, got {:?}", other),
4458 },
4459 other => panic!("expected command, got {:?}", other),
4460 }
4461 }
4462
4463 #[test]
4464 fn parse_string_interpolation_multiple_vars() {
4465 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
4466 match &result.statements[0] {
4467 Stmt::Command(cmd) => match &cmd.args[0] {
4468 Arg::Positional(Expr::Interpolated(parts)) => {
4469 assert_eq!(parts.len(), 3);
4471 assert!(matches!(&parts[0], StringPart::Var(_)));
4472 assert!(matches!(&parts[1], StringPart::Literal(_)));
4473 assert!(matches!(&parts[2], StringPart::Var(_)));
4474 }
4475 other => panic!("expected interpolated, got {:?}", other),
4476 },
4477 other => panic!("expected command, got {:?}", other),
4478 }
4479 }
4480
4481 #[test]
4482 fn parse_empty_function_body() {
4483 let result = parse("empty() { }").unwrap();
4484 match &result.statements[0] {
4485 Stmt::ToolDef(t) => {
4486 assert_eq!(t.name, "empty");
4487 assert!(t.params.is_empty());
4488 assert!(t.body.is_empty());
4489 }
4490 other => panic!("expected function def, got {:?}", other),
4491 }
4492 }
4493
4494 #[test]
4495 fn parse_bash_style_function() {
4496 let result = parse("function greet { echo hello }").unwrap();
4497 match &result.statements[0] {
4498 Stmt::ToolDef(t) => {
4499 assert_eq!(t.name, "greet");
4500 assert!(t.params.is_empty());
4501 assert_eq!(t.body.len(), 1);
4502 }
4503 other => panic!("expected function def, got {:?}", other),
4504 }
4505 }
4506
4507 #[test]
4508 fn parse_comparison_string_values() {
4509 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
4510 match &result.statements[0] {
4511 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4512 Expr::Test(test) => match test.as_ref() {
4513 TestExpr::Comparison { left, op, right } => {
4514 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
4515 assert_eq!(*op, TestCmpOp::Eq);
4516 match right.as_ref() {
4517 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
4518 other => panic!("expected string, got {:?}", other),
4519 }
4520 }
4521 other => panic!("expected comparison, got {:?}", other),
4522 },
4523 other => panic!("expected test expr, got {:?}", other),
4524 },
4525 other => panic!("expected if, got {:?}", other),
4526 }
4527 }
4528
4529 #[test]
4534 fn parse_cmd_subst_simple() {
4535 let result = parse("X=$(echo)").unwrap();
4536 match &result.statements[0] {
4537 Stmt::Assignment(a) => {
4538 assert_eq!(a.name(), "X");
4539 assert_eq!(subst_cmd(&a.value).name, "echo");
4540 }
4541 other => panic!("expected assignment, got {:?}", other),
4542 }
4543 }
4544
4545 #[test]
4546 fn parse_cmd_subst_with_args() {
4547 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
4548 match &result.statements[0] {
4549 Stmt::Assignment(a) => {
4550 let cmd = subst_cmd(&a.value);
4551 assert_eq!(cmd.name, "fetch");
4552 assert_eq!(cmd.args.len(), 1);
4553 match &cmd.args[0] {
4554 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
4555 other => panic!("expected WordAssign arg, got {:?}", other),
4556 }
4557 }
4558 other => panic!("expected assignment, got {:?}", other),
4559 }
4560 }
4561
4562 #[test]
4563 fn parse_cmd_subst_pipeline() {
4564 let result = parse("X=$(cat file | grep pattern)").unwrap();
4565 match &result.statements[0] {
4566 Stmt::Assignment(a) => {
4567 let pipeline = subst_pipeline(&a.value);
4568 assert_eq!(pipeline_commands(pipeline).len(), 2);
4569 assert_eq!(pipeline_commands(pipeline)[0].name, "cat");
4570 assert_eq!(pipeline_commands(pipeline)[1].name, "grep");
4571 }
4572 other => panic!("expected assignment, got {:?}", other),
4573 }
4574 }
4575
4576 #[test]
4577 fn parse_cmd_subst_with_redirect() {
4578 let result = parse("X=$(echo hi > out.txt)").unwrap();
4583 match &result.statements[0] {
4584 Stmt::Assignment(a) => {
4585 let pipeline = subst_pipeline(&a.value);
4586 assert_eq!(pipeline_commands(pipeline).len(), 1);
4587 let cmd = pipeline_commands(pipeline)[0];
4588 assert_eq!(cmd.name, "echo");
4589 assert_eq!(cmd.redirects.len(), 1);
4590 assert!(matches!(
4591 cmd.redirects[0].kind,
4592 RedirectKind::StdoutOverwrite
4593 ));
4594 }
4595 other => panic!("expected assignment, got {:?}", other),
4596 }
4597 }
4598
4599 #[test]
4600 fn parse_cmd_subst_redirect_target_with_nested_subst() {
4601 let result = parse("X=$(echo hi > $(echo f))").unwrap();
4607 match &result.statements[0] {
4608 Stmt::Assignment(a) => {
4609 let pipeline = subst_pipeline(&a.value);
4610 assert_eq!(pipeline_commands(pipeline).len(), 1);
4611 let cmd = pipeline_commands(pipeline)[0];
4612 assert_eq!(cmd.name, "echo");
4613 assert_eq!(cmd.redirects.len(), 1);
4614 assert!(
4615 matches!(cmd.redirects[0].target, Expr::CommandSubst(_)),
4616 "redirect target should be a nested command substitution, got {:?}",
4617 cmd.redirects[0].target
4618 );
4619 }
4620 other => panic!("expected assignment, got {:?}", other),
4621 }
4622 }
4623
4624 #[test]
4625 fn parse_cmd_subst_chain_with_redirect() {
4626 let result = parse("X=$(echo a && echo b > out.txt)").unwrap();
4630 let stmts = match &result.statements[0] {
4631 Stmt::Assignment(a) => match &a.value {
4632 Expr::CommandSubst(s) => s,
4633 other => panic!("expected command subst, got {:?}", other),
4634 },
4635 other => panic!("expected assignment, got {:?}", other),
4636 };
4637 match stmts.as_slice() {
4638 [Stmt::AndChain { left, right }] => {
4639 assert!(
4641 matches!(**left, Stmt::Command(_)),
4642 "left of && should be a bare command, got {:?}",
4643 left
4644 );
4645 match &**right {
4647 Stmt::Pipeline(p) => {
4648 assert_eq!(pipeline_commands(p).len(), 1);
4649 assert_eq!(pipeline_commands(p)[0].name, "echo");
4650 assert_eq!(pipeline_commands(p)[0].redirects.len(), 1);
4651 }
4652 other => panic!("right should be a redirect-bearing pipeline, got {:?}", other),
4653 }
4654 }
4655 other => panic!("expected a single AndChain, got {:?}", other),
4656 }
4657 }
4658
4659 #[test]
4660 fn parse_cmd_subst_in_condition() {
4661 let result = parse("if kaish-validate; then echo; fi").unwrap();
4663 match &result.statements[0] {
4664 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4665 Expr::Command(cmd) => {
4666 assert_eq!(cmd.name, "kaish-validate");
4667 }
4668 other => panic!("expected command, got {:?}", other),
4669 },
4670 other => panic!("expected if, got {:?}", other),
4671 }
4672 }
4673
4674 #[test]
4689 fn parse_cmd_subst_unquoted_for_loop() {
4690 let result = parse("X=$(for f in a b; do echo $f; done)").unwrap();
4691 let stmts = match &result.statements[0] {
4692 Stmt::Assignment(a) => match &a.value {
4693 Expr::CommandSubst(s) => s,
4694 other => panic!("expected command subst, got {:?}", other),
4695 },
4696 other => panic!("expected assignment, got {:?}", other),
4697 };
4698 match stmts.as_slice() {
4699 [Stmt::For(f)] => {
4700 assert_eq!(f.variable, "f");
4701 assert_eq!(f.items.len(), 2);
4702 assert!(matches!(f.body.as_slice(), [Stmt::Command(c)] if c.name == "echo"));
4703 }
4704 other => panic!("expected a single For statement, got {:?}", other),
4705 }
4706 }
4707
4708 #[test]
4709 fn parse_cmd_subst_unquoted_while_loop() {
4710 let result = parse("X=$(while false; do echo x; done)").unwrap();
4711 let stmts = match &result.statements[0] {
4712 Stmt::Assignment(a) => match &a.value {
4713 Expr::CommandSubst(s) => s,
4714 other => panic!("expected command subst, got {:?}", other),
4715 },
4716 other => panic!("expected assignment, got {:?}", other),
4717 };
4718 assert!(
4719 matches!(stmts.as_slice(), [Stmt::While(w)] if matches!(w.body.as_slice(), [Stmt::Command(c)] if c.name == "echo")),
4720 "expected a single While statement, got {stmts:?}"
4721 );
4722 }
4723
4724 #[test]
4725 fn parse_cmd_subst_unquoted_if_else() {
4726 let result = parse("X=$(if true; then echo one; else echo two; fi)").unwrap();
4727 let stmts = match &result.statements[0] {
4728 Stmt::Assignment(a) => match &a.value {
4729 Expr::CommandSubst(s) => s,
4730 other => panic!("expected command subst, got {:?}", other),
4731 },
4732 other => panic!("expected assignment, got {:?}", other),
4733 };
4734 match stmts.as_slice() {
4735 [Stmt::If(i)] => {
4736 assert!(i.else_branch.is_some(), "expected an else branch");
4737 assert!(matches!(i.then_branch.as_slice(), [Stmt::Command(c)] if c.name == "echo"));
4738 }
4739 other => panic!("expected a single If statement, got {:?}", other),
4740 }
4741 }
4742
4743 #[test]
4744 fn parse_cmd_subst_unquoted_case() {
4745 let result = parse("X=$(case a in a) echo hit;; esac)").unwrap();
4750 let stmts = match &result.statements[0] {
4751 Stmt::Assignment(a) => match &a.value {
4752 Expr::CommandSubst(s) => s,
4753 other => panic!("expected command subst, got {:?}", other),
4754 },
4755 other => panic!("expected assignment, got {:?}", other),
4756 };
4757 match stmts.as_slice() {
4758 [Stmt::Case(c)] => {
4759 assert_eq!(c.branches.len(), 1);
4760 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
4761 }
4762 other => panic!("expected a single Case statement, got {:?}", other),
4763 }
4764 }
4765
4766 #[test]
4767 fn parse_cmd_subst_unquoted_case_with_parenthesized_pattern() {
4768 let result = parse("X=$(case a in (a) echo hit;; esac)").unwrap();
4771 let stmts = match &result.statements[0] {
4772 Stmt::Assignment(a) => match &a.value {
4773 Expr::CommandSubst(s) => s,
4774 other => panic!("expected command subst, got {:?}", other),
4775 },
4776 other => panic!("expected assignment, got {:?}", other),
4777 };
4778 assert!(
4779 matches!(stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
4780 "expected a single Case statement, got {stmts:?}"
4781 );
4782 }
4783
4784 #[test]
4785 fn parse_cmd_subst_unquoted_case_parenthesized_pattern_with_bareword_esac_in_body() {
4786 let result = parse("X=$(case a in (a) y=esac;; b) echo two;; esac)").unwrap();
4797 let stmts = match &result.statements[0] {
4798 Stmt::Assignment(a) => match &a.value {
4799 Expr::CommandSubst(s) => s,
4800 other => panic!("expected command subst, got {:?}", other),
4801 },
4802 other => panic!("expected assignment, got {:?}", other),
4803 };
4804 match stmts.as_slice() {
4805 [Stmt::Case(c)] => {
4806 assert_eq!(c.branches.len(), 2);
4807 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
4808 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
4809 }
4810 other => panic!("expected a single Case statement with two branches, got {:?}", other),
4811 }
4812 }
4813
4814 #[test]
4815 fn parse_cmd_subst_unquoted_nested_case_parenthesized_pattern_esac_in_outer_body() {
4816 let result = parse(
4827 "X=$(case a in (a) case b in (b) echo z;; esac; y=esac;; c) echo two;; esac)",
4828 )
4829 .unwrap();
4830 let stmts = match &result.statements[0] {
4831 Stmt::Assignment(a) => match &a.value {
4832 Expr::CommandSubst(s) => s,
4833 other => panic!("expected command subst, got {:?}", other),
4834 },
4835 other => panic!("expected assignment, got {:?}", other),
4836 };
4837 match stmts.as_slice() {
4838 [Stmt::Case(c)] => {
4839 assert_eq!(c.branches.len(), 2);
4840 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
4841 assert_eq!(c.branches[1].patterns, vec!["c".to_string()]);
4842 }
4843 other => panic!("expected a single Case statement with two branches, got {:?}", other),
4844 }
4845 }
4846
4847 #[test]
4848 fn parse_cmd_subst_unquoted_case_eq_argv_key() {
4849 let result = parse("X=$(echo case=x)").unwrap();
4858 let stmts = match &result.statements[0] {
4859 Stmt::Assignment(a) => match &a.value {
4860 Expr::CommandSubst(s) => s,
4861 other => panic!("expected command subst, got {:?}", other),
4862 },
4863 other => panic!("expected assignment, got {:?}", other),
4864 };
4865 let cmd = match stmts.as_slice() {
4866 [Stmt::Command(c)] => c,
4867 other => panic!("expected a single echo command, got {:?}", other),
4868 };
4869 assert_eq!(cmd.name, "echo");
4870 match &cmd.args[0] {
4871 Arg::WordAssign { key, value } => {
4872 assert_eq!(key, "case");
4873 match value {
4874 Expr::Literal(Value::String(s)) => assert_eq!(s, "x"),
4875 other => panic!("expected string \"x\", got {:?}", other),
4876 }
4877 }
4878 other => panic!("expected WordAssign arg, got {:?}", other),
4879 }
4880 }
4881
4882 #[test]
4883 fn parse_cmd_subst_unquoted_case_eq_argv_key_with_sibling_keyword_keys() {
4884 let result = parse("X=$(tool case=x do=y)").unwrap();
4888 let stmts = match &result.statements[0] {
4889 Stmt::Assignment(a) => match &a.value {
4890 Expr::CommandSubst(s) => s,
4891 other => panic!("expected command subst, got {:?}", other),
4892 },
4893 other => panic!("expected assignment, got {:?}", other),
4894 };
4895 let cmd = match stmts.as_slice() {
4896 [Stmt::Command(c)] => c,
4897 other => panic!("expected a single tool command, got {:?}", other),
4898 };
4899 assert_eq!(cmd.name, "tool");
4900 assert_eq!(cmd.args.len(), 2);
4901 assert!(matches!(&cmd.args[0], Arg::WordAssign { key, .. } if key == "case"));
4902 assert!(matches!(&cmd.args[1], Arg::WordAssign { key, .. } if key == "do"));
4903 }
4904
4905 #[test]
4906 fn parse_cmd_subst_unquoted_case_inside_nested_subst() {
4907 let result = parse("X=$(echo $(case b in b) echo x;; esac))").unwrap();
4914 let outer_stmts = match &result.statements[0] {
4915 Stmt::Assignment(a) => match &a.value {
4916 Expr::CommandSubst(s) => s,
4917 other => panic!("expected command subst, got {:?}", other),
4918 },
4919 other => panic!("expected assignment, got {:?}", other),
4920 };
4921 let outer_cmd = match outer_stmts.as_slice() {
4922 [Stmt::Command(c)] => c,
4923 other => panic!("expected a single echo command, got {:?}", other),
4924 };
4925 assert_eq!(outer_cmd.name, "echo");
4926 let inner_stmts = match &outer_cmd.args[0] {
4927 Arg::Positional(Expr::CommandSubst(s)) => s,
4928 other => panic!("expected nested command subst arg, got {:?}", other),
4929 };
4930 assert!(
4931 matches!(inner_stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
4932 "expected a single Case statement inside the inner $(), got {inner_stmts:?}"
4933 );
4934 }
4935
4936 #[test]
4937 fn parse_cmd_subst_unquoted_esac_as_bareword() {
4938 let result = parse("X=$(echo esac)").unwrap();
4943 let stmts = match &result.statements[0] {
4944 Stmt::Assignment(a) => match &a.value {
4945 Expr::CommandSubst(s) => s,
4946 other => panic!("expected command subst, got {:?}", other),
4947 },
4948 other => panic!("expected assignment, got {:?}", other),
4949 };
4950 let cmd = match stmts.as_slice() {
4951 [Stmt::Command(c)] => c,
4952 other => panic!("expected a single echo command, got {:?}", other),
4953 };
4954 assert_eq!(cmd.name, "echo");
4955 assert!(
4956 matches!(&cmd.args[0], Arg::Positional(Expr::Literal(Value::String(s))) if s == "esac"),
4957 "expected \"esac\" as a literal argument, got {:?}",
4958 cmd.args[0]
4959 );
4960 }
4961
4962 #[test]
4963 fn parse_cmd_subst_unquoted_esac_as_bareword_inside_still_open_case() {
4964 let result = parse("X=$(case a in a) y=esac;; b) echo two;; esac)").unwrap();
4973 let stmts = match &result.statements[0] {
4974 Stmt::Assignment(a) => match &a.value {
4975 Expr::CommandSubst(s) => s,
4976 other => panic!("expected command subst, got {:?}", other),
4977 },
4978 other => panic!("expected assignment, got {:?}", other),
4979 };
4980 match stmts.as_slice() {
4981 [Stmt::Case(c)] => {
4982 assert_eq!(c.branches.len(), 2);
4983 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
4984 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
4985 }
4986 other => panic!("expected a single Case statement with two branches, got {:?}", other),
4987 }
4988 }
4989
4990 #[test]
4991 fn parse_quoted_cmd_subst_case_pattern_paren_not_miscounted() {
4992 let result = parse(r#"X="pre $(case v in v) echo x;; esac) post""#).unwrap();
5001 let parts = match &result.statements[0] {
5002 Stmt::Assignment(a) => match &a.value {
5003 Expr::Interpolated(parts) => parts,
5004 other => panic!("expected an interpolated string, got {:?}", other),
5005 },
5006 other => panic!("expected assignment, got {:?}", other),
5007 };
5008 let stmts = match parts.as_slice() {
5009 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5010 {
5011 assert_eq!(pre, "pre ");
5012 assert_eq!(post, " post");
5013 stmts
5014 }
5015 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5016 };
5017 assert!(
5018 matches!(stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
5019 "expected a single Case statement inside the quoted $(...), got {stmts:?}"
5020 );
5021 }
5022
5023 #[test]
5024 fn parse_quoted_cmd_subst_case_parenthesized_pattern_esac_in_body_not_miscounted() {
5025 let result = parse(r#"X="pre $(case a in (a) y=esac;; b) echo two;; esac) post""#).unwrap();
5029 let parts = match &result.statements[0] {
5030 Stmt::Assignment(a) => match &a.value {
5031 Expr::Interpolated(parts) => parts,
5032 other => panic!("expected an interpolated string, got {:?}", other),
5033 },
5034 other => panic!("expected assignment, got {:?}", other),
5035 };
5036 let stmts = match parts.as_slice() {
5037 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5038 {
5039 assert_eq!(pre, "pre ");
5040 assert_eq!(post, " post");
5041 stmts
5042 }
5043 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5044 };
5045 match stmts.as_slice() {
5046 [Stmt::Case(c)] => {
5047 assert_eq!(c.branches.len(), 2);
5048 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5049 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
5050 }
5051 other => panic!("expected a single Case statement with two branches, got {:?}", other),
5052 }
5053 }
5054
5055 #[test]
5056 fn parse_quoted_cmd_subst_case_eq_argv_key_not_miscounted() {
5057 let result = parse(r#"X="pre $(echo case=x) post""#).unwrap();
5060 let parts = match &result.statements[0] {
5061 Stmt::Assignment(a) => match &a.value {
5062 Expr::Interpolated(parts) => parts,
5063 other => panic!("expected an interpolated string, got {:?}", other),
5064 },
5065 other => panic!("expected assignment, got {:?}", other),
5066 };
5067 let stmts = match parts.as_slice() {
5068 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5069 {
5070 assert_eq!(pre, "pre ");
5071 assert_eq!(post, " post");
5072 stmts
5073 }
5074 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5075 };
5076 let cmd = match stmts.as_slice() {
5077 [Stmt::Command(c)] => c,
5078 other => panic!("expected a single echo command, got {:?}", other),
5079 };
5080 assert_eq!(cmd.name, "echo");
5081 assert!(matches!(&cmd.args[0], Arg::WordAssign { key, .. } if key == "case"));
5082 }
5083
5084 #[test]
5085 fn parse_cmd_subst_unquoted_nested_with_control_structure() {
5086 let result = parse("X=$(echo $(for f in a; do echo $f; done))").unwrap();
5089 let outer_stmts = match &result.statements[0] {
5090 Stmt::Assignment(a) => match &a.value {
5091 Expr::CommandSubst(s) => s,
5092 other => panic!("expected command subst, got {:?}", other),
5093 },
5094 other => panic!("expected assignment, got {:?}", other),
5095 };
5096 let outer_cmd = match outer_stmts.as_slice() {
5097 [Stmt::Command(c)] => c,
5098 other => panic!("expected a single echo command, got {:?}", other),
5099 };
5100 assert_eq!(outer_cmd.name, "echo");
5101 let inner_stmts = match &outer_cmd.args[0] {
5102 Arg::Positional(Expr::CommandSubst(s)) => s,
5103 other => panic!("expected nested command subst arg, got {:?}", other),
5104 };
5105 assert!(
5106 matches!(inner_stmts.as_slice(), [Stmt::For(f)] if f.variable == "f"),
5107 "expected a single For statement inside the inner $(), got {inner_stmts:?}"
5108 );
5109 }
5110
5111 #[test]
5112 fn parse_cmd_subst_unquoted_pipeline() {
5113 let result = parse("X=$(cat f | grep pat | wc -l)").unwrap();
5118 let value = match &result.statements[0] {
5119 Stmt::Assignment(a) => a.value.clone(),
5120 other => panic!("expected assignment, got {:?}", other),
5121 };
5122 let pipeline = subst_pipeline(&value);
5123 assert_eq!(pipeline_commands(pipeline).len(), 3);
5124 assert_eq!(pipeline_commands(pipeline)[2].name, "wc");
5125 }
5126
5127 #[test]
5128 fn parse_quoted_cmd_subst_with_for_loop_still_works() {
5129 let result = parse(r#"out="$(for f in a b; do echo $f; done)""#).unwrap();
5133 match &result.statements[0] {
5134 Stmt::Assignment(a) => assert_eq!(a.name(), "out"),
5135 other => panic!("expected assignment, got {:?}", other),
5136 }
5137 }
5138
5139 #[test]
5140 fn parse_cmd_subst_body_error_reports_span_inside_body_not_at_dollar_paren() {
5141 let source = "echo $(for f in a; do echo $f done)";
5149 let errs = parse(source).expect_err("missing loop terminator must be a parse error");
5150 let dollar_paren = source.find("$(").expect("fixture contains $(");
5151 assert!(
5152 errs.iter().all(|e| e.span.start > dollar_paren + 1),
5153 "error span must point inside the $() body, not at '$(' itself: {errs:?}"
5154 );
5155 assert!(
5158 errs.iter().any(|e| e.message.contains("done")),
5159 "expected the missing-`done` diagnostic, got: {errs:?}"
5160 );
5161 }
5162
5163 #[test]
5164 fn parse_cmd_subst_unterminated_reports_error() {
5165 let result = parse("echo $(for f in a; do echo $f; done");
5166 assert!(result.is_err(), "a missing `)` must be a parse error");
5167 }
5168
5169 #[test]
5174 fn parse_env_prefix_single() {
5175 let result = parse("FOO=bar echo hi").unwrap();
5176 match &result.statements[0] {
5177 Stmt::EnvScoped { assignments, body } => {
5178 assert_eq!(assignments.len(), 1);
5179 assert_eq!(assignments[0].name(), "FOO");
5180 assert!(!assignments[0].local);
5181 match body.as_ref() {
5182 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
5183 other => panic!("expected command body, got {other:?}"),
5184 }
5185 }
5186 other => panic!("expected env-scoped, got {other:?}"),
5187 }
5188 }
5189
5190 #[test]
5191 fn parse_env_prefix_multiple() {
5192 let result = parse("A=1 B=2 run").unwrap();
5193 match &result.statements[0] {
5194 Stmt::EnvScoped { assignments, body } => {
5195 assert_eq!(assignments.len(), 2);
5196 assert_eq!(assignments[0].name(), "A");
5197 assert_eq!(assignments[1].name(), "B");
5198 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
5199 }
5200 other => panic!("expected env-scoped, got {other:?}"),
5201 }
5202 }
5203
5204 #[test]
5205 fn parse_bare_assignment_is_not_env_scoped() {
5206 let result = parse("FOO=bar").unwrap();
5208 assert!(
5209 matches!(&result.statements[0], Stmt::Assignment(a) if a.name() == "FOO"),
5210 "got {:?}",
5211 result.statements[0]
5212 );
5213 }
5214
5215 #[test]
5216 fn parse_assignment_then_and_chain_does_not_over_capture() {
5217 let result = parse("FOO=bar && echo hi").unwrap();
5220 match &result.statements[0] {
5221 Stmt::AndChain { left, right } => {
5222 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name() == "FOO"));
5223 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
5224 }
5225 other => panic!("expected and-chain, got {other:?}"),
5226 }
5227 }
5228
5229 #[test]
5230 fn parse_env_prefix_pipeline_body() {
5231 let result = parse("FOO=bar cat | grep x").unwrap();
5232 match &result.statements[0] {
5233 Stmt::EnvScoped { assignments, body } => {
5234 assert_eq!(assignments[0].name(), "FOO");
5235 match body.as_ref() {
5236 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 2),
5237 other => panic!("expected pipeline body, got {other:?}"),
5238 }
5239 }
5240 other => panic!("expected env-scoped, got {other:?}"),
5241 }
5242 }
5243
5244 fn parse_err_message(source: &str) -> String {
5249 parse(source)
5250 .expect_err("expected a parse error")
5251 .iter()
5252 .map(|e| e.message.clone())
5253 .collect::<Vec<_>>()
5254 .join(" ")
5255 }
5256
5257 #[test]
5258 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
5259 let msg = parse_err_message("echo /tmp/$(echo x).txt");
5262 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
5263 }
5264
5265 #[test]
5266 fn argv_splat_var_glued_to_path_is_rejected() {
5267 assert!(parse("echo $dir/out.txt").is_err());
5268 }
5269
5270 #[test]
5271 fn argv_splat_three_way_glue_is_rejected() {
5272 assert!(parse("echo foo$(echo bar)baz").is_err());
5273 }
5274
5275 #[test]
5276 fn argv_splat_quoted_word_is_accepted() {
5277 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
5279 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
5280 }
5281
5282 #[test]
5283 fn argv_single_token_words_are_not_splat() {
5284 assert!(parse("echo file.txt").is_ok(), "file.txt");
5286 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
5287 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
5288 }
5289
5290 #[test]
5291 fn argv_spaced_words_are_not_splat() {
5292 assert!(parse("echo a b c").is_ok());
5293 assert!(parse("echo /tmp/x $(echo y)").is_ok());
5294 }
5295
5296 #[test]
5297 fn parse_cmd_subst_in_command_arg() {
5298 let result = parse("echo $(whoami)").unwrap();
5299 match &result.statements[0] {
5300 Stmt::Command(cmd) => {
5301 assert_eq!(cmd.name, "echo");
5302 match &cmd.args[0] {
5303 Arg::Positional(expr) => {
5304 assert_eq!(subst_cmd(expr).name, "whoami");
5305 }
5306 other => panic!("expected command subst, got {:?}", other),
5307 }
5308 }
5309 other => panic!("expected command, got {:?}", other),
5310 }
5311 }
5312
5313 #[test]
5318 fn parse_condition_and() {
5319 let result = parse("if check-a && check-b; then echo; fi").unwrap();
5321 match &result.statements[0] {
5322 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5323 Expr::BinaryOp { left, op, right } => {
5324 assert_eq!(*op, BinaryOp::And);
5325 assert!(matches!(left.as_ref(), Expr::Command(_)));
5326 assert!(matches!(right.as_ref(), Expr::Command(_)));
5327 }
5328 other => panic!("expected binary op, got {:?}", other),
5329 },
5330 other => panic!("expected if, got {:?}", other),
5331 }
5332 }
5333
5334 #[test]
5335 fn parse_condition_or() {
5336 let result = parse("if try-a || try-b; then echo; fi").unwrap();
5337 match &result.statements[0] {
5338 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5339 Expr::BinaryOp { left, op, right } => {
5340 assert_eq!(*op, BinaryOp::Or);
5341 assert!(matches!(left.as_ref(), Expr::Command(_)));
5342 assert!(matches!(right.as_ref(), Expr::Command(_)));
5343 }
5344 other => panic!("expected binary op, got {:?}", other),
5345 },
5346 other => panic!("expected if, got {:?}", other),
5347 }
5348 }
5349
5350 #[test]
5351 fn parse_condition_and_or_precedence() {
5352 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
5354 match &result.statements[0] {
5355 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5356 Expr::BinaryOp { left, op, right } => {
5357 assert_eq!(*op, BinaryOp::Or);
5359 match left.as_ref() {
5361 Expr::BinaryOp { op: inner_op, .. } => {
5362 assert_eq!(*inner_op, BinaryOp::And);
5363 }
5364 other => panic!("expected binary op (&&), got {:?}", other),
5365 }
5366 assert!(matches!(right.as_ref(), Expr::Command(_)));
5368 }
5369 other => panic!("expected binary op, got {:?}", other),
5370 },
5371 other => panic!("expected if, got {:?}", other),
5372 }
5373 }
5374
5375 #[test]
5376 fn parse_condition_multiple_and() {
5377 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
5378 match &result.statements[0] {
5379 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5380 Expr::BinaryOp { left, op, .. } => {
5381 assert_eq!(*op, BinaryOp::And);
5382 match left.as_ref() {
5384 Expr::BinaryOp { op: inner_op, .. } => {
5385 assert_eq!(*inner_op, BinaryOp::And);
5386 }
5387 other => panic!("expected binary op, got {:?}", other),
5388 }
5389 }
5390 other => panic!("expected binary op, got {:?}", other),
5391 },
5392 other => panic!("expected if, got {:?}", other),
5393 }
5394 }
5395
5396 #[test]
5397 fn parse_condition_mixed_comparison_and_logical() {
5398 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
5400 match &result.statements[0] {
5401 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5402 Expr::BinaryOp { left, op, right } => {
5403 assert_eq!(*op, BinaryOp::And);
5404 match left.as_ref() {
5406 Expr::Test(test) => match test.as_ref() {
5407 TestExpr::Comparison { op: left_op, .. } => {
5408 assert_eq!(*left_op, TestCmpOp::Eq);
5409 }
5410 other => panic!("expected comparison, got {:?}", other),
5411 },
5412 other => panic!("expected test, got {:?}", other),
5413 }
5414 match right.as_ref() {
5416 Expr::Test(test) => match test.as_ref() {
5417 TestExpr::Comparison { op: right_op, .. } => {
5418 assert_eq!(*right_op, TestCmpOp::NumGt);
5419 }
5420 other => panic!("expected comparison, got {:?}", other),
5421 },
5422 other => panic!("expected test, got {:?}", other),
5423 }
5424 }
5425 other => panic!("expected binary op, got {:?}", other),
5426 },
5427 other => panic!("expected if, got {:?}", other),
5428 }
5429 }
5430
5431 #[test]
5437 fn script_level1_linear() {
5438 let script = r#"
5439NAME="kaish"
5440VERSION=1
5441TIMEOUT=30
5442ITEMS="alpha beta gamma"
5443
5444echo "Starting ${NAME} v${VERSION}"
5445cat "README.md" | grep pattern="install" | head count=5
5446fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
5447echo "Items: ${ITEMS}"
5448"#;
5449 let result = parse(script).unwrap();
5450 let stmts: Vec<_> = result.statements.iter()
5451 .filter(|s| !matches!(s, Stmt::Empty))
5452 .collect();
5453
5454 assert_eq!(stmts.len(), 8);
5455 assert!(matches!(stmts[0], Stmt::Assignment(_))); assert!(matches!(stmts[1], Stmt::Assignment(_))); assert!(matches!(stmts[2], Stmt::Assignment(_))); assert!(matches!(stmts[3], Stmt::Assignment(_))); assert!(matches!(stmts[4], Stmt::Command(_))); assert!(matches!(stmts[5], Stmt::Pipeline(_))); assert!(matches!(stmts[6], Stmt::Pipeline(_))); assert!(matches!(stmts[7], Stmt::Command(_))); }
5464
5465 #[test]
5467 fn script_level2_branching() {
5468 let script = r#"
5469RESULT=$(kaish-validate "input.json")
5470
5471if [[ ${RESULT.ok} == true ]]; then
5472 echo "Validation passed"
5473 process "input.json" > "output.json"
5474else
5475 echo "Validation failed: ${RESULT.err}"
5476fi
5477
5478if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
5479 echo "Count in valid range"
5480fi
5481
5482if check-network || check-cache; then
5483 fetch url=${URL}
5484fi
5485"#;
5486 let result = parse(script).unwrap();
5487 let stmts: Vec<_> = result.statements.iter()
5488 .filter(|s| !matches!(s, Stmt::Empty))
5489 .collect();
5490
5491 assert_eq!(stmts.len(), 4);
5492
5493 match stmts[0] {
5495 Stmt::Assignment(a) => {
5496 assert_eq!(a.name(), "RESULT");
5497 assert!(matches!(&a.value, Expr::CommandSubst(_)));
5498 }
5499 other => panic!("expected assignment, got {:?}", other),
5500 }
5501
5502 match stmts[1] {
5504 Stmt::If(if_stmt) => {
5505 assert_eq!(if_stmt.then_branch.len(), 2);
5506 assert!(if_stmt.else_branch.is_some());
5507 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
5508 }
5509 other => panic!("expected if, got {:?}", other),
5510 }
5511
5512 match stmts[2] {
5514 Stmt::If(if_stmt) => {
5515 match if_stmt.condition.as_ref() {
5516 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
5517 other => panic!("expected && condition, got {:?}", other),
5518 }
5519 }
5520 other => panic!("expected if, got {:?}", other),
5521 }
5522
5523 match stmts[3] {
5525 Stmt::If(if_stmt) => {
5526 match if_stmt.condition.as_ref() {
5527 Expr::BinaryOp { op, left, right } => {
5528 assert_eq!(*op, BinaryOp::Or);
5529 assert!(matches!(left.as_ref(), Expr::Command(_)));
5530 assert!(matches!(right.as_ref(), Expr::Command(_)));
5531 }
5532 other => panic!("expected || condition, got {:?}", other),
5533 }
5534 }
5535 other => panic!("expected if, got {:?}", other),
5536 }
5537 }
5538
5539 #[test]
5541 fn script_level3_loops_and_functions() {
5542 let script = r#"
5543greet() {
5544 echo "Hello, $1!"
5545}
5546
5547fetch_all() {
5548 for URL in $@; do
5549 fetch url=${URL}
5550 done
5551}
5552
5553USERS="alice bob charlie"
5554
5555for USER in ${USERS}; do
5556 greet ${USER}
5557 if [[ ${USER} == "bob" ]]; then
5558 echo "Found Bob!"
5559 fi
5560done
5561
5562long-running-task &
5563"#;
5564 let result = parse(script).unwrap();
5565 let stmts: Vec<_> = result.statements.iter()
5566 .filter(|s| !matches!(s, Stmt::Empty))
5567 .collect();
5568
5569 assert_eq!(stmts.len(), 5);
5570
5571 match stmts[0] {
5573 Stmt::ToolDef(t) => {
5574 assert_eq!(t.name, "greet");
5575 assert!(t.params.is_empty());
5576 }
5577 other => panic!("expected function def, got {:?}", other),
5578 }
5579
5580 match stmts[1] {
5582 Stmt::ToolDef(t) => {
5583 assert_eq!(t.name, "fetch_all");
5584 assert_eq!(t.body.len(), 1);
5585 assert!(matches!(&t.body[0], Stmt::For(_)));
5586 }
5587 other => panic!("expected function def, got {:?}", other),
5588 }
5589
5590 assert!(matches!(stmts[2], Stmt::Assignment(_)));
5592
5593 match stmts[3] {
5595 Stmt::For(f) => {
5596 assert_eq!(f.variable, "USER");
5597 assert_eq!(f.body.len(), 2);
5598 assert!(matches!(&f.body[0], Stmt::Command(_)));
5599 assert!(matches!(&f.body[1], Stmt::If(_)));
5600 }
5601 other => panic!("expected for loop, got {:?}", other),
5602 }
5603
5604 match stmts[4] {
5606 Stmt::Pipeline(p) => {
5607 assert!(p.background);
5608 assert_eq!(pipeline_commands(p)[0].name, "long-running-task");
5609 }
5610 other => panic!("expected pipeline (background), got {:?}", other),
5611 }
5612 }
5613
5614 #[test]
5616 fn script_level4_complex_nesting() {
5617 let script = r#"
5618RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
5619
5620if ping host=${HOST} && [[ ${RESULT} == true ]]; then
5621 for SERVER in "prod-1 prod-2"; do
5622 deploy target=${SERVER} port=8080
5623 if [[ $? -ne 0 ]]; then
5624 notify channel="ops" message="Deploy failed"
5625 fi
5626 done
5627fi
5628"#;
5629 let result = parse(script).unwrap();
5630 let stmts: Vec<_> = result.statements.iter()
5631 .filter(|s| !matches!(s, Stmt::Empty))
5632 .collect();
5633
5634 assert_eq!(stmts.len(), 2);
5635
5636 match stmts[0] {
5638 Stmt::Assignment(a) => {
5639 assert_eq!(a.name(), "RESULT");
5640 assert_eq!(pipeline_commands(subst_pipeline(&a.value)).len(), 3);
5641 }
5642 other => panic!("expected assignment, got {:?}", other),
5643 }
5644
5645 match stmts[1] {
5647 Stmt::If(if_stmt) => {
5648 match if_stmt.condition.as_ref() {
5649 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
5650 other => panic!("expected && condition, got {:?}", other),
5651 }
5652 assert_eq!(if_stmt.then_branch.len(), 1);
5653 match &if_stmt.then_branch[0] {
5654 Stmt::For(f) => {
5655 assert_eq!(f.body.len(), 2);
5656 assert!(matches!(&f.body[1], Stmt::If(_)));
5657 }
5658 other => panic!("expected for in if body, got {:?}", other),
5659 }
5660 }
5661 other => panic!("expected if, got {:?}", other),
5662 }
5663 }
5664
5665 #[test]
5667 fn script_level5_edge_cases() {
5668 let script = r#"
5669echo ""
5670echo "quotes: \"nested\" here"
5671echo "escapes: \n\t\r\\"
5672echo "unicode: \u2764"
5673
5674X=-99999
5675Y=3.14159265358979
5676Z=-0.001
5677
5678cmd a=1 b="two" c=true d=false e=null
5679
5680if true; then
5681 if false; then
5682 echo "inner"
5683 else
5684 echo "else"
5685 fi
5686fi
5687
5688for I in "a b c"; do
5689 echo ${I}
5690done
5691
5692no_params() {
5693 echo "no params"
5694}
5695
5696function all_args {
5697 echo "args: $@"
5698}
5699
5700a | b | c | d | e &
5701cmd 2> "errors.log"
5702cmd &> "all.log"
5703cmd >> "append.log"
5704cmd < "input.txt"
5705"#;
5706 let result = parse(script).unwrap();
5707 let stmts: Vec<_> = result.statements.iter()
5708 .filter(|s| !matches!(s, Stmt::Empty))
5709 .collect();
5710
5711 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
5713
5714 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
5716 assert!(bg_stmt.is_some(), "expected background pipeline");
5717
5718 match bg_stmt.unwrap() {
5719 Stmt::Pipeline(p) => {
5720 assert_eq!(pipeline_commands(p).len(), 5);
5721 assert!(p.background);
5722 }
5723 _ => unreachable!(),
5724 }
5725 }
5726
5727 #[test]
5732 fn parse_keyword_as_variable_rejected() {
5733 let result = parse(r#"if="value""#);
5736 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
5737
5738 let result = parse("while=true");
5739 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
5740
5741 let result = parse(r#"then="next""#);
5742 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
5743 }
5744
5745 #[test]
5746 fn parse_set_command_with_flag() {
5747 let result = parse("set -e");
5748 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
5749 let program = result.unwrap();
5750 match &program.statements[0] {
5751 Stmt::Command(cmd) => {
5752 assert_eq!(cmd.name, "set");
5753 assert_eq!(cmd.args.len(), 1);
5754 match &cmd.args[0] {
5755 Arg::ShortFlag(f) => assert_eq!(f, "e"),
5756 other => panic!("expected ShortFlag, got {:?}", other),
5757 }
5758 }
5759 other => panic!("expected Command, got {:?}", other),
5760 }
5761 }
5762
5763 #[test]
5764 fn parse_set_command_no_args() {
5765 let result = parse("set");
5766 assert!(result.is_ok(), "failed to parse set: {:?}", result);
5767 let program = result.unwrap();
5768 match &program.statements[0] {
5769 Stmt::Command(cmd) => {
5770 assert_eq!(cmd.name, "set");
5771 assert_eq!(cmd.args.len(), 0);
5772 }
5773 other => panic!("expected Command, got {:?}", other),
5774 }
5775 }
5776
5777 #[test]
5778 fn parse_set_assignment_vs_command() {
5779 let result = parse("X=5");
5781 assert!(result.is_ok());
5782 let program = result.unwrap();
5783 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
5784
5785 let result = parse("set -e");
5787 assert!(result.is_ok());
5788 let program = result.unwrap();
5789 assert!(matches!(&program.statements[0], Stmt::Command(_)));
5790 }
5791
5792 #[test]
5793 fn parse_true_as_command() {
5794 let result = parse("true");
5795 assert!(result.is_ok());
5796 let program = result.unwrap();
5797 match &program.statements[0] {
5798 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
5799 other => panic!("expected Command(true), got {:?}", other),
5800 }
5801 }
5802
5803 #[test]
5804 fn parse_false_as_command() {
5805 let result = parse("false");
5806 assert!(result.is_ok());
5807 let program = result.unwrap();
5808 match &program.statements[0] {
5809 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
5810 other => panic!("expected Command(false), got {:?}", other),
5811 }
5812 }
5813
5814 #[test]
5815 fn parse_dot_as_source_alias() {
5816 let result = parse(". script.kai");
5817 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
5818 let program = result.unwrap();
5819 match &program.statements[0] {
5820 Stmt::Command(cmd) => {
5821 assert_eq!(cmd.name, ".");
5822 assert_eq!(cmd.args.len(), 1);
5823 }
5824 other => panic!("expected Command(.), got {:?}", other),
5825 }
5826 }
5827
5828 #[test]
5829 fn parse_source_command() {
5830 let result = parse("source utils.kai");
5831 assert!(result.is_ok(), "failed to parse source: {:?}", result);
5832 let program = result.unwrap();
5833 match &program.statements[0] {
5834 Stmt::Command(cmd) => {
5835 assert_eq!(cmd.name, "source");
5836 assert_eq!(cmd.args.len(), 1);
5837 }
5838 other => panic!("expected Command(source), got {:?}", other),
5839 }
5840 }
5841
5842 #[test]
5843 fn parse_test_expr_file_test() {
5844 let result = parse(r#"[[ -f "/path/file" ]]"#);
5846 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
5847 }
5848
5849 #[test]
5850 fn parse_test_expr_comparison() {
5851 let result = parse(r#"[[ $X == "value" ]]"#);
5852 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
5853 }
5854
5855 #[test]
5856 fn parse_test_expr_single_eq() {
5857 let result = parse(r#"[[ $X = "value" ]]"#);
5859 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
5860 let program = result.unwrap();
5861 match &program.statements[0] {
5862 Stmt::Test(TestExpr::Comparison { op, .. }) => {
5863 assert_eq!(op, &TestCmpOp::Eq);
5864 }
5865 other => panic!("expected Test(Comparison), got {:?}", other),
5866 }
5867 }
5868
5869 #[test]
5870 fn parse_while_loop() {
5871 let result = parse("while true; do echo; done");
5872 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
5873 let program = result.unwrap();
5874 assert!(matches!(&program.statements[0], Stmt::While(_)));
5875 }
5876
5877 #[test]
5878 fn parse_break_with_level() {
5879 let result = parse("break 2");
5880 assert!(result.is_ok());
5881 let program = result.unwrap();
5882 match &program.statements[0] {
5883 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
5884 other => panic!("expected Break(2), got {:?}", other),
5885 }
5886 }
5887
5888 #[test]
5889 fn parse_continue_with_level() {
5890 let result = parse("continue 3");
5891 assert!(result.is_ok());
5892 let program = result.unwrap();
5893 match &program.statements[0] {
5894 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
5895 other => panic!("expected Continue(3), got {:?}", other),
5896 }
5897 }
5898
5899 #[test]
5900 fn parse_exit_with_code() {
5901 let result = parse("exit 1");
5902 assert!(result.is_ok());
5903 let program = result.unwrap();
5904 match &program.statements[0] {
5905 Stmt::Exit(Some(expr)) => {
5906 match expr.as_ref() {
5907 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
5908 other => panic!("expected Int(1), got {:?}", other),
5909 }
5910 }
5911 other => panic!("expected Exit(1), got {:?}", other),
5912 }
5913 }
5914
5915 #[test]
5922 fn spanned_literal_only_records_byte_range() {
5923 let parts = parse_interpolated_string_spanned("hello world", 100).unwrap();
5924 assert_eq!(parts.len(), 1);
5925 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
5926 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
5927 assert_eq!(parts[0].len, 11);
5928 }
5929
5930 #[test]
5931 fn spanned_braced_var_at_zero() {
5932 let parts = parse_interpolated_string_spanned("${X}", 50).unwrap();
5933 assert_eq!(parts.len(), 1);
5934 assert!(matches!(&parts[0].part, StringPart::Var(_)));
5935 assert_eq!(parts[0].offset, 50);
5936 assert_eq!(parts[0].len, 4); }
5938
5939 #[test]
5940 fn spanned_simple_var_then_literal() {
5941 let parts = parse_interpolated_string_spanned("$X end", 10).unwrap();
5942 assert_eq!(parts.len(), 2);
5943 assert!(matches!(&parts[0].part, StringPart::Var(_)));
5944 assert_eq!(parts[0].offset, 10);
5945 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
5947 assert_eq!(parts[1].offset, 12);
5948 assert_eq!(parts[1].len, 4);
5949 }
5950
5951 #[test]
5952 fn spanned_mixed_literal_var_literal() {
5953 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0).unwrap();
5954 assert_eq!(parts.len(), 3);
5955 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
5957 assert_eq!(parts[0].offset, 0);
5958 assert_eq!(parts[0].len, 3);
5959 assert!(matches!(&parts[1].part, StringPart::Var(_)));
5961 assert_eq!(parts[1].offset, 3);
5962 assert_eq!(parts[1].len, 4);
5963 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
5965 assert_eq!(parts[2].offset, 7);
5966 assert_eq!(parts[2].len, 4);
5967 }
5968
5969 #[test]
5970 fn spanned_positional_param() {
5971 let parts = parse_interpolated_string_spanned("$1 done", 0).unwrap();
5972 assert_eq!(parts.len(), 2);
5973 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
5974 assert_eq!(parts[0].offset, 0);
5975 assert_eq!(parts[0].len, 2); }
5977
5978 #[test]
5979 fn spanned_special_dollar_dollar() {
5980 let parts = parse_interpolated_string_spanned("$$", 5).unwrap();
5981 assert_eq!(parts.len(), 1);
5982 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
5983 assert_eq!(parts[0].offset, 5);
5984 assert_eq!(parts[0].len, 2);
5985 }
5986
5987 #[test]
5988 fn spanned_arithmetic_marker_recognised() {
5989 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0).unwrap();
5993 assert_eq!(parts.len(), 1);
5994 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
5995 }
5996
5997 #[test]
5998 fn spanned_default_separator_yields_var_with_default() {
5999 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0).unwrap();
6000 assert_eq!(parts.len(), 1);
6001 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
6002 assert_eq!(parts[0].offset, 0);
6003 assert_eq!(parts[0].len, 14); }
6005
6006 #[test]
6007 fn spanned_no_dollar_runs_one_literal() {
6008 let parts = parse_interpolated_string_spanned("plain text only", 7).unwrap();
6009 assert_eq!(parts.len(), 1);
6010 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
6011 assert_eq!(parts[0].offset, 7);
6012 assert_eq!(parts[0].len, 15);
6013 }
6014
6015 #[test]
6016 fn spanned_matches_unspanned_part_count() {
6017 let cases = [
6020 "hello",
6021 "$X",
6022 "${X}",
6023 "${X:-d}",
6024 "hi $A and $B",
6025 "$0 $1 $2",
6026 "$$ $? $#",
6027 ];
6028 for s in &cases {
6029 let unspanned = parse_interpolated_string(s).expect("test input parses");
6030 let spanned = parse_interpolated_string_spanned(s, 0).unwrap();
6031 assert_eq!(
6032 unspanned.len(),
6033 spanned.len(),
6034 "part count differs for {:?}",
6035 s
6036 );
6037 }
6038 }
6039
6040 #[test]
6041 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
6042 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0).unwrap();
6047 assert_eq!(parts.len(), 2);
6048
6049 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
6050 assert_eq!(parts[0].offset, 0);
6051 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
6052
6053 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6054 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
6055 assert_eq!(parts[1].len, 4);
6056 }
6057
6058 #[test]
6059 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
6060 let parts = parse_interpolated_string_spanned("hello 世界 world", 0).unwrap();
6063 assert_eq!(parts.len(), 1);
6064 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
6065 assert_eq!(parts[0].offset, 0);
6066 assert_eq!(parts[0].len, 18);
6067 }
6068
6069 #[test]
6070 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
6071 let parts = parse_interpolated_string_spanned("\\$", 0).unwrap();
6074 assert_eq!(parts.len(), 1);
6075 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
6076 assert_eq!(parts[0].offset, 0);
6077 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
6078 }
6079
6080 #[test]
6081 fn spanned_escape_backslash_collapses_pair_to_one() {
6082 let parts = parse_interpolated_string_spanned("\\\\", 0).unwrap();
6083 assert_eq!(parts.len(), 1);
6084 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
6085 assert_eq!(parts[0].len, 2);
6086 }
6087
6088 #[test]
6089 fn spanned_standalone_cr_continuation_realigns_span_start() {
6090 let parts = parse_interpolated_string_spanned("\\\rCD${x}", 0).unwrap();
6098 assert_eq!(parts.len(), 2);
6099 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "CD"));
6100 assert_eq!(parts[0].offset, 2, "literal run must start after the consumed \\+CR");
6101 assert_eq!(parts[0].len, 2);
6102 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6103 assert_eq!(parts[1].offset, 4);
6104 assert_eq!(parts[1].len, 4); }
6106
6107 #[test]
6108 fn spanned_standalone_cr_continuation_mid_run_keeps_span_start() {
6109 let parts = parse_interpolated_string_spanned("AB\\\rCD${x}", 0).unwrap();
6114 assert_eq!(parts.len(), 2);
6115 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "ABCD"));
6116 assert_eq!(parts[0].offset, 0);
6117 assert_eq!(parts[0].len, 6); assert!(matches!(&parts[1].part, StringPart::Var(_)));
6119 assert_eq!(parts[1].offset, 6);
6120 assert_eq!(parts[1].len, 4); }
6122
6123 fn assignment_value(source: &str) -> Expr {
6127 let program = parse(source).unwrap_or_else(|e| panic!("parse {source:?}: {e:?}"));
6128 match program.statements.as_slice() {
6129 [Stmt::Assignment(a)] => a.value.clone(),
6130 other => panic!("expected a single assignment, got {other:?}"),
6131 }
6132 }
6133
6134 #[test]
6135 fn list_literal_three_elements() {
6136 let expr = assignment_value("xs=[a b c]");
6137 match expr {
6138 Expr::ListLiteral(elems) => {
6139 assert_eq!(elems.len(), 3);
6140 assert!(elems.iter().all(|e| matches!(e, ListElem::Item(_))));
6141 }
6142 other => panic!("expected ListLiteral, got {other:?}"),
6143 }
6144 }
6145
6146 #[test]
6147 fn list_literal_empty() {
6148 let expr = assignment_value("xs=[]");
6149 assert!(matches!(expr, Expr::ListLiteral(elems) if elems.is_empty()));
6150 }
6151
6152 #[test]
6153 fn list_literal_single_glued_dog() {
6154 let expr = assignment_value("xs=[dog]");
6158 match expr {
6159 Expr::ListLiteral(elems) => assert_eq!(elems.len(), 1),
6160 other => panic!("expected ListLiteral, got {other:?}"),
6161 }
6162 }
6163
6164 #[test]
6165 fn list_literal_single_int() {
6166 let expr = assignment_value("xs=[1]");
6167 match expr {
6168 Expr::ListLiteral(elems) => match elems.as_slice() {
6169 [ListElem::Item(Expr::Literal(Value::Int(1)))] => {}
6170 other => panic!("expected one Int(1) item, got {other:?}"),
6171 },
6172 other => panic!("expected ListLiteral, got {other:?}"),
6173 }
6174 }
6175
6176 #[test]
6177 fn record_literal_unspaced_colon_equals_spaced() {
6178 let spaced = assignment_value("x={port: 8080}");
6179 let unspaced = assignment_value("x={port:8080}");
6180 assert_eq!(spaced, unspaced, "{{port:8080}} must parse identically to {{port: 8080}}");
6181 match spaced {
6182 Expr::RecordLiteral(entries) => match entries.as_slice() {
6183 [RecordEntry { key: RecordKey::Bare(k), value: Expr::Literal(Value::Int(8080)) }] => {
6184 assert_eq!(k, "port");
6185 }
6186 other => panic!("expected one port:8080 entry, got {other:?}"),
6187 },
6188 other => panic!("expected RecordLiteral, got {other:?}"),
6189 }
6190 }
6191
6192 #[test]
6193 fn record_literal_name_role() {
6194 let expr = assignment_value("u={name: amy, role: maintainer}");
6195 match expr {
6196 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2),
6197 other => panic!("expected RecordLiteral, got {other:?}"),
6198 }
6199 }
6200
6201 #[test]
6202 fn record_literal_multiline_trailing_comma() {
6203 let source = "services={\n web: {port: 8080, replicas: 3, healthy: true},\n api: {port: 9000, replicas: 2, healthy: false},\n}";
6204 let expr = assignment_value(source);
6205 match expr {
6206 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2, "web + api entries"),
6207 other => panic!("expected RecordLiteral, got {other:?}"),
6208 }
6209 }
6210
6211 #[test]
6212 fn record_literal_quoted_key() {
6213 let expr = assignment_value(r#"r={"content-type": x}"#);
6214 match expr {
6215 Expr::RecordLiteral(entries) => match entries.as_slice() {
6216 [RecordEntry { key: RecordKey::Quoted(k), .. }] => assert_eq!(k, "content-type"),
6217 other => panic!("expected one quoted-key entry, got {other:?}"),
6218 },
6219 other => panic!("expected RecordLiteral, got {other:?}"),
6220 }
6221 }
6222
6223 #[test]
6224 fn nested_list_and_record_in_record() {
6225 let expr = assignment_value("x={tags: [a b], meta: {active: true}}");
6226 match expr {
6227 Expr::RecordLiteral(entries) => {
6228 assert_eq!(entries.len(), 2);
6229 assert!(matches!(entries[0].value, Expr::ListLiteral(_)));
6230 assert!(matches!(entries[1].value, Expr::RecordLiteral(_)));
6231 }
6232 other => panic!("expected RecordLiteral, got {other:?}"),
6233 }
6234 }
6235
6236 #[test]
6237 fn spread_and_item_elements() {
6238 let expr = assignment_value("new=[...$xs date]");
6239 match expr {
6240 Expr::ListLiteral(elems) => match elems.as_slice() {
6241 [ListElem::Spread(Expr::VarRef(_)), ListElem::Item(Expr::Literal(Value::String(s)))] => {
6242 assert_eq!(s, "date");
6243 }
6244 other => panic!("expected [Spread($xs), Item(date)], got {other:?}"),
6245 },
6246 other => panic!("expected ListLiteral, got {other:?}"),
6247 }
6248 }
6249
6250 #[test]
6251 fn spread_of_two_variables() {
6252 let expr = assignment_value("c=[...$a ...$b]");
6253 match expr {
6254 Expr::ListLiteral(elems) => {
6255 assert_eq!(elems.len(), 2);
6256 assert!(elems.iter().all(|e| matches!(e, ListElem::Spread(_))));
6257 }
6258 other => panic!("expected ListLiteral, got {other:?}"),
6259 }
6260 }
6261
6262 #[test]
6263 fn in_rhs_accepts_a_list_literal() {
6264 let program = parse("if [[ $a not in [dog] ]]; then echo hit; fi")
6265 .unwrap_or_else(|e| panic!("parse: {e:?}"));
6266 assert_eq!(program.statements.len(), 1);
6267 }
6268
6269 #[test]
6270 fn multiword_bareword_record_value_is_a_parse_error() {
6271 assert!(parse("x={msg: hello world}").is_err());
6274 }
6275
6276 #[test]
6279 fn argv_bracket_glob_stays_a_glob_pattern() {
6280 let program = parse("ls [dog]").unwrap_or_else(|e| panic!("parse: {e:?}"));
6284 assert_eq!(program.statements.len(), 1);
6285 }
6286
6287 #[test]
6288 fn brace_expansion_at_argv_position_is_unaffected() {
6289 let program = parse("cmd *.{rs,go}").unwrap_or_else(|e| panic!("parse: {e:?}"));
6294 assert_eq!(program.statements.len(), 1);
6295 }
6296
6297 #[test]
6298 fn for_head_item_is_not_a_literal() {
6299 let program = parse("for x in [a]; do echo $x; done")
6302 .unwrap_or_else(|e| panic!("parse: {e:?}"));
6303 match program.statements.as_slice() {
6304 [Stmt::For(for_loop)] => {
6305 assert_eq!(for_loop.items.len(), 1);
6306 assert!(
6307 !matches!(for_loop.items[0], Expr::ListLiteral(_)),
6308 "for-head item must not be a ListLiteral: {:?}",
6309 for_loop.items[0]
6310 );
6311 }
6312 other => panic!("expected a single For statement, got {other:?}"),
6313 }
6314 }
6315
6316 #[derive(Debug, Clone, Copy)]
6322 enum NestingLayer {
6323 CmdSubst,
6325 QuotedCmdSubst,
6327 Case,
6329 If,
6330 For,
6331 }
6332
6333 fn wrap_in_layer(inner: &str, layer: NestingLayer) -> String {
6334 match layer {
6335 NestingLayer::CmdSubst => format!("x=$({inner})"),
6336 NestingLayer::QuotedCmdSubst => format!("x=\"pre $({inner}) post\""),
6337 NestingLayer::Case => format!("case v in v) {inner};; esac"),
6338 NestingLayer::If => format!("if true; then {inner}; fi"),
6339 NestingLayer::For => format!("for f in a; do {inner}; done"),
6340 }
6341 }
6342
6343 proptest::proptest! {
6344 #[test]
6362 fn nested_compound_constructs_always_parse(
6363 layers in proptest::collection::vec(
6364 proptest::prop_oneof![
6365 proptest::strategy::Just(NestingLayer::CmdSubst),
6366 proptest::strategy::Just(NestingLayer::QuotedCmdSubst),
6367 proptest::strategy::Just(NestingLayer::Case),
6368 proptest::strategy::Just(NestingLayer::If),
6369 proptest::strategy::Just(NestingLayer::For),
6370 ],
6371 1..=4,
6372 ).prop_filter("at most one QuotedCmdSubst layer", |layers| {
6373 layers.iter().filter(|l| matches!(l, NestingLayer::QuotedCmdSubst)).count() <= 1
6374 })
6375 ) {
6376 let source = layers
6377 .iter()
6378 .fold("echo x".to_string(), |inner, &layer| wrap_in_layer(&inner, layer));
6379 proptest::prop_assert!(
6380 parse(&source).is_ok(),
6381 "grammar-nested construct failed to parse: {source:?}"
6382 );
6383 }
6384 }
6385}