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 if find_default_separator_in_content(name).is_some() {
721 return Err(format!(
722 "${{#{name}}}: a length cannot carry a default — \
723 ${{#NAME}} counts, ${{NAME:-default}} substitutes. \
724 Write ${{#NAME}} on a name you have set, or test it \
725 first."
726 ));
727 }
728 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
729 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
730 let expr = var_content
731 .strip_prefix("__ARITH:")
732 .and_then(|s| s.strip_suffix("__"))
733 .unwrap_or("");
734 StringPart::Arithmetic(expr.to_string())
735 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
736 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
737 let default_str = &var_content[colon_idx + 2..];
738 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
749 StringPart::VarWithDefault { path, default }
750 } else if let Some(msg) = bash_substring_hint(&var_content) {
751 return Err(msg);
752 } else {
753 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
754 };
755 parts.push(SpannedPart {
756 part,
757 offset: base_offset + part_start,
758 len: pos - part_start,
759 });
760 current_text_start = pos;
761 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
762 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
763 i += 1; pos += 1;
765 if let Some(&digit) = chars_vec.get(i) {
766 let n = digit.to_digit(10).unwrap_or(0) as usize;
767 i += 1;
768 pos += digit.len_utf8();
769 parts.push(SpannedPart {
770 part: StringPart::Positional(n),
771 offset: base_offset + part_start,
772 len: pos - part_start,
773 });
774 }
775 current_text_start = pos;
776 } else if next == Some('@') {
777 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
778 i += 2; pos += 2;
780 parts.push(SpannedPart {
781 part: StringPart::AllArgs,
782 offset: base_offset + part_start,
783 len: pos - part_start,
784 });
785 current_text_start = pos;
786 } else if next == Some('#') {
787 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
788 i += 2; pos += 2;
790 parts.push(SpannedPart {
791 part: StringPart::ArgCount,
792 offset: base_offset + part_start,
793 len: pos - part_start,
794 });
795 current_text_start = pos;
796 } else if next == Some('?') {
797 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
798 i += 2; pos += 2;
800 parts.push(SpannedPart {
801 part: StringPart::LastExitCode,
802 offset: base_offset + part_start,
803 len: pos - part_start,
804 });
805 current_text_start = pos;
806 } else if next == Some('$') {
807 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
808 i += 2; pos += 2;
810 parts.push(SpannedPart {
811 part: StringPart::CurrentPid,
812 offset: base_offset + part_start,
813 len: pos - part_start,
814 });
815 current_text_start = pos;
816 } else if next.map(is_name_start).unwrap_or(false) {
817 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
818 i += 1; pos += 1;
820 let mut var_name = String::new();
821 while let Some(&c) = chars_vec.get(i) {
822 if is_name_char(c) {
823 var_name.push(c);
824 i += 1;
825 pos += c.len_utf8();
826 } else {
827 break;
828 }
829 }
830 parts.push(SpannedPart {
831 part: StringPart::Var(VarPath::simple(var_name)),
832 offset: base_offset + part_start,
833 len: pos - part_start,
834 });
835 current_text_start = pos;
836 } else {
837 if current_text.is_empty() {
839 current_text_start = pos;
840 }
841 current_text.push(ch);
842 i += 1;
843 pos += 1;
844 }
845 } else {
846 if current_text.is_empty() {
847 current_text_start = pos;
848 }
849 current_text.push(ch);
850 i += 1;
851 pos += ch.len_utf8();
852 }
853 }
854
855 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
856
857 Ok(parts)
858}
859
860fn parse_interpolated_string(s: &str) -> Result<Vec<StringPart>, String> {
861 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
864
865 let mut parts = Vec::new();
866 let mut current_text = String::new();
867 let mut chars = s.chars().peekable();
868
869 while let Some(ch) = chars.next() {
870 if ch == '\x00' {
871 let mut marker = String::new();
873 while let Some(&c) = chars.peek() {
874 if c == '\x00' {
875 chars.next(); break;
877 }
878 if let Some(c) = chars.next() {
879 marker.push(c);
880 }
881 }
882 if marker == "DOLLAR" {
883 current_text.push('$');
884 }
885 } else if ch == '$' {
886 if chars.peek() == Some(&'(') {
888 if !current_text.is_empty() {
890 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
891 }
892
893 chars.next();
895
896 let remainder: String = chars.clone().collect();
906 let close = lexer::tokenize(&remainder).ok().and_then(|toks| {
907 let toks: Vec<(Token, Span)> = toks
908 .into_iter()
909 .map(|sp| (sp.token, (sp.span.start..sp.span.end).into()))
910 .collect();
911 find_cmd_subst_close(&toks).map(|idx| toks[idx].1)
912 });
913 let Some(rparen_span) = close else {
920 return Err("unterminated command substitution: missing `)`".to_string());
921 };
922 let (cmd_content, consume_bytes) =
923 (remainder[..rparen_span.start].to_string(), rparen_span.end);
924 let mut consumed = 0usize;
925 while consumed < consume_bytes {
926 match chars.next() {
927 Some(c) => consumed += c.len_utf8(),
928 None => break,
929 }
930 }
931
932 match parse(&cmd_content) {
935 Ok(program) => {
936 let stmts = strip_empty_stmts(program.statements);
937 if stmts.is_empty() {
938 current_text.push_str("$(");
941 current_text.push_str(&cmd_content);
942 current_text.push(')');
943 } else {
944 parts.push(StringPart::CommandSubst(stmts));
945 }
946 }
947 Err(_) => {
948 return Err(format!(
952 "syntax error in command substitution: $({cmd_content})"
953 ));
954 }
955 }
956 } else if chars.peek() == Some(&'{') {
957 if !current_text.is_empty() {
959 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
960 }
961
962 chars.next();
964
965 let mut var_content = String::new();
967 let mut depth = 1;
968 for c in chars.by_ref() {
969 if c == '{' && var_content.ends_with('$') {
970 depth += 1;
971 var_content.push(c);
972 } else if c == '}' {
973 depth -= 1;
974 if depth == 0 {
975 break;
976 }
977 var_content.push(c);
978 } else {
979 var_content.push(c);
980 }
981 }
982
983 let part = if let Some(name) = var_content.strip_prefix('#') {
985 if find_default_separator_in_content(name).is_some() {
993 return Err(format!(
994 "${{#{name}}}: a length cannot carry a default — \
995 ${{#NAME}} counts, ${{NAME:-default}} substitutes. \
996 Write ${{#NAME}} on a name you have set, or test it \
997 first."
998 ));
999 }
1000 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
1001 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
1002 let expr = var_content
1004 .strip_prefix("__ARITH:")
1005 .and_then(|s| s.strip_suffix("__"))
1006 .unwrap_or("");
1007 StringPart::Arithmetic(expr.to_string())
1008 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
1009 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
1011 let default_str = &var_content[colon_idx + 2..];
1012 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
1013 StringPart::VarWithDefault { path, default }
1014 } else if let Some(msg) = bash_substring_hint(&var_content) {
1015 return Err(msg);
1016 } else {
1017 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
1019 };
1020 parts.push(part);
1021 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
1022 if !current_text.is_empty() {
1024 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1025 }
1026 if let Some(digit) = chars.next() {
1027 let n = digit.to_digit(10).unwrap_or(0) as usize;
1028 parts.push(StringPart::Positional(n));
1029 }
1030 } else if chars.peek() == Some(&'@') {
1031 if !current_text.is_empty() {
1033 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1034 }
1035 chars.next(); parts.push(StringPart::AllArgs);
1037 } else if chars.peek() == Some(&'#') {
1038 if !current_text.is_empty() {
1040 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1041 }
1042 chars.next(); parts.push(StringPart::ArgCount);
1044 } else if chars.peek() == Some(&'?') {
1045 if !current_text.is_empty() {
1047 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1048 }
1049 chars.next(); parts.push(StringPart::LastExitCode);
1051 } else if chars.peek() == Some(&'$') {
1052 if !current_text.is_empty() {
1054 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1055 }
1056 chars.next(); parts.push(StringPart::CurrentPid);
1058 } else if chars.peek().copied().map(is_name_start).unwrap_or(false) {
1059 if !current_text.is_empty() {
1061 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1062 }
1063
1064 let mut var_name = String::new();
1066 while let Some(&c) = chars.peek() {
1067 if is_name_char(c) {
1068 if let Some(c) = chars.next() {
1069 var_name.push(c);
1070 }
1071 } else {
1072 break;
1073 }
1074 }
1075
1076 parts.push(StringPart::Var(VarPath::simple(var_name)));
1077 } else {
1078 current_text.push(ch);
1080 }
1081 } else {
1082 current_text.push(ch);
1083 }
1084 }
1085
1086 if !current_text.is_empty() {
1087 parts.push(StringPart::Literal(current_text));
1088 }
1089
1090 Ok(parts)
1091}
1092
1093#[derive(Debug, Clone)]
1095pub struct ParseError {
1096 pub span: Span,
1097 pub message: String,
1098}
1099
1100impl ParseError {
1101 pub fn format(&self, source: &str) -> String {
1106 let start = self.span.start;
1107 let mut line = 1usize;
1108 let mut col = 1usize;
1109 for (i, ch) in source.char_indices() {
1110 if i >= start {
1111 break;
1112 }
1113 if ch == '\n' {
1114 line += 1;
1115 col = 1;
1116 } else {
1117 col += 1;
1118 }
1119 }
1120 let line_content = {
1121 let line_start = source[..start.min(source.len())]
1122 .rfind('\n')
1123 .map_or(0, |i| i + 1);
1124 let line_end = source[start.min(source.len())..]
1125 .find('\n')
1126 .map_or(source.len(), |i| start + i);
1127 source.get(line_start..line_end).unwrap_or("")
1128 };
1129 if line_content.is_empty() {
1130 format!("{}:{} [parse]: {}", line, col, self.message)
1131 } else {
1132 format!(
1133 "{}:{} [parse]: {}\n | {}",
1134 line, col, self.message, line_content
1135 )
1136 }
1137 }
1138}
1139
1140impl std::fmt::Display for ParseError {
1141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1142 write!(f, "{} at {:?}", self.message, self.span)
1143 }
1144}
1145
1146impl std::error::Error for ParseError {}
1147
1148pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
1150 let tokens = lexer::tokenize(source).map_err(|errs| {
1152 errs.into_iter()
1153 .map(|e| ParseError {
1154 span: (e.span.start..e.span.end).into(),
1155 message: format!("lexer error: {}", e.token),
1156 })
1157 .collect::<Vec<_>>()
1158 })?;
1159
1160 let tokens: Vec<(Token, Span)> = tokens
1162 .into_iter()
1163 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
1164 .collect();
1165
1166 for (i, (tok, span)) in tokens.iter().enumerate() {
1177 let prev = i.checked_sub(1).and_then(|j| tokens.get(j)).map(|(t, _)| t);
1178 if let Some((name, is_target)) =
1179 name_in_token_kind(tok, prev, tokens.get(i + 1).map(|(t, _)| t))
1180 {
1181 if let Err(bad) = crate::name::validate(name) {
1182 let defer = is_target && matches!(bad.ch, '.' | '#');
1191 if !defer {
1192 return Err(vec![ParseError { span: *span, message: bad.to_string() }]);
1193 }
1194 }
1195 }
1196 if let Token::String(s) = tok {
1202 if !s.is_ascii() && s.contains('$') {
1203 if let Ok(parts) = parse_interpolated_string(s) {
1204 if let Some(bad) = bad_name_in_parts(&parts) {
1205 return Err(vec![ParseError { span: *span, message: bad.to_string() }]);
1206 }
1207 }
1208 }
1209 }
1210 let message = match tok {
1211 Token::VarRef(raw) => raw
1212 .strip_prefix("${")
1213 .and_then(|s| s.strip_suffix('}'))
1214 .filter(|_| find_default_separator(raw).is_none())
1215 .and_then(bash_substring_hint),
1216 Token::String(s) if s.contains("${") && s.contains(':') => {
1220 parse_interpolated_string(s).err()
1221 }
1222 _ => None,
1223 };
1224 if let Some(message) = message {
1225 return Err(vec![ParseError {
1226 span: *span,
1227 message,
1228 }]);
1229 }
1230 }
1231
1232 let end_span: Span = (source.len()..source.len()).into();
1234
1235 parse_tokens(tokens, end_span, (0..0).into())
1236}
1237
1238fn parse_tokens(
1251 tokens: Vec<(Token, Span)>,
1252 end_span: Span,
1253 stdin_anchor: Span,
1254) -> Result<Program, Vec<ParseError>> {
1255 let input = Stream::from_iter(tokens.clone()).map(end_span, keep_pair as PairFn);
1268 let result = CACHED_PARSER.with(|parser| parser.parse(input));
1269
1270 let program = result.into_result().map_err(|errs| {
1271 if let Err(specific) = validate_cmd_subst_bodies(&tokens) {
1279 return specific;
1280 }
1281 if let Err(specific) = validate_interpolated_strings(&tokens) {
1285 return specific;
1286 }
1287 if let Err(specific) = validate_heredoc_bodies(&tokens) {
1289 return specific;
1290 }
1291 errs.into_iter()
1292 .map(|e| ParseError {
1293 span: *e.span(),
1294 message: e.to_string(),
1295 })
1296 .collect::<Vec<_>>()
1297 })?;
1298
1299 if first_ambiguous_stdin(&program.statements) {
1304 return Err(vec![ParseError {
1305 span: stdin_anchor,
1309 message: "multiple stdin redirects on one command are ambiguous; \
1310 use exactly one of `<`, `<<`, or `<<<`"
1311 .to_string(),
1312 }]);
1313 }
1314
1315 Ok(program)
1316}
1317
1318pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
1320 let program = parse(source)?;
1321 program
1322 .statements
1323 .into_iter()
1324 .find(|s| !matches!(s, Stmt::Empty))
1325 .ok_or_else(|| {
1326 vec![ParseError {
1327 span: (0..source.len()).into(),
1328 message: "empty input".to_string(),
1329 }]
1330 })
1331}
1332
1333fn program_parser<'tokens, 'src: 'tokens, I>(
1339) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
1340where
1341 I: ValueInput<'tokens, Token = Token, Span = Span>,
1342{
1343 statement_parser()
1344 .repeated()
1345 .collect::<Vec<_>>()
1346 .map(|statements| Program { statements })
1347}
1348
1349fn statement_parser<'tokens, I>(
1352) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1353where
1354 I: ValueInput<'tokens, Token = Token, Span = Span>,
1355{
1356 recursive(|stmt| {
1357 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
1358
1359 let break_stmt = just(Token::Break)
1361 .ignore_then(
1362 select! { Token::Int(n) => n as usize }.or_not()
1363 )
1364 .map(Stmt::Break);
1365
1366 let continue_stmt = just(Token::Continue)
1368 .ignore_then(
1369 select! { Token::Int(n) => n as usize }.or_not()
1370 )
1371 .map(Stmt::Continue);
1372
1373 let return_stmt = just(Token::Return)
1375 .ignore_then(primary_expr_parser().or_not())
1376 .map(|e| Stmt::Return(e.map(Box::new)));
1377
1378 let exit_stmt = just(Token::Exit)
1380 .ignore_then(primary_expr_parser().or_not())
1381 .map(|e| Stmt::Exit(e.map(Box::new)));
1382
1383 let set_flag_arg = choice((
1392 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
1393 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
1394 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
1396 ));
1397
1398 let option_value_str = select! {
1402 Token::NumberIdent(s) => s,
1403 Token::Int(n) => n.to_string(),
1404 Token::Ident(s) => s,
1405 };
1406
1407 let set_option_assign = ident_parser()
1411 .then_ignore(just(Token::Eq))
1412 .then(option_value_str)
1413 .map(|(name, value)| {
1414 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
1415 });
1416
1417 let set_quoted_arg = select! {
1421 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1422 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1423 };
1424
1425 let set_with_flags = just(Token::Set)
1427 .then(set_flag_arg)
1428 .then(
1429 choice((
1430 set_flag_arg,
1431 set_option_assign,
1433 set_quoted_arg,
1434 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
1436 ))
1437 .repeated()
1438 .collect::<Vec<_>>(),
1439 )
1440 .map(|((_, first_arg), mut rest_args)| {
1441 let mut args = vec![first_arg];
1442 args.append(&mut rest_args);
1443 Stmt::Command(Command {
1444 name: "set".to_string(),
1445 args,
1446 redirects: vec![],
1447 })
1448 });
1449
1450 let set_no_args = just(Token::Set)
1453 .then(
1454 choice((
1455 just(Token::Newline).to(()),
1456 just(Token::Semi).to(()),
1457 just(Token::And).to(()),
1458 just(Token::Or).to(()),
1459 end(),
1460 ))
1461 .rewind(),
1462 )
1463 .map(|_| Stmt::Command(Command {
1464 name: "set".to_string(),
1465 args: vec![],
1466 redirects: vec![],
1467 }));
1468
1469 let set_command = set_with_flags.or(set_no_args);
1473
1474 let env_prefix_assign = ident_parser()
1491 .then_ignore(just(Token::Eq))
1492 .then(value_expr_parser())
1493 .map(|(name, value)| Assignment { path: VarPath::simple(name), value, local: false });
1494 let env_scoped = env_prefix_assign
1495 .repeated()
1496 .at_least(1)
1497 .collect::<Vec<_>>()
1498 .then(pipeline_parser(command_stage_parser()).map(pipeline_into_stmt))
1499 .map(|(assignments, body)| Stmt::EnvScoped {
1500 assignments,
1501 body: Box::new(body),
1502 });
1503
1504 let compound = choice((
1512 if_parser(stmt.clone()).map(Stmt::If),
1513 for_parser(stmt.clone()).map(Stmt::For),
1514 while_parser(stmt.clone()).map(Stmt::While),
1515 case_parser(stmt.clone()).map(Stmt::Case),
1516 ))
1517 .boxed();
1518
1519 let base_statement = choice((
1521 just(Token::Newline).to(Stmt::Empty),
1522 set_command,
1523 env_scoped,
1524 assignment_parser().map(Stmt::Assignment),
1525 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), break_stmt,
1529 continue_stmt,
1530 return_stmt,
1531 exit_stmt,
1532 test_expr_stmt_parser().map(Stmt::Test),
1533 pipeline_parser(choice((
1535 compound.map(|s| PipelineStage::Compound(Box::new(s))),
1536 command_stage_parser(),
1537 )))
1538 .map(pipeline_into_stmt),
1539 ))
1540 .boxed();
1541
1542 base_statement
1548 .clone()
1549 .foldl(
1550 choice((
1551 just(Token::And).to(true), just(Token::Or).to(false), ))
1554 .then(base_statement)
1555 .repeated(),
1556 |left, (is_and, right): (bool, Stmt)| {
1557 if is_and {
1558 Stmt::AndChain {
1559 left: Box::new(left),
1560 right: Box::new(right),
1561 }
1562 } else {
1563 Stmt::OrChain {
1564 left: Box::new(left),
1565 right: Box::new(right),
1566 }
1567 }
1568 },
1569 )
1570 .then_ignore(terminator)
1571 })
1572}
1573
1574fn lvalue_subscript_parser<'tokens, I>(
1584) -> impl Parser<'tokens, I, VarSegment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1585where
1586 I: ValueInput<'tokens, Token = Token, Span = Span>,
1587{
1588 let interior = choice((
1589 select! { Token::SimpleVarRef(name) => VarSegment::Dynamic(name) },
1590 select! { Token::String(s) => VarSegment::Key(s) },
1591 select! { Token::SingleString(s) => VarSegment::Key(s) },
1592 select! { Token::Int(n) => VarSegment::Index(n) },
1593 select! { Token::Ident(s) => parse_subscript(&s) },
1594 ));
1595
1596 just(Token::LBracket)
1597 .ignore_then(interior)
1598 .then_ignore(just(Token::RBracket))
1599 .labelled("subscript")
1600}
1601
1602fn lvalue_path_parser<'tokens, I>(
1608) -> impl Parser<'tokens, I, VarPath, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1609where
1610 I: ValueInput<'tokens, Token = Token, Span = Span>,
1611{
1612 ident_parser()
1613 .then(lvalue_subscript_parser().repeated().collect::<Vec<_>>())
1614 .map(|(name, subscripts)| {
1615 let mut segments = vec![VarSegment::Field(crate::ast::normalize_name(name))];
1618 segments.extend(subscripts);
1619 VarPath { segments }
1620 })
1621 .labelled("lvalue path")
1622}
1623
1624fn assignment_parser<'tokens, I>(
1630) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1631where
1632 I: ValueInput<'tokens, Token = Token, Span = Span>,
1633{
1634 let local_assignment = just(Token::Local)
1636 .ignore_then(lvalue_path_parser())
1637 .then_ignore(just(Token::Eq))
1638 .then(value_expr_parser())
1639 .map(|(path, value)| Assignment {
1640 path,
1641 value,
1642 local: true,
1643 });
1644
1645 let bash_assignment = lvalue_path_parser()
1648 .then_ignore(just(Token::Eq))
1649 .then(value_expr_parser())
1650 .map(|(path, value)| Assignment {
1651 path,
1652 value,
1653 local: false,
1654 });
1655
1656 choice((local_assignment, bash_assignment))
1657 .labelled("assignment")
1658 .boxed()
1659}
1660
1661fn posix_function_parser<'tokens, I, S>(
1665 stmt: S,
1666) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1667where
1668 I: ValueInput<'tokens, Token = Token, Span = Span>,
1669 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1670{
1671 ident_parser()
1672 .then_ignore(just(Token::LParen))
1673 .then_ignore(just(Token::RParen))
1674 .then_ignore(just(Token::LBrace))
1675 .then_ignore(just(Token::Newline).repeated())
1676 .then(
1677 stmt.repeated()
1678 .collect::<Vec<_>>()
1679 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1680 )
1681 .then_ignore(just(Token::Newline).repeated())
1682 .then_ignore(just(Token::RBrace))
1683 .map(|(name, body)| ToolDef { name, params: vec![], body })
1684 .labelled("POSIX function")
1685 .boxed()
1686}
1687
1688fn bash_function_parser<'tokens, I, S>(
1692 stmt: S,
1693) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1694where
1695 I: ValueInput<'tokens, Token = Token, Span = Span>,
1696 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1697{
1698 just(Token::Function)
1699 .ignore_then(ident_parser())
1700 .then_ignore(just(Token::LBrace))
1701 .then_ignore(just(Token::Newline).repeated())
1702 .then(
1703 stmt.repeated()
1704 .collect::<Vec<_>>()
1705 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1706 )
1707 .then_ignore(just(Token::Newline).repeated())
1708 .then_ignore(just(Token::RBrace))
1709 .map(|(name, body)| ToolDef { name, params: vec![], body })
1710 .labelled("bash function")
1711 .boxed()
1712}
1713
1714fn if_parser<'tokens, I, S>(
1721 stmt: S,
1722) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1723where
1724 I: ValueInput<'tokens, Token = Token, Span = Span>,
1725 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1726{
1727 let branch = condition_parser()
1729 .then_ignore(just(Token::Semi).or_not())
1730 .then_ignore(just(Token::Newline).repeated())
1731 .then_ignore(just(Token::Then))
1732 .then_ignore(just(Token::Newline).repeated())
1733 .then(
1734 stmt.clone()
1735 .repeated()
1736 .collect::<Vec<_>>()
1737 .map(|stmts: Vec<Stmt>| {
1738 stmts
1739 .into_iter()
1740 .filter(|s| !matches!(s, Stmt::Empty))
1741 .collect::<Vec<_>>()
1742 }),
1743 );
1744
1745 let elif_branch = just(Token::Elif)
1747 .ignore_then(condition_parser())
1748 .then_ignore(just(Token::Semi).or_not())
1749 .then_ignore(just(Token::Newline).repeated())
1750 .then_ignore(just(Token::Then))
1751 .then_ignore(just(Token::Newline).repeated())
1752 .then(
1753 stmt.clone()
1754 .repeated()
1755 .collect::<Vec<_>>()
1756 .map(|stmts: Vec<Stmt>| {
1757 stmts
1758 .into_iter()
1759 .filter(|s| !matches!(s, Stmt::Empty))
1760 .collect::<Vec<_>>()
1761 }),
1762 );
1763
1764 let else_branch = just(Token::Else)
1766 .ignore_then(just(Token::Newline).repeated())
1767 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1768 .map(|stmts: Vec<Stmt>| {
1769 stmts
1770 .into_iter()
1771 .filter(|s| !matches!(s, Stmt::Empty))
1772 .collect::<Vec<_>>()
1773 });
1774
1775 just(Token::If)
1776 .ignore_then(branch)
1777 .then(elif_branch.repeated().collect::<Vec<_>>())
1778 .then(else_branch.or_not())
1779 .then_ignore(just(Token::Fi))
1780 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1781 build_if_chain(condition, then_branch, elif_branches, else_branch)
1783 })
1784 .labelled("if statement")
1785 .boxed()
1786}
1787
1788fn build_if_chain(
1795 condition: Expr,
1796 then_branch: Vec<Stmt>,
1797 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1798 else_branch: Option<Vec<Stmt>>,
1799) -> IfStmt {
1800 if elif_branches.is_empty() {
1801 IfStmt {
1803 condition: Box::new(condition),
1804 then_branch,
1805 else_branch,
1806 }
1807 } else {
1808 let (elif_cond, elif_then) = elif_branches.remove(0);
1810 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1811 IfStmt {
1812 condition: Box::new(condition),
1813 then_branch,
1814 else_branch: Some(vec![Stmt::If(nested_if)]),
1815 }
1816 }
1817}
1818
1819fn for_parser<'tokens, I, S>(
1821 stmt: S,
1822) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1823where
1824 I: ValueInput<'tokens, Token = Token, Span = Span>,
1825 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1826{
1827 just(Token::For)
1828 .ignore_then(ident_parser())
1829 .then_ignore(just(Token::In))
1830 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1831 .then_ignore(just(Token::Semi).or_not())
1832 .then_ignore(just(Token::Newline).repeated())
1833 .then_ignore(just(Token::Do))
1834 .then_ignore(just(Token::Newline).repeated())
1835 .then(
1836 stmt.repeated()
1837 .collect::<Vec<_>>()
1838 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1839 )
1840 .then_ignore(just(Token::Done))
1841 .map(|((variable, items), body)| ForLoop {
1842 variable,
1843 items,
1844 body,
1845 })
1846 .labelled("for loop")
1847 .boxed()
1848}
1849
1850fn while_parser<'tokens, I, S>(
1852 stmt: S,
1853) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1854where
1855 I: ValueInput<'tokens, Token = Token, Span = Span>,
1856 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1857{
1858 just(Token::While)
1859 .ignore_then(condition_parser())
1860 .then_ignore(just(Token::Semi).or_not())
1861 .then_ignore(just(Token::Newline).repeated())
1862 .then_ignore(just(Token::Do))
1863 .then_ignore(just(Token::Newline).repeated())
1864 .then(
1865 stmt.repeated()
1866 .collect::<Vec<_>>()
1867 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1868 )
1869 .then_ignore(just(Token::Done))
1870 .map(|(condition, body)| WhileLoop {
1871 condition: Box::new(condition),
1872 body,
1873 })
1874 .labelled("while loop")
1875 .boxed()
1876}
1877
1878fn case_parser<'tokens, I, S>(
1885 stmt: S,
1886) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1887where
1888 I: ValueInput<'tokens, Token = Token, Span = Span>,
1889 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1890{
1891 let pattern_part = choice((
1894 select! { Token::GlobWord(s) => s },
1895 select! { Token::Ident(s) => s },
1896 select! { Token::NumberIdent(s) => s },
1897 select! { Token::DashNumWord(s) => s },
1898 select! { Token::AtWord(s) => s },
1899 select! { Token::DottedIdent(s) => s },
1900 select! { Token::String(s) => s },
1901 select! { Token::SingleString(s) => s },
1902 select! { Token::Int(n) => n.to_string() },
1903 select! { Token::Star => "*".to_string() },
1904 select! { Token::Question => "?".to_string() },
1905 select! { Token::Dot => ".".to_string() },
1906 select! { Token::DotDot => "..".to_string() },
1907 select! { Token::Tilde => "~".to_string() },
1908 select! { Token::TildePath(s) => s },
1909 select! { Token::RelativePath(s) => s },
1910 select! { Token::DotSlashPath(s) => s },
1911 select! { Token::Path(p) => p },
1912 select! { Token::VarRef(v) => v },
1913 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1914 choice((
1922 select! { Token::DoubleDashBare(s) => s },
1923 select! { Token::PlusBare(s) => s },
1924 select! { Token::MinusBare(s) => s },
1925 select! { Token::MinusAlone => "-".to_string() },
1926 select! { Token::DoubleDash => "--".to_string() },
1927 select! { Token::ShortFlag(s) => format!("-{}", s) },
1928 select! { Token::LongFlag(s) => format!("--{}", s) },
1929 select! { Token::PlusFlag(s) => format!("+{}", s) },
1930 )),
1931 just(Token::LBracket)
1933 .ignore_then(
1934 choice((
1935 select! { Token::Ident(s) => s },
1936 select! { Token::Int(n) => n.to_string() },
1937 just(Token::Colon).to(":".to_string()),
1938 just(Token::Bang).to("!".to_string()),
1940 select! { Token::ShortFlag(s) => format!("-{}", s) },
1942 ))
1943 .repeated()
1944 .at_least(1)
1945 .collect::<Vec<String>>()
1946 )
1947 .then_ignore(just(Token::RBracket))
1948 .map(|parts| format!("[{}]", parts.join(""))),
1949 just(Token::LBrace)
1951 .ignore_then(
1952 choice((
1953 select! { Token::Ident(s) => s },
1954 select! { Token::Int(n) => n.to_string() },
1955 ))
1956 .separated_by(just(Token::Comma))
1957 .at_least(1)
1958 .collect::<Vec<String>>()
1959 )
1960 .then_ignore(just(Token::RBrace))
1961 .map(|parts| format!("{{{}}}", parts.join(","))),
1962 ));
1963
1964 let pattern = pattern_part
1967 .repeated()
1968 .at_least(1)
1969 .collect::<Vec<String>>()
1970 .map(|parts| parts.join(""))
1971 .labelled("case pattern");
1972
1973 let patterns = pattern
1975 .separated_by(just(Token::Pipe))
1976 .at_least(1)
1977 .collect::<Vec<String>>()
1978 .labelled("case patterns");
1979
1980 let branch = just(Token::LParen)
1982 .or_not()
1983 .ignore_then(just(Token::Newline).repeated())
1984 .ignore_then(patterns)
1985 .then_ignore(just(Token::RParen))
1986 .then_ignore(just(Token::Newline).repeated())
1987 .then(
1988 stmt.clone()
1989 .repeated()
1990 .collect::<Vec<_>>()
1991 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1992 )
1993 .then_ignore(just(Token::DoubleSemi))
1994 .then_ignore(just(Token::Newline).repeated())
1995 .map(|(patterns, body)| CaseBranch { patterns, body })
1996 .labelled("case branch");
1997
1998 just(Token::Case)
1999 .ignore_then(expr_parser())
2000 .then_ignore(just(Token::In))
2001 .then_ignore(just(Token::Newline).repeated())
2002 .then(branch.repeated().collect::<Vec<_>>())
2003 .then_ignore(just(Token::Esac))
2004 .map(|(expr, branches)| CaseStmt { expr, branches })
2005 .labelled("case statement")
2006 .boxed()
2007}
2008
2009fn pipeline_parser<'tokens, I, S>(
2018 stage: S,
2019) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2020where
2021 I: ValueInput<'tokens, Token = Token, Span = Span>,
2022 S: Parser<'tokens, I, PipelineStage, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2023{
2024 stage
2025 .separated_by(just(Token::Pipe))
2026 .at_least(1)
2027 .collect::<Vec<_>>()
2028 .then(just(Token::Amp).or_not())
2029 .map(|(stages, bg)| Pipeline {
2030 stages,
2031 background: bg.is_some(),
2032 })
2033 .labelled("pipeline")
2034 .boxed()
2035}
2036
2037fn command_stage_parser<'tokens, I>(
2039) -> impl Parser<'tokens, I, PipelineStage, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2040where
2041 I: ValueInput<'tokens, Token = Token, Span = Span>,
2042{
2043 command_parser().map(PipelineStage::Command)
2044}
2045
2046fn command_parser<'tokens, I>(
2050) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2051where
2052 I: ValueInput<'tokens, Token = Token, Span = Span>,
2053{
2054 let command_name = choice((
2060 ident_parser(),
2061 path_parser(),
2062 select! { Token::DotSlashPath(s) => s },
2063 just(Token::True).to("true".to_string()),
2064 just(Token::False).to("false".to_string()),
2065 just(Token::Colon).to(":".to_string()),
2066 just(Token::Dot).to(".".to_string()),
2067 ));
2068
2069 command_name
2079 .then(args_list_parser())
2080 .then(redirect_parser(primary_expr_parser()).repeated().collect::<Vec<_>>())
2081 .map(|((name, args), redirects)| Command {
2082 name,
2083 args,
2084 redirects,
2085 })
2086 .labelled("command")
2087 .boxed()
2088}
2089
2090fn pipeline_into_stmt(p: Pipeline) -> Stmt {
2095 if p.stages.len() == 1 && !p.background && p.stages[0].redirects().is_empty() {
2096 match p.stages.into_iter().next() {
2097 Some(PipelineStage::Compound(stmt)) => *stmt,
2101 Some(PipelineStage::Command(cmd)) => Stmt::Command(cmd),
2102 None => Stmt::Empty, }
2104 } else {
2105 Stmt::Pipeline(p)
2106 }
2107}
2108
2109fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
2113 cmd.redirects
2114 .iter()
2115 .filter(|r| {
2116 matches!(
2117 r.kind,
2118 RedirectKind::Stdin | RedirectKind::HereDoc(_) | RedirectKind::HereString
2119 )
2120 })
2121 .count()
2122 > 1
2123}
2124
2125fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
2129 stmts.iter().any(stmt_has_ambiguous_stdin)
2130}
2131
2132fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
2133 match stmt {
2134 Stmt::Command(c) => command_has_ambiguous_stdin(c),
2135 Stmt::Pipeline(p) => p.stages.iter().any(|stage| match stage {
2136 PipelineStage::Command(cmd) => command_has_ambiguous_stdin(cmd),
2137 PipelineStage::Compound(inner) => stmt_has_ambiguous_stdin(inner),
2138 }),
2139 Stmt::If(i) => {
2140 first_ambiguous_stdin(&i.then_branch)
2141 || i.else_branch
2142 .as_deref()
2143 .is_some_and(first_ambiguous_stdin)
2144 }
2145 Stmt::For(f) => first_ambiguous_stdin(&f.body),
2146 Stmt::While(w) => first_ambiguous_stdin(&w.body),
2147 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
2148 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
2149 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
2150 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
2151 }
2152 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
2153 Stmt::Assignment(_)
2154 | Stmt::Break(_)
2155 | Stmt::Continue(_)
2156 | Stmt::Return(_)
2157 | Stmt::Exit(_)
2158 | Stmt::Test(_)
2159 | Stmt::Empty => false,
2160 }
2161}
2162
2163fn is_glue_candidate(arg: &Arg) -> bool {
2187 matches!(
2188 arg,
2189 Arg::Positional(_) | Arg::LongFlag(_) | Arg::Named { .. } | Arg::WordAssign { .. }
2190 )
2191}
2192
2193fn reject_glued_args<'src>(
2211 args: Vec<(Arg, Span)>,
2212) -> Result<Vec<Arg>, Rich<'src, Token, Span>> {
2213 for pair in args.windows(2) {
2214 let (prev, prev_span) = &pair[0];
2215 let (next, next_span) = &pair[1];
2216 if is_glue_candidate(prev) && is_glue_candidate(next) && prev_span.end == next_span.start {
2217 let msg = "adjacent words with no space between them are not joined into one \
2218 argument (kaish does no token pasting); quote the whole word, e.g. \
2219 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\"";
2220 return Err(Rich::custom(*next_span, msg));
2221 }
2222 }
2223 Ok(args.into_iter().map(|(arg, _)| arg).collect())
2224}
2225
2226fn args_list_parser<'tokens, I>(
2230) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2231where
2232 I: ValueInput<'tokens, Token = Token, Span = Span>,
2233{
2234 let pre_dash = arg_before_double_dash_parser()
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 let double_dash = select! {
2249 Token::DoubleDash => Arg::DoubleDash,
2250 };
2251
2252 let post_dash_arg = choice((
2254 post_dash_flag_value_parser(),
2262 select! {
2264 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
2265 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
2266 },
2267 word_assign_arg_parser(),
2274 test_operator_arg_parser(),
2276 primary_expr_parser().map(Arg::Positional),
2278 ));
2279
2280 let post_dash = post_dash_arg
2284 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
2285 .repeated()
2286 .collect::<Vec<(Arg, Span)>>()
2287 .try_map(|args, _span| reject_glued_args(args));
2288
2289 pre_dash
2291 .then(double_dash.then(post_dash).or_not())
2292 .map(|(mut args, maybe_dd)| {
2293 if let Some((dd, post)) = maybe_dd {
2294 args.push(dd);
2295 args.extend(post);
2296 }
2297 args
2298 })
2299}
2300
2301fn keyword_word<'tokens, I>(
2311) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2312where
2313 I: ValueInput<'tokens, Token = Token, Span = Span>,
2314{
2315 select! {
2316 Token::Set => "set",
2317 Token::Local => "local",
2318 Token::If => "if",
2319 Token::Then => "then",
2320 Token::Else => "else",
2321 Token::Elif => "elif",
2322 Token::Fi => "fi",
2323 Token::For => "for",
2324 Token::While => "while",
2325 Token::In => "in",
2326 Token::Do => "do",
2327 Token::Done => "done",
2328 Token::Case => "case",
2329 Token::Esac => "esac",
2330 Token::Function => "function",
2331 Token::Break => "break",
2332 Token::Continue => "continue",
2333 Token::Return => "return",
2334 Token::Exit => "exit",
2335 }
2336 .map(|s| s.to_string())
2337}
2338
2339fn post_dash_flag_value_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 select! { Token::LongFlag(name) => name }
2368 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
2369 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
2370 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
2371 .try_map(
2372 |(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)),
2373 span| {
2374 if key_span.end != eq_span.start || eq_span.end != value_span.start {
2375 Err(Rich::custom(
2376 span,
2377 "a flag and its value must not have spaces around '=' \
2378 (use '--flag=value' not '--flag = value')",
2379 ))
2380 } else {
2381 Ok(Arg::Named { key, value })
2382 }
2383 },
2384 )
2385}
2386
2387fn word_assign_arg_parser<'tokens, I>(
2388) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2389where
2390 I: ValueInput<'tokens, Token = Token, Span = Span>,
2391{
2392 choice((
2393 select! { Token::Ident(s) => s },
2394 keyword_word(),
2395 ))
2396 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
2397 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
2398 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
2399 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
2400 if key_span.end != eq_span.start || eq_span.end != value_span.start {
2402 Err(Rich::custom(
2403 span,
2404 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
2405 ))
2406 } else {
2407 Ok(Arg::WordAssign { key, value })
2408 }
2409 })
2410}
2411
2412fn test_operator_arg_parser<'tokens, I>(
2429) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2430where
2431 I: ValueInput<'tokens, Token = Token, Span = Span>,
2432{
2433 select! {
2434 Token::Eq => "=",
2435 Token::EqEq => "==",
2436 Token::NotEq => "!=",
2437 Token::Bang => "!",
2438 }
2439 .map(|s| Arg::Positional(Expr::Literal(Value::String(s.to_string()))))
2440}
2441
2442fn arg_before_double_dash_parser<'tokens, I>(
2444) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2445where
2446 I: ValueInput<'tokens, Token = Token, Span = Span>,
2447{
2448 let long_flag_with_value = select! {
2450 Token::LongFlag(name) => name,
2451 }
2452 .then_ignore(just(Token::Eq))
2453 .then(primary_expr_parser())
2454 .map(|(key, value)| Arg::Named { key, value });
2455
2456 let long_flag = select! {
2458 Token::LongFlag(name) => Arg::LongFlag(name),
2459 };
2460
2461 let short_flag = select! {
2463 Token::ShortFlag(name) => Arg::ShortFlag(name),
2464 };
2465
2466 let named = word_assign_arg_parser();
2468
2469 let positional = primary_expr_parser().map(Arg::Positional);
2471
2472 let test_operator = test_operator_arg_parser();
2476
2477 choice((
2480 long_flag_with_value,
2481 long_flag,
2482 short_flag,
2483 named,
2484 test_operator,
2485 positional,
2486 ))
2487 .boxed()
2488}
2489
2490fn redirect_parser<'tokens, I, T>(
2504 target: T,
2505) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2506where
2507 I: ValueInput<'tokens, Token = Token, Span = Span>,
2508 T: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2509{
2510 let target = target
2523 .clone()
2524 .map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) })
2525 .then(target.clone().map_with(|_, e| e.span()).rewind().or_not())
2526 .try_map(|((expr, span), glued), _| match glued {
2527 Some(next_span) if next_span.start == span.end => Err(Rich::custom(
2528 next_span,
2529 "adjacent words with no space between them are not joined into the redirect \
2530 target (kaish does no token pasting); quote the whole target, e.g. \
2531 \"/tmp/$(echo x).txt\"",
2532 )),
2533 _ => Ok(expr),
2534 })
2535 .boxed();
2536
2537 let regular_redirect = select! {
2539 Token::GtGt => RedirectKind::StdoutAppend,
2540 Token::Gt => RedirectKind::StdoutOverwrite,
2541 Token::Lt => RedirectKind::Stdin,
2542 Token::Stderr => RedirectKind::Stderr,
2543 Token::Both => RedirectKind::Both,
2544 }
2545 .then(target.clone())
2546 .map(|(kind, target)| Redirect { kind, target });
2547
2548 let heredoc_redirect = just(Token::HereDocStart)
2556 .ignore_then(select! { Token::HereDoc(data) => data })
2557 .try_map(|data: HereDocData, span| {
2558 let meta = HereDocMeta {
2561 delimiter: data.delimiter.clone(),
2562 literal: data.literal,
2563 strip_tabs: data.strip_tabs,
2564 body: data.source_body.clone(),
2565 body_offset: data.body_start_offset,
2566 };
2567 let target = if data.literal {
2568 let body = if data.strip_tabs {
2569 crate::interpreter::strip_leading_tabs(&data.content)
2570 } else {
2571 data.content
2572 };
2573 Expr::Literal(Value::String(body))
2574 } else {
2575 let parts =
2576 parse_interpolated_string_spanned(&data.content, data.body_start_offset)
2577 .map_err(|msg| Rich::custom(span, msg))?;
2578 if parts.len() == 1 && !data.strip_tabs {
2582 if let StringPart::Literal(text) = &parts[0].part {
2583 return Ok(Redirect {
2584 kind: RedirectKind::HereDoc(meta),
2585 target: Expr::Literal(Value::String(text.clone())),
2586 });
2587 }
2588 }
2589 Expr::HereDocBody {
2590 parts,
2591 strip_tabs: data.strip_tabs,
2592 }
2593 };
2594 Ok(Redirect {
2595 kind: RedirectKind::HereDoc(meta),
2596 target,
2597 })
2598 });
2599
2600 let herestring_redirect = just(Token::HereString)
2604 .ignore_then(target.clone())
2605 .map(|target| Redirect {
2606 kind: RedirectKind::HereString,
2607 target,
2608 });
2609
2610 let merge_stderr_redirect = just(Token::StderrToStdout)
2612 .map(|_| Redirect {
2613 kind: RedirectKind::MergeStderr,
2614 target: Expr::Literal(Value::Null),
2616 });
2617
2618 let merge_stdout_redirect = choice((
2620 just(Token::StdoutToStderr),
2621 just(Token::StdoutToStderr2),
2622 ))
2623 .map(|_| Redirect {
2624 kind: RedirectKind::MergeStdout,
2625 target: Expr::Literal(Value::Null),
2627 });
2628
2629 choice((
2630 heredoc_redirect,
2631 herestring_redirect,
2632 merge_stderr_redirect,
2633 merge_stdout_redirect,
2634 regular_redirect,
2635 ))
2636 .labelled("redirect")
2637 .boxed()
2638}
2639
2640fn test_expr_stmt_parser<'tokens, I>(
2652) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2653where
2654 I: ValueInput<'tokens, Token = Token, Span = Span>,
2655{
2656 let file_test_op = select! {
2658 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
2659 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
2660 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
2661 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
2662 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
2663 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
2664 };
2665
2666 let string_test_op = select! {
2670 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
2671 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
2672 Token::ShortFlag(s) if s == "list" => StringTestOp::IsList,
2673 Token::ShortFlag(s) if s == "record" => StringTestOp::IsRecord,
2674 };
2675
2676 let cmp_op = choice((
2679 just(Token::EqEq).to(TestCmpOp::Eq),
2680 just(Token::Eq).to(TestCmpOp::Eq),
2681 just(Token::NotEq).to(TestCmpOp::NotEq),
2682 just(Token::Match).to(TestCmpOp::Match),
2683 just(Token::NotMatch).to(TestCmpOp::NotMatch),
2684 just(Token::Gt).to(TestCmpOp::Gt),
2685 just(Token::Lt).to(TestCmpOp::Lt),
2686 just(Token::GtEq).to(TestCmpOp::GtEq),
2687 just(Token::LtEq).to(TestCmpOp::LtEq),
2688 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
2689 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
2690 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
2691 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
2692 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
2693 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
2694 ));
2695
2696 let file_test = file_test_op
2698 .then(primary_expr_parser())
2699 .map(|(op, path)| TestExpr::FileTest {
2700 op,
2701 path: Box::new(path),
2702 });
2703
2704 let string_test = string_test_op
2706 .then(primary_expr_parser())
2707 .map(|(op, value)| TestExpr::StringTest {
2708 op,
2709 value: Box::new(value),
2710 });
2711
2712 let comparison = primary_expr_parser()
2714 .then(cmp_op)
2715 .then(primary_expr_parser())
2716 .map(|((left, op), right)| TestExpr::Comparison {
2717 left: Box::new(left),
2718 op,
2719 right: Box::new(right),
2720 });
2721
2722 let not_in = primary_expr_parser()
2728 .then_ignore(select! { Token::Ident(s) if s == "not" => () })
2729 .then_ignore(just(Token::In))
2730 .then(value_primary_parser())
2731 .map(|(left, right)| TestExpr::NotIn {
2732 left: Box::new(left),
2733 right: Box::new(right),
2734 });
2735
2736 let in_ = primary_expr_parser()
2737 .then_ignore(just(Token::In))
2738 .then(value_primary_parser())
2739 .map(|(left, right)| TestExpr::In {
2740 left: Box::new(left),
2741 right: Box::new(right),
2742 });
2743
2744 let primary_test = choice((file_test, string_test, not_in, in_, comparison));
2746
2747 let unary = recursive(|unary| {
2761 let not_expr = just(Token::Bang)
2762 .ignore_then(unary)
2763 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
2764 choice((not_expr, primary_test.clone()))
2765 });
2766
2767 let and_expr = unary.clone().foldl(
2769 just(Token::And).ignore_then(unary).repeated(),
2770 |left, right| TestExpr::And {
2771 left: Box::new(left),
2772 right: Box::new(right),
2773 },
2774 );
2775
2776 let compound_test = and_expr.clone().foldl(
2778 just(Token::Or).ignore_then(and_expr).repeated(),
2779 |left, right| TestExpr::Or {
2780 left: Box::new(left),
2781 right: Box::new(right),
2782 },
2783 );
2784
2785 just(Token::LBracket)
2788 .then(just(Token::LBracket))
2789 .ignore_then(compound_test)
2790 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
2791 .labelled("test expression")
2792 .boxed()
2793}
2794
2795fn condition_parser<'tokens, I>(
2810) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2811where
2812 I: ValueInput<'tokens, Token = Token, Span = Span>,
2813{
2814 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
2816
2817 let command_condition = command_parser().map(Expr::Command);
2820
2821 let base = choice((test_expr_condition, command_condition));
2823
2824 let base = just(Token::Bang)
2828 .repeated()
2829 .foldr(base, |_, inner| Expr::Not(Box::new(inner)));
2830
2831 let and_expr = base.clone().foldl(
2834 just(Token::And).ignore_then(base).repeated(),
2835 |left, right| Expr::BinaryOp {
2836 left: Box::new(left),
2837 op: BinaryOp::And,
2838 right: Box::new(right),
2839 },
2840 );
2841
2842 and_expr
2844 .clone()
2845 .foldl(
2846 just(Token::Or).ignore_then(and_expr).repeated(),
2847 |left, right| Expr::BinaryOp {
2848 left: Box::new(left),
2849 op: BinaryOp::Or,
2850 right: Box::new(right),
2851 },
2852 )
2853 .labelled("condition")
2854 .boxed()
2855}
2856
2857fn expr_parser<'tokens, I>(
2864) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2865where
2866 I: ValueInput<'tokens, Token = Token, Span = Span>,
2867{
2868 primary_expr_parser()
2870}
2871
2872fn value_expr_parser<'tokens, I>(
2877) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2878where
2879 I: ValueInput<'tokens, Token = Token, Span = Span>,
2880{
2881 value_literal_parser()
2882}
2883
2884fn value_primary_parser<'tokens, I>(
2890) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2891where
2892 I: ValueInput<'tokens, Token = Token, Span = Span>,
2893{
2894 value_literal_parser()
2895}
2896
2897fn value_literal_parser<'tokens, I>(
2909) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2910where
2911 I: ValueInput<'tokens, Token = Token, Span = Span>,
2912{
2913 recursive(|value| {
2914 choice((
2915 list_literal_parser(value.clone()),
2916 record_literal_parser(value.clone()),
2917 primary_expr_parser(),
2918 ))
2919 })
2920 .boxed()
2921}
2922
2923fn list_literal_parser<'tokens, I, V>(
2931 value: V,
2932) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2933where
2934 I: ValueInput<'tokens, Token = Token, Span = Span>,
2935 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2936{
2937 let spread_elem = just(Token::DotDotDot)
2938 .ignore_then(value.clone())
2939 .map(ListElem::Spread);
2940 let item_elem = value.map(ListElem::Item);
2941 let elem = choice((spread_elem, item_elem));
2942
2943 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2944
2945 just(Token::LBracket)
2946 .ignore_then(just(Token::Newline).repeated())
2947 .ignore_then(elem.then_ignore(sep).repeated().collect::<Vec<_>>())
2948 .then_ignore(just(Token::RBracket))
2949 .map(Expr::ListLiteral)
2950 .labelled("list literal")
2951}
2952
2953fn record_literal_parser<'tokens, I, V>(
2961 value: V,
2962) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2963where
2964 I: ValueInput<'tokens, Token = Token, Span = Span>,
2965 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2966{
2967 let bare_key = select! { Token::Ident(s) => RecordKey::Bare(s) };
2968 let double_key = select! { Token::String(s) => s }.try_map(|s, span| {
2974 let parts = parse_interpolated_string(&s)
2975 .map_err(|e| Rich::custom(span, format!("record key: {e}")))?;
2976 Ok(match parts.as_slice() {
2977 [] => RecordKey::Quoted(String::new()),
2978 [StringPart::Literal(lit)] => RecordKey::Quoted(lit.clone()),
2979 _ => RecordKey::Interpolated(parts),
2980 })
2981 });
2982 let single_key = select! { Token::SingleString(s) => RecordKey::Quoted(s) };
2983 let key = choice((double_key, single_key, bare_key)).labelled("record key");
2984
2985 let stray_bareword_after_value = select! { Token::Ident(s) => s }
2997 .then(just(Token::Colon).or_not())
2998 .rewind()
2999 .or_not()
3000 .try_map(|maybe, span| match maybe {
3001 Some((word, None)) => Err(Rich::custom(
3002 span,
3003 format!(
3004 "record value: unexpected word \"{word}\" after the value — a multi-word \
3005 value must be quoted, e.g. {{key: \"hello world\"}}"
3006 ),
3007 )),
3008 _ => Ok(()),
3009 });
3010
3011 let entry = key
3012 .then_ignore(just(Token::Colon))
3013 .then(value)
3014 .then_ignore(stray_bareword_after_value)
3015 .map(|(key, value)| RecordEntry { key, value });
3016
3017 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
3018
3019 just(Token::LBrace)
3020 .ignore_then(just(Token::Newline).repeated())
3021 .ignore_then(entry.then_ignore(sep).repeated().collect::<Vec<_>>())
3022 .then_ignore(just(Token::RBrace))
3023 .map(Expr::RecordLiteral)
3024 .labelled("record literal")
3025}
3026
3027fn primary_expr_parser<'tokens, I>(
3031) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3032where
3033 I: ValueInput<'tokens, Token = Token, Span = Span>,
3034{
3035 let positional = select! {
3037 Token::Positional(n) => Expr::Positional(n),
3038 Token::AllArgs => Expr::AllArgs,
3039 Token::ArgCount => Expr::ArgCount,
3040 Token::VarLength(name) => Expr::VarLength(parse_varpath(&format!("${{{name}}}"))),
3041 Token::LastExitCode => Expr::LastExitCode,
3042 Token::CurrentPid => Expr::CurrentPid,
3043 };
3044
3045 let arithmetic = select! {
3047 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
3048 };
3049
3050 let keyword_as_bareword = select! {
3053 Token::Done => "done",
3054 Token::Fi => "fi",
3055 Token::Then => "then",
3056 Token::Else => "else",
3057 Token::Elif => "elif",
3058 Token::In => "in",
3059 Token::Do => "do",
3060 Token::Esac => "esac",
3061 Token::Set => "set",
3066 }
3067 .map(|s| Expr::Literal(Value::String(s.to_string())));
3068
3069 let plus_minus_bare = select! {
3073 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
3074 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
3075 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
3076 Token::DoubleDashBare(s) => Expr::Literal(Value::String(s)),
3077 };
3078
3079 let glob_pattern = select! {
3081 Token::GlobWord(s) => Expr::GlobPattern(s),
3082 Token::Star => Expr::GlobPattern("*".to_string()),
3083 Token::Question => Expr::GlobPattern("?".to_string()),
3084 };
3085
3086 choice((
3093 positional,
3094 arithmetic,
3095 cmd_subst_parser(),
3096 var_expr_parser(),
3097 interpolated_string_parser(),
3098 literal_parser().map(Expr::Literal),
3099 glob_pattern,
3101 ident_parser().map(|s| Expr::Literal(Value::String(s))),
3103 path_parser().map(|s| Expr::Literal(Value::String(s))),
3105 select! {
3108 Token::Dot => Expr::Literal(Value::String(".".into())),
3114 Token::DotDot => Expr::Literal(Value::String("..".into())),
3115 Token::Comma => Expr::Literal(Value::String(",".into())),
3127 Token::Colon => Expr::Literal(Value::String(":".into())),
3134 Token::Tilde => Expr::Literal(Value::String("~".into())),
3135 Token::TildePath(s) => Expr::Literal(Value::String(s)),
3136 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
3137 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
3138 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
3140 Token::DashNumWord(s) => Expr::Literal(Value::String(s)),
3143 Token::AtWord(s) => Expr::Literal(Value::String(s)),
3145 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
3150 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
3153 },
3154 plus_minus_bare,
3155 keyword_as_bareword,
3157 ))
3158 .labelled("expression")
3159 .boxed()
3160}
3161
3162fn var_expr_parser<'tokens, I>(
3165) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3166where
3167 I: ValueInput<'tokens, Token = Token, Span = Span>,
3168{
3169 choice((
3170 select! { Token::VarRef(raw) => raw }.try_map(|raw, span| {
3171 let inner = raw
3175 .strip_prefix("${")
3176 .and_then(|s| s.strip_suffix('}'))
3177 .unwrap_or(&raw);
3178 if !raw.starts_with("${?}")
3179 && !raw.starts_with("${$}")
3180 && find_default_separator(&raw).is_none()
3181 && let Some(msg) = bash_substring_hint(inner)
3182 {
3183 return Err(Rich::custom(span, msg));
3184 }
3185 if let Some(colon) = find_default_separator(&raw)
3193 && raw.len() > colon + 3
3194 && let Err(msg) =
3195 parse_interpolated_string(&unquote_default_word(&raw[colon + 2..raw.len() - 1]))
3196 {
3197 return Err(Rich::custom(span, msg));
3198 }
3199 Ok(parse_var_expr(&raw))
3200 }),
3201 select! { Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)) },
3202 ))
3203 .labelled("variable reference")
3204}
3205
3206type CmdSubstBody = (Vec<(Token, Span)>, Span);
3226
3227fn cmd_subst_body_tokens<'tokens, I>(
3228) -> impl Parser<'tokens, I, CmdSubstBody, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3229where
3230 I: ValueInput<'tokens, Token = Token, Span = Span>,
3231{
3232 custom(|inp| {
3233 let mut tracker = CmdSubstFrames::default();
3234 let mut body: Vec<(Token, Span)> = Vec::new();
3235 loop {
3236 let before = inp.cursor();
3237 match inp.next() {
3238 None => {
3239 let span = inp.span_since(&before);
3240 return Err(Rich::custom(
3241 span,
3242 "unterminated command substitution: missing `)`",
3243 ));
3244 }
3245 Some(tok) => {
3246 let span = inp.span_since(&before);
3247 let next = inp.peek();
3251 if tracker.step(&tok, next.as_ref()) {
3252 return Ok((body, span));
3253 }
3254 body.push((tok, span));
3255 }
3256 }
3257 }
3258 })
3259}
3260
3261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3263enum CmdSubstFrame {
3264 Subst,
3266 Paren,
3272 Case { awaiting_pattern: bool },
3281}
3282
3283#[derive(Default)]
3328struct CmdSubstFrames(Vec<CmdSubstFrame>);
3329
3330impl CmdSubstFrames {
3331 fn step(&mut self, tok: &Token, next: Option<&Token>) -> bool {
3338 match tok {
3339 Token::RParen => match self.0.last_mut() {
3340 None => return true,
3341 Some(CmdSubstFrame::Case { awaiting_pattern }) => {
3342 *awaiting_pattern = false;
3343 }
3344 Some(CmdSubstFrame::Paren) => {
3345 self.0.pop();
3346 if let Some(CmdSubstFrame::Case { awaiting_pattern }) = self.0.last_mut() {
3350 *awaiting_pattern = false;
3351 }
3352 }
3353 Some(CmdSubstFrame::Subst) => {
3354 self.0.pop();
3355 }
3356 },
3357 Token::LParen => self.0.push(CmdSubstFrame::Paren),
3358 Token::CmdSubstStart => self.0.push(CmdSubstFrame::Subst),
3359 Token::Case if !matches!(next, Some(Token::Eq)) => {
3366 self.0.push(CmdSubstFrame::Case { awaiting_pattern: true });
3367 }
3368 Token::Case => {}
3369 Token::DoubleSemi => {
3370 if let Some(CmdSubstFrame::Case { awaiting_pattern }) = self.0.last_mut() {
3371 *awaiting_pattern = true;
3372 }
3373 }
3374 Token::Esac
3375 if matches!(
3376 self.0.last(),
3377 Some(CmdSubstFrame::Case { awaiting_pattern: true })
3378 ) =>
3379 {
3380 self.0.pop();
3381 }
3382 _ => {}
3383 }
3384 false
3385 }
3386}
3387
3388fn find_cmd_subst_close(tokens: &[(Token, Span)]) -> Option<usize> {
3396 let mut tracker = CmdSubstFrames::default();
3397 (0..tokens.len()).find(|&i| {
3398 let next = tokens.get(i + 1).map(|(t, _)| t);
3399 tracker.step(&tokens[i].0, next)
3400 })
3401}
3402
3403fn validate_cmd_subst_bodies(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3424 let mut i = 0;
3425 while i < tokens.len() {
3426 if !matches!(tokens[i].0, Token::CmdSubstStart) {
3427 i += 1;
3428 continue;
3429 }
3430 let start_span = tokens[i].1;
3431 let rest = &tokens[i + 1..];
3432 let Some(close_rel) = find_cmd_subst_close(rest) else {
3433 return Err(vec![ParseError {
3434 span: start_span,
3435 message: "unterminated command substitution: missing `)`".to_string(),
3436 }]);
3437 };
3438 let body = &rest[..close_rel];
3439 let rparen_span = rest[close_rel].1;
3440 let end_span: Span = (rparen_span.start..rparen_span.start).into();
3441 parse_tokens(body.to_vec(), end_span, start_span)?;
3445 i += 1 + close_rel + 1;
3446 }
3447 Ok(())
3448}
3449
3450fn validate_interpolated_strings(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3462 for (tok, span) in tokens {
3463 let owned;
3464 let body = match tok {
3465 Token::String(s) => Some(s.as_str()),
3466 Token::VarRef(raw) => match find_default_separator(raw) {
3470 Some(colon) if raw.len() > colon + 3 => {
3471 owned = unquote_default_word(&raw[colon + 2..raw.len() - 1]);
3472 Some(owned.as_str())
3473 }
3474 _ => None,
3475 },
3476 _ => None,
3477 };
3478 if let Some(body) = body
3479 && let Err(message) = parse_interpolated_string(body)
3480 {
3481 return Err(vec![ParseError { span: *span, message }]);
3482 }
3483 }
3484 Ok(())
3485}
3486
3487fn validate_heredoc_bodies(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3497 for (tok, span) in tokens {
3498 if let Token::HereDoc(d) = tok
3499 && !d.literal
3500 && let Err(message) = parse_interpolated_string_spanned(&d.content, 0)
3501 {
3502 return Err(vec![ParseError { span: *span, message }]);
3503 }
3504 }
3505 Ok(())
3506}
3507
3508fn cmd_subst_parser<'tokens, I>(
3531) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3532where
3533 I: ValueInput<'tokens, Token = Token, Span = Span>,
3534{
3535 just(Token::CmdSubstStart)
3536 .ignore_then(cmd_subst_body_tokens())
3537 .try_map(|(body_tokens, rparen_span), outer_span| {
3538 let end_span: Span = (rparen_span.start..rparen_span.start).into();
3539 parse_tokens(body_tokens, end_span, outer_span)
3540 .map(|program| Expr::CommandSubst(program.statements))
3541 .map_err(|errs| {
3542 let first = errs.into_iter().next().unwrap_or_else(|| ParseError {
3543 span: outer_span,
3544 message: "command substitution failed to parse".to_string(),
3545 });
3546 Rich::custom(first.span, first.message)
3547 })
3548 })
3549 .labelled("command substitution")
3550}
3551
3552fn interpolated_string_parser<'tokens, I>(
3554) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3555where
3556 I: ValueInput<'tokens, Token = Token, Span = Span>,
3557{
3558 let double_quoted = select! {
3560 Token::String(s) => s,
3561 }
3562 .try_map(|s, span| {
3563 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
3565 let parts = parse_interpolated_string(&s)
3568 .map_err(|msg| Rich::custom(span, msg))?;
3569 if parts.len() == 1
3570 && let StringPart::Literal(text) = &parts[0] {
3571 return Ok(Expr::Literal(Value::String(text.clone())));
3572 }
3573 Ok(Expr::Interpolated(parts))
3574 } else {
3575 Ok(Expr::Literal(Value::String(s)))
3576 }
3577 });
3578
3579 let single_quoted = select! {
3581 Token::SingleString(s) => Expr::Literal(Value::String(s)),
3582 };
3583
3584 choice((single_quoted, double_quoted)).labelled("string")
3585}
3586
3587fn literal_parser<'tokens, I>(
3589) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3590where
3591 I: ValueInput<'tokens, Token = Token, Span = Span>,
3592{
3593 choice((
3594 select! {
3595 Token::True => Value::Bool(true),
3596 Token::False => Value::Bool(false),
3597 },
3598 select! {
3599 Token::Int(n) => Value::Int(n),
3600 Token::Float(f) => Value::Float(f),
3601 },
3602 ))
3603 .labelled("literal")
3604 .boxed()
3605}
3606
3607fn ident_parser<'tokens, I>(
3609) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3610where
3611 I: ValueInput<'tokens, Token = Token, Span = Span>,
3612{
3613 select! {
3614 Token::Ident(s) => s,
3615 }
3616 .labelled("identifier")
3617}
3618
3619fn path_parser<'tokens, I>(
3621) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3622where
3623 I: ValueInput<'tokens, Token = Token, Span = Span>,
3624{
3625 select! {
3626 Token::Path(s) => s,
3627 }
3628 .labelled("path")
3629}
3630
3631#[cfg(test)]
3632#[allow(clippy::approx_constant)]
3633mod tests {
3634 use super::*;
3635 use proptest::strategy::Strategy;
3636
3637 fn pipeline_commands(p: &Pipeline) -> Vec<&Command> {
3641 p.stages
3642 .iter()
3643 .map(|stage| stage.as_command().expect("expected a command stage"))
3644 .collect()
3645 }
3646
3647 fn subst_cmd(expr: &Expr) -> &Command {
3649 match expr {
3650 Expr::CommandSubst(stmts) => match stmts.as_slice() {
3651 [Stmt::Command(cmd)] => cmd,
3652 other => panic!("expected a single command in $(), got {other:?}"),
3653 },
3654 other => panic!("expected command subst, got {other:?}"),
3655 }
3656 }
3657
3658 fn subst_pipeline(expr: &Expr) -> &Pipeline {
3660 match expr {
3661 Expr::CommandSubst(stmts) => match stmts.as_slice() {
3662 [Stmt::Pipeline(p)] => p,
3663 other => panic!("expected a single pipeline in $(), got {other:?}"),
3664 },
3665 other => panic!("expected command subst, got {other:?}"),
3666 }
3667 }
3668
3669 #[test]
3670 fn parse_empty() {
3671 let result = parse("");
3672 assert!(result.is_ok());
3673 assert_eq!(result.expect("ok").statements.len(), 0);
3674 }
3675
3676 #[test]
3677 fn parse_newlines_only() {
3678 let result = parse("\n\n\n");
3679 assert!(result.is_ok());
3680 }
3681
3682 #[test]
3683 fn parse_simple_command() {
3684 let result = parse("echo");
3685 assert!(result.is_ok());
3686 let program = result.expect("ok");
3687 assert_eq!(program.statements.len(), 1);
3688 assert!(matches!(&program.statements[0], Stmt::Command(_)));
3689 }
3690
3691 #[test]
3692 fn parse_command_with_string_arg() {
3693 let result = parse(r#"echo "hello""#);
3694 assert!(result.is_ok());
3695 let program = result.expect("ok");
3696 match &program.statements[0] {
3697 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
3698 _ => panic!("expected Command"),
3699 }
3700 }
3701
3702 #[test]
3703 fn parse_assignment() {
3704 let result = parse("X=5");
3705 assert!(result.is_ok());
3706 let program = result.expect("ok");
3707 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
3708 }
3709
3710 #[test]
3711 fn parse_pipeline() {
3712 let result = parse("a | b | c");
3713 assert!(result.is_ok());
3714 let program = result.expect("ok");
3715 match &program.statements[0] {
3716 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 3),
3717 _ => panic!("expected Pipeline"),
3718 }
3719 }
3720
3721 #[test]
3722 fn parse_background_job() {
3723 let result = parse("cmd &");
3724 assert!(result.is_ok());
3725 let program = result.expect("ok");
3726 match &program.statements[0] {
3727 Stmt::Pipeline(p) => assert!(p.background),
3728 _ => panic!("expected Pipeline with background"),
3729 }
3730 }
3731
3732 #[test]
3733 fn parse_if_simple() {
3734 let result = parse("if true; then echo; fi");
3735 assert!(result.is_ok());
3736 let program = result.expect("ok");
3737 assert!(matches!(&program.statements[0], Stmt::If(_)));
3738 }
3739
3740 #[test]
3741 fn parse_if_else() {
3742 let result = parse("if true; then echo; else echo; fi");
3743 assert!(result.is_ok());
3744 let program = result.expect("ok");
3745 match &program.statements[0] {
3746 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
3747 _ => panic!("expected If"),
3748 }
3749 }
3750
3751 #[test]
3752 fn parse_elif_simple() {
3753 let result = parse("if true; then echo a; elif false; then echo b; fi");
3754 assert!(result.is_ok(), "parse failed: {:?}", result);
3755 let program = result.expect("ok");
3756 match &program.statements[0] {
3757 Stmt::If(if_stmt) => {
3758 assert!(if_stmt.else_branch.is_some());
3760 let else_branch = if_stmt.else_branch.as_ref().unwrap();
3761 assert_eq!(else_branch.len(), 1);
3762 assert!(matches!(&else_branch[0], Stmt::If(_)));
3763 }
3764 _ => panic!("expected If"),
3765 }
3766 }
3767
3768 #[test]
3769 fn parse_elif_with_else() {
3770 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
3771 assert!(result.is_ok(), "parse failed: {:?}", result);
3772 let program = result.expect("ok");
3773 match &program.statements[0] {
3774 Stmt::If(outer_if) => {
3775 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
3777 assert_eq!(else_branch.len(), 1);
3778 match &else_branch[0] {
3779 Stmt::If(inner_if) => {
3780 assert!(inner_if.else_branch.is_some());
3782 }
3783 _ => panic!("expected nested If from elif"),
3784 }
3785 }
3786 _ => panic!("expected If"),
3787 }
3788 }
3789
3790 #[test]
3791 fn parse_multiple_elif() {
3792 let result = parse(
3794 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
3795 );
3796 assert!(result.is_ok(), "parse failed: {:?}", result);
3797 }
3798
3799 #[test]
3800 fn parse_for_loop() {
3801 let result = parse("for X in items; do echo; done");
3802 assert!(result.is_ok());
3803 let program = result.expect("ok");
3804 assert!(matches!(&program.statements[0], Stmt::For(_)));
3805 }
3806
3807 #[test]
3808 fn parse_brackets_not_array_literal() {
3809 let result = parse("cmd [1");
3811 let _ = result;
3814 }
3815
3816 #[test]
3817 fn parse_named_arg() {
3818 let result = parse("cmd foo=5");
3822 assert!(result.is_ok());
3823 let program = result.expect("ok");
3824 match &program.statements[0] {
3825 Stmt::Command(cmd) => {
3826 assert_eq!(cmd.args.len(), 1);
3827 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
3828 }
3829 _ => panic!("expected Command"),
3830 }
3831 }
3832
3833 #[test]
3834 fn parse_short_flag() {
3835 let result = parse("ls -l");
3836 assert!(result.is_ok());
3837 let program = result.expect("ok");
3838 match &program.statements[0] {
3839 Stmt::Command(cmd) => {
3840 assert_eq!(cmd.name, "ls");
3841 assert_eq!(cmd.args.len(), 1);
3842 match &cmd.args[0] {
3843 Arg::ShortFlag(name) => assert_eq!(name, "l"),
3844 _ => panic!("expected ShortFlag"),
3845 }
3846 }
3847 _ => panic!("expected Command"),
3848 }
3849 }
3850
3851 #[test]
3852 fn parse_long_flag() {
3853 let result = parse("git push --force");
3854 assert!(result.is_ok());
3855 let program = result.expect("ok");
3856 match &program.statements[0] {
3857 Stmt::Command(cmd) => {
3858 assert_eq!(cmd.name, "git");
3859 assert_eq!(cmd.args.len(), 2);
3860 match &cmd.args[0] {
3861 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
3862 _ => panic!("expected Positional push"),
3863 }
3864 match &cmd.args[1] {
3865 Arg::LongFlag(name) => assert_eq!(name, "force"),
3866 _ => panic!("expected LongFlag"),
3867 }
3868 }
3869 _ => panic!("expected Command"),
3870 }
3871 }
3872
3873 #[test]
3874 fn parse_long_flag_with_value() {
3875 let result = parse(r#"git commit --message="hello""#);
3876 assert!(result.is_ok());
3877 let program = result.expect("ok");
3878 match &program.statements[0] {
3879 Stmt::Command(cmd) => {
3880 assert_eq!(cmd.name, "git");
3881 assert_eq!(cmd.args.len(), 2);
3882 match &cmd.args[1] {
3883 Arg::Named { key, value } => {
3884 assert_eq!(key, "message");
3885 match value {
3886 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
3887 _ => panic!("expected String value"),
3888 }
3889 }
3890 _ => panic!("expected Named from --flag=value"),
3891 }
3892 }
3893 _ => panic!("expected Command"),
3894 }
3895 }
3896
3897 #[test]
3898 fn parse_mixed_flags_and_args() {
3899 let result = parse(r#"git commit -m "message" --amend"#);
3900 assert!(result.is_ok());
3901 let program = result.expect("ok");
3902 match &program.statements[0] {
3903 Stmt::Command(cmd) => {
3904 assert_eq!(cmd.name, "git");
3905 assert_eq!(cmd.args.len(), 4);
3906 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
3908 match &cmd.args[1] {
3910 Arg::ShortFlag(name) => assert_eq!(name, "m"),
3911 _ => panic!("expected ShortFlag -m"),
3912 }
3913 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
3915 match &cmd.args[3] {
3917 Arg::LongFlag(name) => assert_eq!(name, "amend"),
3918 _ => panic!("expected LongFlag --amend"),
3919 }
3920 }
3921 _ => panic!("expected Command"),
3922 }
3923 }
3924
3925 #[test]
3926 fn parse_redirect_stdout() {
3927 let result = parse("cmd > file");
3928 assert!(result.is_ok());
3929 let program = result.expect("ok");
3930 match &program.statements[0] {
3932 Stmt::Pipeline(p) => {
3933 assert_eq!(pipeline_commands(p).len(), 1);
3934 let cmd = pipeline_commands(p)[0];
3935 assert_eq!(cmd.redirects.len(), 1);
3936 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
3937 }
3938 _ => panic!("expected Pipeline"),
3939 }
3940 }
3941
3942 #[test]
3943 fn parse_var_ref() {
3944 let result = parse("echo ${VAR}");
3945 assert!(result.is_ok());
3946 let program = result.expect("ok");
3947 match &program.statements[0] {
3948 Stmt::Command(cmd) => {
3949 assert_eq!(cmd.args.len(), 1);
3950 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
3951 }
3952 _ => panic!("expected Command"),
3953 }
3954 }
3955
3956 #[test]
3957 fn parse_multiple_statements() {
3958 let result = parse("a\nb\nc");
3959 assert!(result.is_ok());
3960 let program = result.expect("ok");
3961 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3962 assert_eq!(non_empty.len(), 3);
3963 }
3964
3965 #[test]
3966 fn parse_semicolon_separated() {
3967 let result = parse("a; b; c");
3968 assert!(result.is_ok());
3969 let program = result.expect("ok");
3970 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3971 assert_eq!(non_empty.len(), 3);
3972 }
3973
3974 #[test]
3975 fn parse_complex_pipeline() {
3976 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
3977 assert!(result.is_ok());
3978 let program = result.expect("ok");
3979 match &program.statements[0] {
3980 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 3),
3981 _ => panic!("expected Pipeline"),
3982 }
3983 }
3984
3985 #[test]
3986 fn parse_json_as_string_arg() {
3987 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
3989 assert!(result.is_ok());
3990 }
3991
3992 #[test]
3993 fn parse_mixed_args() {
3994 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
3995 assert!(result.is_ok());
3996 let program = result.expect("ok");
3997 match &program.statements[0] {
3998 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
3999 _ => panic!("expected Command"),
4000 }
4001 }
4002
4003 #[test]
4004 fn error_unterminated_string() {
4005 let result = parse(r#"echo "hello"#);
4006 assert!(result.is_err());
4007 }
4008
4009 #[test]
4010 fn error_unterminated_var_ref() {
4011 let result = parse("echo ${VAR");
4012 assert!(result.is_err());
4013 }
4014
4015 #[test]
4016 fn error_missing_fi() {
4017 let result = parse("if true; then echo");
4018 assert!(result.is_err());
4019 }
4020
4021 #[test]
4022 fn error_missing_done() {
4023 let result = parse("for X in items; do echo");
4024 assert!(result.is_err());
4025 }
4026
4027 #[test]
4028 fn parse_lvalue_single_index() {
4029 let result = parse("xs[0]=9").unwrap();
4030 match &result.statements[0] {
4031 Stmt::Assignment(a) => {
4032 assert_eq!(a.name(), "xs");
4033 assert_eq!(
4034 a.path.segments,
4035 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
4036 );
4037 assert!(!a.local);
4038 }
4039 other => panic!("expected assignment, got {:?}", other),
4040 }
4041 }
4042
4043 #[test]
4044 fn parse_lvalue_negative_index() {
4045 let result = parse("xs[-1]=7").unwrap();
4046 match &result.statements[0] {
4047 Stmt::Assignment(a) => assert_eq!(
4048 a.path.segments,
4049 vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)]
4050 ),
4051 other => panic!("expected assignment, got {:?}", other),
4052 }
4053 }
4054
4055 #[test]
4056 fn parse_lvalue_bareword_key() {
4057 let result = parse("user[email]=x").unwrap();
4058 match &result.statements[0] {
4059 Stmt::Assignment(a) => assert_eq!(
4060 a.path.segments,
4061 vec![
4062 VarSegment::Field("user".into()),
4063 VarSegment::Key("email".into())
4064 ]
4065 ),
4066 other => panic!("expected assignment, got {:?}", other),
4067 }
4068 }
4069
4070 #[test]
4071 fn parse_lvalue_chained_keys() {
4072 let result = parse("s[web][port]=9000").unwrap();
4073 match &result.statements[0] {
4074 Stmt::Assignment(a) => assert_eq!(
4075 a.path.segments,
4076 vec![
4077 VarSegment::Field("s".into()),
4078 VarSegment::Key("web".into()),
4079 VarSegment::Key("port".into())
4080 ]
4081 ),
4082 other => panic!("expected assignment, got {:?}", other),
4083 }
4084 }
4085
4086 #[test]
4087 fn parse_lvalue_dynamic_key() {
4088 let result = parse("r[$k]=v").unwrap();
4089 match &result.statements[0] {
4090 Stmt::Assignment(a) => assert_eq!(
4091 a.path.segments,
4092 vec![
4093 VarSegment::Field("r".into()),
4094 VarSegment::Dynamic("k".into())
4095 ]
4096 ),
4097 other => panic!("expected assignment, got {:?}", other),
4098 }
4099 }
4100
4101 #[test]
4102 fn parse_local_lvalue_spaced() {
4103 let result = parse("local xs[0] = 9").unwrap();
4104 match &result.statements[0] {
4105 Stmt::Assignment(a) => {
4106 assert!(a.local);
4107 assert_eq!(
4108 a.path.segments,
4109 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
4110 );
4111 }
4112 other => panic!("expected assignment, got {:?}", other),
4113 }
4114 }
4115
4116 #[test]
4117 fn env_prefix_subscripted_target_is_not_captured_as_env_scoped() {
4118 let result = parse("user={}\nuser[email]=x echo hi").unwrap();
4128 for stmt in &result.statements {
4129 assert!(
4130 !matches!(stmt, Stmt::EnvScoped { .. }),
4131 "a subscripted assignment must never be captured into EnvScoped: {stmt:?}"
4132 );
4133 }
4134 assert!(matches!(&result.statements[1], Stmt::Assignment(a) if a.name() == "user"));
4136 assert!(matches!(&result.statements[2], Stmt::Command(c) if c.name == "echo"));
4137 }
4138
4139 #[test]
4140 fn parse_nested_cmd_subst() {
4141 let result = parse("X=$(echo $(date))").unwrap();
4143 match &result.statements[0] {
4144 Stmt::Assignment(a) => {
4145 assert_eq!(a.name(), "X");
4146 let outer = subst_cmd(&a.value);
4147 assert_eq!(outer.name, "echo");
4148 match &outer.args[0] {
4150 Arg::Positional(inner_expr) => {
4151 assert_eq!(subst_cmd(inner_expr).name, "date");
4152 }
4153 other => panic!("expected nested cmd subst arg, got {:?}", other),
4154 }
4155 }
4156 other => panic!("expected assignment, got {:?}", other),
4157 }
4158 }
4159
4160 #[test]
4161 fn parse_deeply_nested_cmd_subst() {
4162 let result = parse("X=$(a $(b $(c)))").unwrap();
4164 match &result.statements[0] {
4165 Stmt::Assignment(a) => {
4166 let level1 = subst_cmd(&a.value);
4167 assert_eq!(level1.name, "a");
4168 match &level1.args[0] {
4169 Arg::Positional(level2_expr) => {
4170 let level2 = subst_cmd(level2_expr);
4171 assert_eq!(level2.name, "b");
4172 match &level2.args[0] {
4173 Arg::Positional(level3_expr) => {
4174 assert_eq!(subst_cmd(level3_expr).name, "c");
4175 }
4176 other => panic!("expected level3 cmd subst, got {:?}", other),
4177 }
4178 }
4179 other => panic!("expected level2 cmd subst, got {:?}", other),
4180 }
4181 }
4182 other => panic!("expected assignment, got {:?}", other),
4183 }
4184 }
4185
4186 #[test]
4191 fn value_int_preserved() {
4192 let result = parse("X=42").unwrap();
4193 match &result.statements[0] {
4194 Stmt::Assignment(a) => {
4195 assert_eq!(a.name(), "X");
4196 match &a.value {
4197 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
4198 other => panic!("expected int literal, got {:?}", other),
4199 }
4200 }
4201 other => panic!("expected assignment, got {:?}", other),
4202 }
4203 }
4204
4205 #[test]
4206 fn value_negative_int_preserved() {
4207 let result = parse("X=-99").unwrap();
4208 match &result.statements[0] {
4209 Stmt::Assignment(a) => match &a.value {
4210 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
4211 other => panic!("expected int, got {:?}", other),
4212 },
4213 other => panic!("expected assignment, got {:?}", other),
4214 }
4215 }
4216
4217 #[test]
4218 fn value_float_preserved() {
4219 let result = parse("PI=3.14").unwrap();
4220 match &result.statements[0] {
4221 Stmt::Assignment(a) => match &a.value {
4222 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
4223 other => panic!("expected float, got {:?}", other),
4224 },
4225 other => panic!("expected assignment, got {:?}", other),
4226 }
4227 }
4228
4229 #[test]
4230 fn value_string_preserved() {
4231 let result = parse(r#"echo "hello world""#).unwrap();
4232 match &result.statements[0] {
4233 Stmt::Command(cmd) => {
4234 assert_eq!(cmd.name, "echo");
4235 match &cmd.args[0] {
4236 Arg::Positional(Expr::Literal(Value::String(s))) => {
4237 assert_eq!(s, "hello world");
4238 }
4239 other => panic!("expected string arg, got {:?}", other),
4240 }
4241 }
4242 other => panic!("expected command, got {:?}", other),
4243 }
4244 }
4245
4246 #[test]
4247 fn value_string_with_escapes_preserved() {
4248 let result = parse(r#"echo "line1\nline2""#).unwrap();
4249 match &result.statements[0] {
4250 Stmt::Command(cmd) => match &cmd.args[0] {
4251 Arg::Positional(Expr::Literal(Value::String(s))) => {
4252 assert_eq!(s, "line1\nline2");
4253 }
4254 other => panic!("expected string, got {:?}", other),
4255 },
4256 other => panic!("expected command, got {:?}", other),
4257 }
4258 }
4259
4260 #[test]
4261 fn value_command_name_preserved() {
4262 let result = parse("my-command").unwrap();
4263 match &result.statements[0] {
4264 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
4265 other => panic!("expected command, got {:?}", other),
4266 }
4267 }
4268
4269 #[test]
4270 fn value_assignment_name_preserved() {
4271 let result = parse("MY_VAR=1").unwrap();
4272 match &result.statements[0] {
4273 Stmt::Assignment(a) => assert_eq!(a.name(), "MY_VAR"),
4274 other => panic!("expected assignment, got {:?}", other),
4275 }
4276 }
4277
4278 #[test]
4279 fn value_for_variable_preserved() {
4280 let result = parse("for ITEM in items; do echo; done").unwrap();
4281 match &result.statements[0] {
4282 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
4283 other => panic!("expected for, got {:?}", other),
4284 }
4285 }
4286
4287 #[test]
4288 fn value_varref_name_preserved() {
4289 let result = parse("echo ${MESSAGE}").unwrap();
4290 match &result.statements[0] {
4291 Stmt::Command(cmd) => match &cmd.args[0] {
4292 Arg::Positional(Expr::VarRef(path)) => {
4293 assert_eq!(path.segments.len(), 1);
4294 let VarSegment::Field(name) = &path.segments[0] else {
4295 panic!("expected root field, got {:?}", path.segments[0]);
4296 };
4297 assert_eq!(name, "MESSAGE");
4298 }
4299 other => panic!("expected varref, got {:?}", other),
4300 },
4301 other => panic!("expected command, got {:?}", other),
4302 }
4303 }
4304
4305 #[test]
4306 fn value_varref_field_access_preserved() {
4307 let result = parse("echo ${RESULT.data}").unwrap();
4308 match &result.statements[0] {
4309 Stmt::Command(cmd) => match &cmd.args[0] {
4310 Arg::Positional(Expr::VarRef(path)) => {
4311 assert_eq!(path.segments.len(), 2);
4315 let VarSegment::Field(a) = &path.segments[0] else {
4316 panic!("expected field, got {:?}", path.segments[0]);
4317 };
4318 let VarSegment::Field(b) = &path.segments[1] else {
4319 panic!("expected field, got {:?}", path.segments[1]);
4320 };
4321 assert_eq!(a, "RESULT");
4322 assert_eq!(b, "data");
4323 }
4324 other => panic!("expected varref, got {:?}", other),
4325 },
4326 other => panic!("expected command, got {:?}", other),
4327 }
4328 }
4329
4330 #[test]
4331 fn value_varref_index_parsed() {
4332 let result = parse("echo ${ITEMS[0]}").unwrap();
4335 match &result.statements[0] {
4336 Stmt::Command(cmd) => match &cmd.args[0] {
4337 Arg::Positional(Expr::VarRef(path)) => {
4338 assert_eq!(path.segments.len(), 2);
4339 let VarSegment::Field(name) = &path.segments[0] else {
4340 panic!("expected root field, got {:?}", path.segments[0]);
4341 };
4342 assert_eq!(name, "ITEMS");
4343 assert_eq!(path.segments[1], VarSegment::Index(0));
4344 }
4345 other => panic!("expected varref, got {:?}", other),
4346 },
4347 other => panic!("expected command, got {:?}", other),
4348 }
4349 }
4350
4351 #[test]
4352 fn value_named_arg_preserved() {
4353 let result = parse("cmd count=42").unwrap();
4357 match &result.statements[0] {
4358 Stmt::Command(cmd) => {
4359 assert_eq!(cmd.name, "cmd");
4360 match &cmd.args[0] {
4361 Arg::WordAssign { key, value } => {
4362 assert_eq!(key, "count");
4363 match value {
4364 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
4365 other => panic!("expected int, got {:?}", other),
4366 }
4367 }
4368 other => panic!("expected WordAssign arg, got {:?}", other),
4369 }
4370 }
4371 other => panic!("expected command, got {:?}", other),
4372 }
4373 }
4374
4375 #[test]
4376 fn value_function_def_name_preserved() {
4377 let result = parse("greet() { echo }").unwrap();
4378 match &result.statements[0] {
4379 Stmt::ToolDef(t) => {
4380 assert_eq!(t.name, "greet");
4381 assert!(t.params.is_empty());
4382 }
4383 other => panic!("expected function def, got {:?}", other),
4384 }
4385 }
4386
4387 #[test]
4392 fn parse_comparison_equals() {
4393 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
4395 match &result.statements[0] {
4396 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4397 Expr::Test(test) => match test.as_ref() {
4398 TestExpr::Comparison { left, op, right } => {
4399 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
4400 assert_eq!(*op, TestCmpOp::Eq);
4401 match right.as_ref() {
4402 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
4403 other => panic!("expected int, got {:?}", other),
4404 }
4405 }
4406 other => panic!("expected comparison, got {:?}", other),
4407 },
4408 other => panic!("expected test expr, got {:?}", other),
4409 },
4410 other => panic!("expected if, got {:?}", other),
4411 }
4412 }
4413
4414 #[test]
4415 fn parse_comparison_not_equals() {
4416 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
4417 match &result.statements[0] {
4418 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4419 Expr::Test(test) => match test.as_ref() {
4420 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
4421 other => panic!("expected comparison, got {:?}", other),
4422 },
4423 other => panic!("expected test expr, got {:?}", other),
4424 },
4425 other => panic!("expected if, got {:?}", other),
4426 }
4427 }
4428
4429 #[test]
4430 fn parse_comparison_less_than() {
4431 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
4432 match &result.statements[0] {
4433 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4434 Expr::Test(test) => match test.as_ref() {
4435 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
4436 other => panic!("expected comparison, got {:?}", other),
4437 },
4438 other => panic!("expected test expr, got {:?}", other),
4439 },
4440 other => panic!("expected if, got {:?}", other),
4441 }
4442 }
4443
4444 #[test]
4445 fn parse_comparison_greater_than() {
4446 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
4447 match &result.statements[0] {
4448 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4449 Expr::Test(test) => match test.as_ref() {
4450 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
4451 other => panic!("expected comparison, got {:?}", other),
4452 },
4453 other => panic!("expected test expr, got {:?}", other),
4454 },
4455 other => panic!("expected if, got {:?}", other),
4456 }
4457 }
4458
4459 #[test]
4460 fn parse_comparison_less_equal() {
4461 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
4462 match &result.statements[0] {
4463 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4464 Expr::Test(test) => match test.as_ref() {
4465 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
4466 other => panic!("expected comparison, got {:?}", other),
4467 },
4468 other => panic!("expected test expr, got {:?}", other),
4469 },
4470 other => panic!("expected if, got {:?}", other),
4471 }
4472 }
4473
4474 #[test]
4475 fn parse_comparison_greater_equal() {
4476 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
4477 match &result.statements[0] {
4478 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4479 Expr::Test(test) => match test.as_ref() {
4480 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
4481 other => panic!("expected comparison, got {:?}", other),
4482 },
4483 other => panic!("expected test expr, got {:?}", other),
4484 },
4485 other => panic!("expected if, got {:?}", other),
4486 }
4487 }
4488
4489 #[test]
4490 fn parse_regex_match() {
4491 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
4492 match &result.statements[0] {
4493 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4494 Expr::Test(test) => match test.as_ref() {
4495 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
4496 other => panic!("expected comparison, got {:?}", other),
4497 },
4498 other => panic!("expected test expr, got {:?}", other),
4499 },
4500 other => panic!("expected if, got {:?}", other),
4501 }
4502 }
4503
4504 #[test]
4505 fn parse_regex_not_match() {
4506 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
4507 match &result.statements[0] {
4508 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4509 Expr::Test(test) => match test.as_ref() {
4510 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
4511 other => panic!("expected comparison, got {:?}", other),
4512 },
4513 other => panic!("expected test expr, got {:?}", other),
4514 },
4515 other => panic!("expected if, got {:?}", other),
4516 }
4517 }
4518
4519 #[test]
4520 fn parse_string_interpolation() {
4521 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
4522 match &result.statements[0] {
4523 Stmt::Command(cmd) => match &cmd.args[0] {
4524 Arg::Positional(Expr::Interpolated(parts)) => {
4525 assert_eq!(parts.len(), 3);
4526 match &parts[0] {
4527 StringPart::Literal(s) => assert_eq!(s, "Hello "),
4528 other => panic!("expected literal, got {:?}", other),
4529 }
4530 match &parts[1] {
4531 StringPart::Var(path) => {
4532 assert_eq!(path.segments.len(), 1);
4533 let VarSegment::Field(name) = &path.segments[0] else {
4534 panic!("expected root field, got {:?}", path.segments[0]);
4535 };
4536 assert_eq!(name, "NAME");
4537 }
4538 other => panic!("expected var, got {:?}", other),
4539 }
4540 match &parts[2] {
4541 StringPart::Literal(s) => assert_eq!(s, "!"),
4542 other => panic!("expected literal, got {:?}", other),
4543 }
4544 }
4545 other => panic!("expected interpolated, got {:?}", other),
4546 },
4547 other => panic!("expected command, got {:?}", other),
4548 }
4549 }
4550
4551 #[test]
4552 fn parse_string_interpolation_multiple_vars() {
4553 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
4554 match &result.statements[0] {
4555 Stmt::Command(cmd) => match &cmd.args[0] {
4556 Arg::Positional(Expr::Interpolated(parts)) => {
4557 assert_eq!(parts.len(), 3);
4559 assert!(matches!(&parts[0], StringPart::Var(_)));
4560 assert!(matches!(&parts[1], StringPart::Literal(_)));
4561 assert!(matches!(&parts[2], StringPart::Var(_)));
4562 }
4563 other => panic!("expected interpolated, got {:?}", other),
4564 },
4565 other => panic!("expected command, got {:?}", other),
4566 }
4567 }
4568
4569 #[test]
4570 fn parse_empty_function_body() {
4571 let result = parse("empty() { }").unwrap();
4572 match &result.statements[0] {
4573 Stmt::ToolDef(t) => {
4574 assert_eq!(t.name, "empty");
4575 assert!(t.params.is_empty());
4576 assert!(t.body.is_empty());
4577 }
4578 other => panic!("expected function def, got {:?}", other),
4579 }
4580 }
4581
4582 #[test]
4583 fn parse_bash_style_function() {
4584 let result = parse("function greet { echo hello }").unwrap();
4585 match &result.statements[0] {
4586 Stmt::ToolDef(t) => {
4587 assert_eq!(t.name, "greet");
4588 assert!(t.params.is_empty());
4589 assert_eq!(t.body.len(), 1);
4590 }
4591 other => panic!("expected function def, got {:?}", other),
4592 }
4593 }
4594
4595 #[test]
4596 fn parse_comparison_string_values() {
4597 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
4598 match &result.statements[0] {
4599 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4600 Expr::Test(test) => match test.as_ref() {
4601 TestExpr::Comparison { left, op, right } => {
4602 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
4603 assert_eq!(*op, TestCmpOp::Eq);
4604 match right.as_ref() {
4605 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
4606 other => panic!("expected string, got {:?}", other),
4607 }
4608 }
4609 other => panic!("expected comparison, got {:?}", other),
4610 },
4611 other => panic!("expected test expr, got {:?}", other),
4612 },
4613 other => panic!("expected if, got {:?}", other),
4614 }
4615 }
4616
4617 #[test]
4622 fn parse_cmd_subst_simple() {
4623 let result = parse("X=$(echo)").unwrap();
4624 match &result.statements[0] {
4625 Stmt::Assignment(a) => {
4626 assert_eq!(a.name(), "X");
4627 assert_eq!(subst_cmd(&a.value).name, "echo");
4628 }
4629 other => panic!("expected assignment, got {:?}", other),
4630 }
4631 }
4632
4633 #[test]
4634 fn parse_cmd_subst_with_args() {
4635 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
4636 match &result.statements[0] {
4637 Stmt::Assignment(a) => {
4638 let cmd = subst_cmd(&a.value);
4639 assert_eq!(cmd.name, "fetch");
4640 assert_eq!(cmd.args.len(), 1);
4641 match &cmd.args[0] {
4642 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
4643 other => panic!("expected WordAssign arg, got {:?}", other),
4644 }
4645 }
4646 other => panic!("expected assignment, got {:?}", other),
4647 }
4648 }
4649
4650 #[test]
4651 fn parse_cmd_subst_pipeline() {
4652 let result = parse("X=$(cat file | grep pattern)").unwrap();
4653 match &result.statements[0] {
4654 Stmt::Assignment(a) => {
4655 let pipeline = subst_pipeline(&a.value);
4656 assert_eq!(pipeline_commands(pipeline).len(), 2);
4657 assert_eq!(pipeline_commands(pipeline)[0].name, "cat");
4658 assert_eq!(pipeline_commands(pipeline)[1].name, "grep");
4659 }
4660 other => panic!("expected assignment, got {:?}", other),
4661 }
4662 }
4663
4664 #[test]
4665 fn parse_cmd_subst_with_redirect() {
4666 let result = parse("X=$(echo hi > out.txt)").unwrap();
4671 match &result.statements[0] {
4672 Stmt::Assignment(a) => {
4673 let pipeline = subst_pipeline(&a.value);
4674 assert_eq!(pipeline_commands(pipeline).len(), 1);
4675 let cmd = pipeline_commands(pipeline)[0];
4676 assert_eq!(cmd.name, "echo");
4677 assert_eq!(cmd.redirects.len(), 1);
4678 assert!(matches!(
4679 cmd.redirects[0].kind,
4680 RedirectKind::StdoutOverwrite
4681 ));
4682 }
4683 other => panic!("expected assignment, got {:?}", other),
4684 }
4685 }
4686
4687 #[test]
4688 fn parse_cmd_subst_redirect_target_with_nested_subst() {
4689 let result = parse("X=$(echo hi > $(echo f))").unwrap();
4695 match &result.statements[0] {
4696 Stmt::Assignment(a) => {
4697 let pipeline = subst_pipeline(&a.value);
4698 assert_eq!(pipeline_commands(pipeline).len(), 1);
4699 let cmd = pipeline_commands(pipeline)[0];
4700 assert_eq!(cmd.name, "echo");
4701 assert_eq!(cmd.redirects.len(), 1);
4702 assert!(
4703 matches!(cmd.redirects[0].target, Expr::CommandSubst(_)),
4704 "redirect target should be a nested command substitution, got {:?}",
4705 cmd.redirects[0].target
4706 );
4707 }
4708 other => panic!("expected assignment, got {:?}", other),
4709 }
4710 }
4711
4712 #[test]
4713 fn parse_cmd_subst_chain_with_redirect() {
4714 let result = parse("X=$(echo a && echo b > out.txt)").unwrap();
4718 let stmts = match &result.statements[0] {
4719 Stmt::Assignment(a) => match &a.value {
4720 Expr::CommandSubst(s) => s,
4721 other => panic!("expected command subst, got {:?}", other),
4722 },
4723 other => panic!("expected assignment, got {:?}", other),
4724 };
4725 match stmts.as_slice() {
4726 [Stmt::AndChain { left, right }] => {
4727 assert!(
4729 matches!(**left, Stmt::Command(_)),
4730 "left of && should be a bare command, got {:?}",
4731 left
4732 );
4733 match &**right {
4735 Stmt::Pipeline(p) => {
4736 assert_eq!(pipeline_commands(p).len(), 1);
4737 assert_eq!(pipeline_commands(p)[0].name, "echo");
4738 assert_eq!(pipeline_commands(p)[0].redirects.len(), 1);
4739 }
4740 other => panic!("right should be a redirect-bearing pipeline, got {:?}", other),
4741 }
4742 }
4743 other => panic!("expected a single AndChain, got {:?}", other),
4744 }
4745 }
4746
4747 #[test]
4748 fn parse_cmd_subst_in_condition() {
4749 let result = parse("if kaish-validate; then echo; fi").unwrap();
4751 match &result.statements[0] {
4752 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4753 Expr::Command(cmd) => {
4754 assert_eq!(cmd.name, "kaish-validate");
4755 }
4756 other => panic!("expected command, got {:?}", other),
4757 },
4758 other => panic!("expected if, got {:?}", other),
4759 }
4760 }
4761
4762 #[test]
4777 fn parse_cmd_subst_unquoted_for_loop() {
4778 let result = parse("X=$(for f in a b; do echo $f; done)").unwrap();
4779 let stmts = match &result.statements[0] {
4780 Stmt::Assignment(a) => match &a.value {
4781 Expr::CommandSubst(s) => s,
4782 other => panic!("expected command subst, got {:?}", other),
4783 },
4784 other => panic!("expected assignment, got {:?}", other),
4785 };
4786 match stmts.as_slice() {
4787 [Stmt::For(f)] => {
4788 assert_eq!(f.variable, "f");
4789 assert_eq!(f.items.len(), 2);
4790 assert!(matches!(f.body.as_slice(), [Stmt::Command(c)] if c.name == "echo"));
4791 }
4792 other => panic!("expected a single For statement, got {:?}", other),
4793 }
4794 }
4795
4796 #[test]
4797 fn parse_cmd_subst_unquoted_while_loop() {
4798 let result = parse("X=$(while false; do echo x; done)").unwrap();
4799 let stmts = match &result.statements[0] {
4800 Stmt::Assignment(a) => match &a.value {
4801 Expr::CommandSubst(s) => s,
4802 other => panic!("expected command subst, got {:?}", other),
4803 },
4804 other => panic!("expected assignment, got {:?}", other),
4805 };
4806 assert!(
4807 matches!(stmts.as_slice(), [Stmt::While(w)] if matches!(w.body.as_slice(), [Stmt::Command(c)] if c.name == "echo")),
4808 "expected a single While statement, got {stmts:?}"
4809 );
4810 }
4811
4812 #[test]
4813 fn parse_cmd_subst_unquoted_if_else() {
4814 let result = parse("X=$(if true; then echo one; else echo two; fi)").unwrap();
4815 let stmts = match &result.statements[0] {
4816 Stmt::Assignment(a) => match &a.value {
4817 Expr::CommandSubst(s) => s,
4818 other => panic!("expected command subst, got {:?}", other),
4819 },
4820 other => panic!("expected assignment, got {:?}", other),
4821 };
4822 match stmts.as_slice() {
4823 [Stmt::If(i)] => {
4824 assert!(i.else_branch.is_some(), "expected an else branch");
4825 assert!(matches!(i.then_branch.as_slice(), [Stmt::Command(c)] if c.name == "echo"));
4826 }
4827 other => panic!("expected a single If statement, got {:?}", other),
4828 }
4829 }
4830
4831 #[test]
4832 fn parse_cmd_subst_unquoted_case() {
4833 let result = parse("X=$(case a in a) echo hit;; esac)").unwrap();
4838 let stmts = match &result.statements[0] {
4839 Stmt::Assignment(a) => match &a.value {
4840 Expr::CommandSubst(s) => s,
4841 other => panic!("expected command subst, got {:?}", other),
4842 },
4843 other => panic!("expected assignment, got {:?}", other),
4844 };
4845 match stmts.as_slice() {
4846 [Stmt::Case(c)] => {
4847 assert_eq!(c.branches.len(), 1);
4848 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
4849 }
4850 other => panic!("expected a single Case statement, got {:?}", other),
4851 }
4852 }
4853
4854 #[test]
4855 fn parse_cmd_subst_unquoted_case_with_parenthesized_pattern() {
4856 let result = parse("X=$(case a in (a) echo hit;; esac)").unwrap();
4859 let stmts = match &result.statements[0] {
4860 Stmt::Assignment(a) => match &a.value {
4861 Expr::CommandSubst(s) => s,
4862 other => panic!("expected command subst, got {:?}", other),
4863 },
4864 other => panic!("expected assignment, got {:?}", other),
4865 };
4866 assert!(
4867 matches!(stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
4868 "expected a single Case statement, got {stmts:?}"
4869 );
4870 }
4871
4872 #[test]
4873 fn parse_cmd_subst_unquoted_case_parenthesized_pattern_with_bareword_esac_in_body() {
4874 let result = parse("X=$(case a in (a) y=esac;; b) echo two;; esac)").unwrap();
4885 let stmts = match &result.statements[0] {
4886 Stmt::Assignment(a) => match &a.value {
4887 Expr::CommandSubst(s) => s,
4888 other => panic!("expected command subst, got {:?}", other),
4889 },
4890 other => panic!("expected assignment, got {:?}", other),
4891 };
4892 match stmts.as_slice() {
4893 [Stmt::Case(c)] => {
4894 assert_eq!(c.branches.len(), 2);
4895 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
4896 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
4897 }
4898 other => panic!("expected a single Case statement with two branches, got {:?}", other),
4899 }
4900 }
4901
4902 #[test]
4903 fn parse_cmd_subst_unquoted_nested_case_parenthesized_pattern_esac_in_outer_body() {
4904 let result = parse(
4915 "X=$(case a in (a) case b in (b) echo z;; esac; y=esac;; c) echo two;; esac)",
4916 )
4917 .unwrap();
4918 let stmts = match &result.statements[0] {
4919 Stmt::Assignment(a) => match &a.value {
4920 Expr::CommandSubst(s) => s,
4921 other => panic!("expected command subst, got {:?}", other),
4922 },
4923 other => panic!("expected assignment, got {:?}", other),
4924 };
4925 match stmts.as_slice() {
4926 [Stmt::Case(c)] => {
4927 assert_eq!(c.branches.len(), 2);
4928 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
4929 assert_eq!(c.branches[1].patterns, vec!["c".to_string()]);
4930 }
4931 other => panic!("expected a single Case statement with two branches, got {:?}", other),
4932 }
4933 }
4934
4935 #[test]
4936 fn parse_cmd_subst_unquoted_case_eq_argv_key() {
4937 let result = parse("X=$(echo case=x)").unwrap();
4946 let stmts = match &result.statements[0] {
4947 Stmt::Assignment(a) => match &a.value {
4948 Expr::CommandSubst(s) => s,
4949 other => panic!("expected command subst, got {:?}", other),
4950 },
4951 other => panic!("expected assignment, got {:?}", other),
4952 };
4953 let cmd = match stmts.as_slice() {
4954 [Stmt::Command(c)] => c,
4955 other => panic!("expected a single echo command, got {:?}", other),
4956 };
4957 assert_eq!(cmd.name, "echo");
4958 match &cmd.args[0] {
4959 Arg::WordAssign { key, value } => {
4960 assert_eq!(key, "case");
4961 match value {
4962 Expr::Literal(Value::String(s)) => assert_eq!(s, "x"),
4963 other => panic!("expected string \"x\", got {:?}", other),
4964 }
4965 }
4966 other => panic!("expected WordAssign arg, got {:?}", other),
4967 }
4968 }
4969
4970 #[test]
4971 fn parse_cmd_subst_unquoted_case_eq_argv_key_with_sibling_keyword_keys() {
4972 let result = parse("X=$(tool case=x do=y)").unwrap();
4976 let stmts = match &result.statements[0] {
4977 Stmt::Assignment(a) => match &a.value {
4978 Expr::CommandSubst(s) => s,
4979 other => panic!("expected command subst, got {:?}", other),
4980 },
4981 other => panic!("expected assignment, got {:?}", other),
4982 };
4983 let cmd = match stmts.as_slice() {
4984 [Stmt::Command(c)] => c,
4985 other => panic!("expected a single tool command, got {:?}", other),
4986 };
4987 assert_eq!(cmd.name, "tool");
4988 assert_eq!(cmd.args.len(), 2);
4989 assert!(matches!(&cmd.args[0], Arg::WordAssign { key, .. } if key == "case"));
4990 assert!(matches!(&cmd.args[1], Arg::WordAssign { key, .. } if key == "do"));
4991 }
4992
4993 #[test]
4994 fn parse_cmd_subst_unquoted_case_inside_nested_subst() {
4995 let result = parse("X=$(echo $(case b in b) echo x;; esac))").unwrap();
5002 let outer_stmts = match &result.statements[0] {
5003 Stmt::Assignment(a) => match &a.value {
5004 Expr::CommandSubst(s) => s,
5005 other => panic!("expected command subst, got {:?}", other),
5006 },
5007 other => panic!("expected assignment, got {:?}", other),
5008 };
5009 let outer_cmd = match outer_stmts.as_slice() {
5010 [Stmt::Command(c)] => c,
5011 other => panic!("expected a single echo command, got {:?}", other),
5012 };
5013 assert_eq!(outer_cmd.name, "echo");
5014 let inner_stmts = match &outer_cmd.args[0] {
5015 Arg::Positional(Expr::CommandSubst(s)) => s,
5016 other => panic!("expected nested command subst arg, got {:?}", other),
5017 };
5018 assert!(
5019 matches!(inner_stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
5020 "expected a single Case statement inside the inner $(), got {inner_stmts:?}"
5021 );
5022 }
5023
5024 #[test]
5025 fn parse_cmd_subst_unquoted_esac_as_bareword() {
5026 let result = parse("X=$(echo esac)").unwrap();
5031 let stmts = match &result.statements[0] {
5032 Stmt::Assignment(a) => match &a.value {
5033 Expr::CommandSubst(s) => s,
5034 other => panic!("expected command subst, got {:?}", other),
5035 },
5036 other => panic!("expected assignment, got {:?}", other),
5037 };
5038 let cmd = match stmts.as_slice() {
5039 [Stmt::Command(c)] => c,
5040 other => panic!("expected a single echo command, got {:?}", other),
5041 };
5042 assert_eq!(cmd.name, "echo");
5043 assert!(
5044 matches!(&cmd.args[0], Arg::Positional(Expr::Literal(Value::String(s))) if s == "esac"),
5045 "expected \"esac\" as a literal argument, got {:?}",
5046 cmd.args[0]
5047 );
5048 }
5049
5050 #[test]
5051 fn parse_cmd_subst_unquoted_esac_as_bareword_inside_still_open_case() {
5052 let result = parse("X=$(case a in a) y=esac;; b) echo two;; esac)").unwrap();
5061 let stmts = match &result.statements[0] {
5062 Stmt::Assignment(a) => match &a.value {
5063 Expr::CommandSubst(s) => s,
5064 other => panic!("expected command subst, got {:?}", other),
5065 },
5066 other => panic!("expected assignment, got {:?}", other),
5067 };
5068 match stmts.as_slice() {
5069 [Stmt::Case(c)] => {
5070 assert_eq!(c.branches.len(), 2);
5071 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5072 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
5073 }
5074 other => panic!("expected a single Case statement with two branches, got {:?}", other),
5075 }
5076 }
5077
5078 #[test]
5079 fn parse_quoted_cmd_subst_case_pattern_paren_not_miscounted() {
5080 let result = parse(r#"X="pre $(case v in v) echo x;; esac) post""#).unwrap();
5089 let parts = match &result.statements[0] {
5090 Stmt::Assignment(a) => match &a.value {
5091 Expr::Interpolated(parts) => parts,
5092 other => panic!("expected an interpolated string, got {:?}", other),
5093 },
5094 other => panic!("expected assignment, got {:?}", other),
5095 };
5096 let stmts = match parts.as_slice() {
5097 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5098 {
5099 assert_eq!(pre, "pre ");
5100 assert_eq!(post, " post");
5101 stmts
5102 }
5103 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5104 };
5105 assert!(
5106 matches!(stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
5107 "expected a single Case statement inside the quoted $(...), got {stmts:?}"
5108 );
5109 }
5110
5111 #[test]
5112 fn parse_quoted_cmd_subst_case_parenthesized_pattern_esac_in_body_not_miscounted() {
5113 let result = parse(r#"X="pre $(case a in (a) y=esac;; b) echo two;; esac) post""#).unwrap();
5117 let parts = match &result.statements[0] {
5118 Stmt::Assignment(a) => match &a.value {
5119 Expr::Interpolated(parts) => parts,
5120 other => panic!("expected an interpolated string, got {:?}", other),
5121 },
5122 other => panic!("expected assignment, got {:?}", other),
5123 };
5124 let stmts = match parts.as_slice() {
5125 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5126 {
5127 assert_eq!(pre, "pre ");
5128 assert_eq!(post, " post");
5129 stmts
5130 }
5131 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5132 };
5133 match stmts.as_slice() {
5134 [Stmt::Case(c)] => {
5135 assert_eq!(c.branches.len(), 2);
5136 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5137 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
5138 }
5139 other => panic!("expected a single Case statement with two branches, got {:?}", other),
5140 }
5141 }
5142
5143 #[test]
5144 fn parse_quoted_cmd_subst_case_eq_argv_key_not_miscounted() {
5145 let result = parse(r#"X="pre $(echo case=x) post""#).unwrap();
5148 let parts = match &result.statements[0] {
5149 Stmt::Assignment(a) => match &a.value {
5150 Expr::Interpolated(parts) => parts,
5151 other => panic!("expected an interpolated string, got {:?}", other),
5152 },
5153 other => panic!("expected assignment, got {:?}", other),
5154 };
5155 let stmts = match parts.as_slice() {
5156 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5157 {
5158 assert_eq!(pre, "pre ");
5159 assert_eq!(post, " post");
5160 stmts
5161 }
5162 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5163 };
5164 let cmd = match stmts.as_slice() {
5165 [Stmt::Command(c)] => c,
5166 other => panic!("expected a single echo command, got {:?}", other),
5167 };
5168 assert_eq!(cmd.name, "echo");
5169 assert!(matches!(&cmd.args[0], Arg::WordAssign { key, .. } if key == "case"));
5170 }
5171
5172 #[test]
5173 fn parse_cmd_subst_unquoted_nested_with_control_structure() {
5174 let result = parse("X=$(echo $(for f in a; do echo $f; done))").unwrap();
5177 let outer_stmts = match &result.statements[0] {
5178 Stmt::Assignment(a) => match &a.value {
5179 Expr::CommandSubst(s) => s,
5180 other => panic!("expected command subst, got {:?}", other),
5181 },
5182 other => panic!("expected assignment, got {:?}", other),
5183 };
5184 let outer_cmd = match outer_stmts.as_slice() {
5185 [Stmt::Command(c)] => c,
5186 other => panic!("expected a single echo command, got {:?}", other),
5187 };
5188 assert_eq!(outer_cmd.name, "echo");
5189 let inner_stmts = match &outer_cmd.args[0] {
5190 Arg::Positional(Expr::CommandSubst(s)) => s,
5191 other => panic!("expected nested command subst arg, got {:?}", other),
5192 };
5193 assert!(
5194 matches!(inner_stmts.as_slice(), [Stmt::For(f)] if f.variable == "f"),
5195 "expected a single For statement inside the inner $(), got {inner_stmts:?}"
5196 );
5197 }
5198
5199 #[test]
5200 fn parse_cmd_subst_unquoted_pipeline() {
5201 let result = parse("X=$(cat f | grep pat | wc -l)").unwrap();
5206 let value = match &result.statements[0] {
5207 Stmt::Assignment(a) => a.value.clone(),
5208 other => panic!("expected assignment, got {:?}", other),
5209 };
5210 let pipeline = subst_pipeline(&value);
5211 assert_eq!(pipeline_commands(pipeline).len(), 3);
5212 assert_eq!(pipeline_commands(pipeline)[2].name, "wc");
5213 }
5214
5215 #[test]
5216 fn parse_quoted_cmd_subst_with_for_loop_still_works() {
5217 let result = parse(r#"out="$(for f in a b; do echo $f; done)""#).unwrap();
5221 match &result.statements[0] {
5222 Stmt::Assignment(a) => assert_eq!(a.name(), "out"),
5223 other => panic!("expected assignment, got {:?}", other),
5224 }
5225 }
5226
5227 #[test]
5228 fn parse_cmd_subst_body_error_reports_span_inside_body_not_at_dollar_paren() {
5229 let source = "echo $(for f in a; do echo $f done)";
5237 let errs = parse(source).expect_err("missing loop terminator must be a parse error");
5238 let dollar_paren = source.find("$(").expect("fixture contains $(");
5239 assert!(
5240 errs.iter().all(|e| e.span.start > dollar_paren + 1),
5241 "error span must point inside the $() body, not at '$(' itself: {errs:?}"
5242 );
5243 assert!(
5246 errs.iter().any(|e| e.message.contains("done")),
5247 "expected the missing-`done` diagnostic, got: {errs:?}"
5248 );
5249 }
5250
5251 #[test]
5252 fn parse_cmd_subst_unterminated_reports_error() {
5253 let result = parse("echo $(for f in a; do echo $f; done");
5254 assert!(result.is_err(), "a missing `)` must be a parse error");
5255 }
5256
5257 #[test]
5262 fn parse_env_prefix_single() {
5263 let result = parse("FOO=bar echo hi").unwrap();
5264 match &result.statements[0] {
5265 Stmt::EnvScoped { assignments, body } => {
5266 assert_eq!(assignments.len(), 1);
5267 assert_eq!(assignments[0].name(), "FOO");
5268 assert!(!assignments[0].local);
5269 match body.as_ref() {
5270 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
5271 other => panic!("expected command body, got {other:?}"),
5272 }
5273 }
5274 other => panic!("expected env-scoped, got {other:?}"),
5275 }
5276 }
5277
5278 #[test]
5279 fn parse_env_prefix_multiple() {
5280 let result = parse("A=1 B=2 run").unwrap();
5281 match &result.statements[0] {
5282 Stmt::EnvScoped { assignments, body } => {
5283 assert_eq!(assignments.len(), 2);
5284 assert_eq!(assignments[0].name(), "A");
5285 assert_eq!(assignments[1].name(), "B");
5286 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
5287 }
5288 other => panic!("expected env-scoped, got {other:?}"),
5289 }
5290 }
5291
5292 #[test]
5293 fn parse_bare_assignment_is_not_env_scoped() {
5294 let result = parse("FOO=bar").unwrap();
5296 assert!(
5297 matches!(&result.statements[0], Stmt::Assignment(a) if a.name() == "FOO"),
5298 "got {:?}",
5299 result.statements[0]
5300 );
5301 }
5302
5303 #[test]
5304 fn parse_assignment_then_and_chain_does_not_over_capture() {
5305 let result = parse("FOO=bar && echo hi").unwrap();
5308 match &result.statements[0] {
5309 Stmt::AndChain { left, right } => {
5310 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name() == "FOO"));
5311 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
5312 }
5313 other => panic!("expected and-chain, got {other:?}"),
5314 }
5315 }
5316
5317 #[test]
5318 fn parse_env_prefix_pipeline_body() {
5319 let result = parse("FOO=bar cat | grep x").unwrap();
5320 match &result.statements[0] {
5321 Stmt::EnvScoped { assignments, body } => {
5322 assert_eq!(assignments[0].name(), "FOO");
5323 match body.as_ref() {
5324 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 2),
5325 other => panic!("expected pipeline body, got {other:?}"),
5326 }
5327 }
5328 other => panic!("expected env-scoped, got {other:?}"),
5329 }
5330 }
5331
5332 fn parse_err_message(source: &str) -> String {
5337 parse(source)
5338 .expect_err("expected a parse error")
5339 .iter()
5340 .map(|e| e.message.clone())
5341 .collect::<Vec<_>>()
5342 .join(" ")
5343 }
5344
5345 #[test]
5346 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
5347 let msg = parse_err_message("echo /tmp/$(echo x).txt");
5350 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
5351 }
5352
5353 #[test]
5354 fn argv_splat_var_glued_to_path_is_rejected() {
5355 assert!(parse("echo $dir/out.txt").is_err());
5356 }
5357
5358 #[test]
5359 fn argv_splat_three_way_glue_is_rejected() {
5360 assert!(parse("echo foo$(echo bar)baz").is_err());
5361 }
5362
5363 #[test]
5364 fn argv_splat_quoted_word_is_accepted() {
5365 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
5367 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
5368 }
5369
5370 #[test]
5371 fn argv_single_token_words_are_not_splat() {
5372 assert!(parse("echo file.txt").is_ok(), "file.txt");
5374 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
5375 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
5376 }
5377
5378 #[test]
5379 fn argv_spaced_words_are_not_splat() {
5380 assert!(parse("echo a b c").is_ok());
5381 assert!(parse("echo /tmp/x $(echo y)").is_ok());
5382 }
5383
5384 #[test]
5385 fn parse_cmd_subst_in_command_arg() {
5386 let result = parse("echo $(whoami)").unwrap();
5387 match &result.statements[0] {
5388 Stmt::Command(cmd) => {
5389 assert_eq!(cmd.name, "echo");
5390 match &cmd.args[0] {
5391 Arg::Positional(expr) => {
5392 assert_eq!(subst_cmd(expr).name, "whoami");
5393 }
5394 other => panic!("expected command subst, got {:?}", other),
5395 }
5396 }
5397 other => panic!("expected command, got {:?}", other),
5398 }
5399 }
5400
5401 #[test]
5406 fn parse_condition_and() {
5407 let result = parse("if check-a && check-b; then echo; fi").unwrap();
5409 match &result.statements[0] {
5410 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5411 Expr::BinaryOp { left, op, right } => {
5412 assert_eq!(*op, BinaryOp::And);
5413 assert!(matches!(left.as_ref(), Expr::Command(_)));
5414 assert!(matches!(right.as_ref(), Expr::Command(_)));
5415 }
5416 other => panic!("expected binary op, got {:?}", other),
5417 },
5418 other => panic!("expected if, got {:?}", other),
5419 }
5420 }
5421
5422 #[test]
5423 fn parse_condition_or() {
5424 let result = parse("if try-a || try-b; then echo; fi").unwrap();
5425 match &result.statements[0] {
5426 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5427 Expr::BinaryOp { left, op, right } => {
5428 assert_eq!(*op, BinaryOp::Or);
5429 assert!(matches!(left.as_ref(), Expr::Command(_)));
5430 assert!(matches!(right.as_ref(), Expr::Command(_)));
5431 }
5432 other => panic!("expected binary op, got {:?}", other),
5433 },
5434 other => panic!("expected if, got {:?}", other),
5435 }
5436 }
5437
5438 #[test]
5439 fn parse_condition_and_or_precedence() {
5440 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
5442 match &result.statements[0] {
5443 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5444 Expr::BinaryOp { left, op, right } => {
5445 assert_eq!(*op, BinaryOp::Or);
5447 match left.as_ref() {
5449 Expr::BinaryOp { op: inner_op, .. } => {
5450 assert_eq!(*inner_op, BinaryOp::And);
5451 }
5452 other => panic!("expected binary op (&&), got {:?}", other),
5453 }
5454 assert!(matches!(right.as_ref(), Expr::Command(_)));
5456 }
5457 other => panic!("expected binary op, got {:?}", other),
5458 },
5459 other => panic!("expected if, got {:?}", other),
5460 }
5461 }
5462
5463 #[test]
5464 fn parse_condition_multiple_and() {
5465 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
5466 match &result.statements[0] {
5467 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5468 Expr::BinaryOp { left, op, .. } => {
5469 assert_eq!(*op, BinaryOp::And);
5470 match left.as_ref() {
5472 Expr::BinaryOp { op: inner_op, .. } => {
5473 assert_eq!(*inner_op, BinaryOp::And);
5474 }
5475 other => panic!("expected binary op, got {:?}", other),
5476 }
5477 }
5478 other => panic!("expected binary op, got {:?}", other),
5479 },
5480 other => panic!("expected if, got {:?}", other),
5481 }
5482 }
5483
5484 #[test]
5485 fn parse_condition_mixed_comparison_and_logical() {
5486 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
5488 match &result.statements[0] {
5489 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5490 Expr::BinaryOp { left, op, right } => {
5491 assert_eq!(*op, BinaryOp::And);
5492 match left.as_ref() {
5494 Expr::Test(test) => match test.as_ref() {
5495 TestExpr::Comparison { op: left_op, .. } => {
5496 assert_eq!(*left_op, TestCmpOp::Eq);
5497 }
5498 other => panic!("expected comparison, got {:?}", other),
5499 },
5500 other => panic!("expected test, got {:?}", other),
5501 }
5502 match right.as_ref() {
5504 Expr::Test(test) => match test.as_ref() {
5505 TestExpr::Comparison { op: right_op, .. } => {
5506 assert_eq!(*right_op, TestCmpOp::NumGt);
5507 }
5508 other => panic!("expected comparison, got {:?}", other),
5509 },
5510 other => panic!("expected test, got {:?}", other),
5511 }
5512 }
5513 other => panic!("expected binary op, got {:?}", other),
5514 },
5515 other => panic!("expected if, got {:?}", other),
5516 }
5517 }
5518
5519 #[test]
5525 fn script_level1_linear() {
5526 let script = r#"
5527NAME="kaish"
5528VERSION=1
5529TIMEOUT=30
5530ITEMS="alpha beta gamma"
5531
5532echo "Starting ${NAME} v${VERSION}"
5533cat "README.md" | grep pattern="install" | head count=5
5534fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
5535echo "Items: ${ITEMS}"
5536"#;
5537 let result = parse(script).unwrap();
5538 let stmts: Vec<_> = result.statements.iter()
5539 .filter(|s| !matches!(s, Stmt::Empty))
5540 .collect();
5541
5542 assert_eq!(stmts.len(), 8);
5543 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(_))); }
5552
5553 #[test]
5555 fn script_level2_branching() {
5556 let script = r#"
5557RESULT=$(kaish-validate "input.json")
5558
5559if [[ ${RESULT.ok} == true ]]; then
5560 echo "Validation passed"
5561 process "input.json" > "output.json"
5562else
5563 echo "Validation failed: ${RESULT.err}"
5564fi
5565
5566if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
5567 echo "Count in valid range"
5568fi
5569
5570if check-network || check-cache; then
5571 fetch url=${URL}
5572fi
5573"#;
5574 let result = parse(script).unwrap();
5575 let stmts: Vec<_> = result.statements.iter()
5576 .filter(|s| !matches!(s, Stmt::Empty))
5577 .collect();
5578
5579 assert_eq!(stmts.len(), 4);
5580
5581 match stmts[0] {
5583 Stmt::Assignment(a) => {
5584 assert_eq!(a.name(), "RESULT");
5585 assert!(matches!(&a.value, Expr::CommandSubst(_)));
5586 }
5587 other => panic!("expected assignment, got {:?}", other),
5588 }
5589
5590 match stmts[1] {
5592 Stmt::If(if_stmt) => {
5593 assert_eq!(if_stmt.then_branch.len(), 2);
5594 assert!(if_stmt.else_branch.is_some());
5595 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
5596 }
5597 other => panic!("expected if, got {:?}", other),
5598 }
5599
5600 match stmts[2] {
5602 Stmt::If(if_stmt) => {
5603 match if_stmt.condition.as_ref() {
5604 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
5605 other => panic!("expected && condition, got {:?}", other),
5606 }
5607 }
5608 other => panic!("expected if, got {:?}", other),
5609 }
5610
5611 match stmts[3] {
5613 Stmt::If(if_stmt) => {
5614 match if_stmt.condition.as_ref() {
5615 Expr::BinaryOp { op, left, right } => {
5616 assert_eq!(*op, BinaryOp::Or);
5617 assert!(matches!(left.as_ref(), Expr::Command(_)));
5618 assert!(matches!(right.as_ref(), Expr::Command(_)));
5619 }
5620 other => panic!("expected || condition, got {:?}", other),
5621 }
5622 }
5623 other => panic!("expected if, got {:?}", other),
5624 }
5625 }
5626
5627 #[test]
5629 fn script_level3_loops_and_functions() {
5630 let script = r#"
5631greet() {
5632 echo "Hello, $1!"
5633}
5634
5635fetch_all() {
5636 for URL in $@; do
5637 fetch url=${URL}
5638 done
5639}
5640
5641USERS="alice bob charlie"
5642
5643for USER in ${USERS}; do
5644 greet ${USER}
5645 if [[ ${USER} == "bob" ]]; then
5646 echo "Found Bob!"
5647 fi
5648done
5649
5650long-running-task &
5651"#;
5652 let result = parse(script).unwrap();
5653 let stmts: Vec<_> = result.statements.iter()
5654 .filter(|s| !matches!(s, Stmt::Empty))
5655 .collect();
5656
5657 assert_eq!(stmts.len(), 5);
5658
5659 match stmts[0] {
5661 Stmt::ToolDef(t) => {
5662 assert_eq!(t.name, "greet");
5663 assert!(t.params.is_empty());
5664 }
5665 other => panic!("expected function def, got {:?}", other),
5666 }
5667
5668 match stmts[1] {
5670 Stmt::ToolDef(t) => {
5671 assert_eq!(t.name, "fetch_all");
5672 assert_eq!(t.body.len(), 1);
5673 assert!(matches!(&t.body[0], Stmt::For(_)));
5674 }
5675 other => panic!("expected function def, got {:?}", other),
5676 }
5677
5678 assert!(matches!(stmts[2], Stmt::Assignment(_)));
5680
5681 match stmts[3] {
5683 Stmt::For(f) => {
5684 assert_eq!(f.variable, "USER");
5685 assert_eq!(f.body.len(), 2);
5686 assert!(matches!(&f.body[0], Stmt::Command(_)));
5687 assert!(matches!(&f.body[1], Stmt::If(_)));
5688 }
5689 other => panic!("expected for loop, got {:?}", other),
5690 }
5691
5692 match stmts[4] {
5694 Stmt::Pipeline(p) => {
5695 assert!(p.background);
5696 assert_eq!(pipeline_commands(p)[0].name, "long-running-task");
5697 }
5698 other => panic!("expected pipeline (background), got {:?}", other),
5699 }
5700 }
5701
5702 #[test]
5704 fn script_level4_complex_nesting() {
5705 let script = r#"
5706RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
5707
5708if ping host=${HOST} && [[ ${RESULT} == true ]]; then
5709 for SERVER in "prod-1 prod-2"; do
5710 deploy target=${SERVER} port=8080
5711 if [[ $? -ne 0 ]]; then
5712 notify channel="ops" message="Deploy failed"
5713 fi
5714 done
5715fi
5716"#;
5717 let result = parse(script).unwrap();
5718 let stmts: Vec<_> = result.statements.iter()
5719 .filter(|s| !matches!(s, Stmt::Empty))
5720 .collect();
5721
5722 assert_eq!(stmts.len(), 2);
5723
5724 match stmts[0] {
5726 Stmt::Assignment(a) => {
5727 assert_eq!(a.name(), "RESULT");
5728 assert_eq!(pipeline_commands(subst_pipeline(&a.value)).len(), 3);
5729 }
5730 other => panic!("expected assignment, got {:?}", other),
5731 }
5732
5733 match stmts[1] {
5735 Stmt::If(if_stmt) => {
5736 match if_stmt.condition.as_ref() {
5737 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
5738 other => panic!("expected && condition, got {:?}", other),
5739 }
5740 assert_eq!(if_stmt.then_branch.len(), 1);
5741 match &if_stmt.then_branch[0] {
5742 Stmt::For(f) => {
5743 assert_eq!(f.body.len(), 2);
5744 assert!(matches!(&f.body[1], Stmt::If(_)));
5745 }
5746 other => panic!("expected for in if body, got {:?}", other),
5747 }
5748 }
5749 other => panic!("expected if, got {:?}", other),
5750 }
5751 }
5752
5753 #[test]
5755 fn script_level5_edge_cases() {
5756 let script = r#"
5757echo ""
5758echo "quotes: \"nested\" here"
5759echo "escapes: \n\t\r\\"
5760echo "unicode: \u2764"
5761
5762X=-99999
5763Y=3.14159265358979
5764Z=-0.001
5765
5766cmd a=1 b="two" c=true d=false e=null
5767
5768if true; then
5769 if false; then
5770 echo "inner"
5771 else
5772 echo "else"
5773 fi
5774fi
5775
5776for I in "a b c"; do
5777 echo ${I}
5778done
5779
5780no_params() {
5781 echo "no params"
5782}
5783
5784function all_args {
5785 echo "args: $@"
5786}
5787
5788a | b | c | d | e &
5789cmd 2> "errors.log"
5790cmd &> "all.log"
5791cmd >> "append.log"
5792cmd < "input.txt"
5793"#;
5794 let result = parse(script).unwrap();
5795 let stmts: Vec<_> = result.statements.iter()
5796 .filter(|s| !matches!(s, Stmt::Empty))
5797 .collect();
5798
5799 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
5801
5802 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
5804 assert!(bg_stmt.is_some(), "expected background pipeline");
5805
5806 match bg_stmt.unwrap() {
5807 Stmt::Pipeline(p) => {
5808 assert_eq!(pipeline_commands(p).len(), 5);
5809 assert!(p.background);
5810 }
5811 _ => unreachable!(),
5812 }
5813 }
5814
5815 #[test]
5820 fn parse_keyword_as_variable_rejected() {
5821 let result = parse(r#"if="value""#);
5824 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
5825
5826 let result = parse("while=true");
5827 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
5828
5829 let result = parse(r#"then="next""#);
5830 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
5831 }
5832
5833 #[test]
5834 fn parse_set_command_with_flag() {
5835 let result = parse("set -e");
5836 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
5837 let program = result.unwrap();
5838 match &program.statements[0] {
5839 Stmt::Command(cmd) => {
5840 assert_eq!(cmd.name, "set");
5841 assert_eq!(cmd.args.len(), 1);
5842 match &cmd.args[0] {
5843 Arg::ShortFlag(f) => assert_eq!(f, "e"),
5844 other => panic!("expected ShortFlag, got {:?}", other),
5845 }
5846 }
5847 other => panic!("expected Command, got {:?}", other),
5848 }
5849 }
5850
5851 #[test]
5852 fn parse_set_command_no_args() {
5853 let result = parse("set");
5854 assert!(result.is_ok(), "failed to parse set: {:?}", result);
5855 let program = result.unwrap();
5856 match &program.statements[0] {
5857 Stmt::Command(cmd) => {
5858 assert_eq!(cmd.name, "set");
5859 assert_eq!(cmd.args.len(), 0);
5860 }
5861 other => panic!("expected Command, got {:?}", other),
5862 }
5863 }
5864
5865 #[test]
5866 fn parse_set_assignment_vs_command() {
5867 let result = parse("X=5");
5869 assert!(result.is_ok());
5870 let program = result.unwrap();
5871 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
5872
5873 let result = parse("set -e");
5875 assert!(result.is_ok());
5876 let program = result.unwrap();
5877 assert!(matches!(&program.statements[0], Stmt::Command(_)));
5878 }
5879
5880 #[test]
5881 fn parse_true_as_command() {
5882 let result = parse("true");
5883 assert!(result.is_ok());
5884 let program = result.unwrap();
5885 match &program.statements[0] {
5886 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
5887 other => panic!("expected Command(true), got {:?}", other),
5888 }
5889 }
5890
5891 #[test]
5892 fn parse_false_as_command() {
5893 let result = parse("false");
5894 assert!(result.is_ok());
5895 let program = result.unwrap();
5896 match &program.statements[0] {
5897 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
5898 other => panic!("expected Command(false), got {:?}", other),
5899 }
5900 }
5901
5902 #[test]
5903 fn parse_dot_as_source_alias() {
5904 let result = parse(". script.kai");
5905 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
5906 let program = result.unwrap();
5907 match &program.statements[0] {
5908 Stmt::Command(cmd) => {
5909 assert_eq!(cmd.name, ".");
5910 assert_eq!(cmd.args.len(), 1);
5911 }
5912 other => panic!("expected Command(.), got {:?}", other),
5913 }
5914 }
5915
5916 #[test]
5917 fn parse_source_command() {
5918 let result = parse("source utils.kai");
5919 assert!(result.is_ok(), "failed to parse source: {:?}", result);
5920 let program = result.unwrap();
5921 match &program.statements[0] {
5922 Stmt::Command(cmd) => {
5923 assert_eq!(cmd.name, "source");
5924 assert_eq!(cmd.args.len(), 1);
5925 }
5926 other => panic!("expected Command(source), got {:?}", other),
5927 }
5928 }
5929
5930 #[test]
5931 fn parse_test_expr_file_test() {
5932 let result = parse(r#"[[ -f "/path/file" ]]"#);
5934 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
5935 }
5936
5937 #[test]
5938 fn parse_test_expr_comparison() {
5939 let result = parse(r#"[[ $X == "value" ]]"#);
5940 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
5941 }
5942
5943 #[test]
5944 fn parse_test_expr_single_eq() {
5945 let result = parse(r#"[[ $X = "value" ]]"#);
5947 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
5948 let program = result.unwrap();
5949 match &program.statements[0] {
5950 Stmt::Test(TestExpr::Comparison { op, .. }) => {
5951 assert_eq!(op, &TestCmpOp::Eq);
5952 }
5953 other => panic!("expected Test(Comparison), got {:?}", other),
5954 }
5955 }
5956
5957 #[test]
5958 fn parse_while_loop() {
5959 let result = parse("while true; do echo; done");
5960 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
5961 let program = result.unwrap();
5962 assert!(matches!(&program.statements[0], Stmt::While(_)));
5963 }
5964
5965 #[test]
5966 fn parse_break_with_level() {
5967 let result = parse("break 2");
5968 assert!(result.is_ok());
5969 let program = result.unwrap();
5970 match &program.statements[0] {
5971 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
5972 other => panic!("expected Break(2), got {:?}", other),
5973 }
5974 }
5975
5976 #[test]
5977 fn parse_continue_with_level() {
5978 let result = parse("continue 3");
5979 assert!(result.is_ok());
5980 let program = result.unwrap();
5981 match &program.statements[0] {
5982 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
5983 other => panic!("expected Continue(3), got {:?}", other),
5984 }
5985 }
5986
5987 #[test]
5988 fn parse_exit_with_code() {
5989 let result = parse("exit 1");
5990 assert!(result.is_ok());
5991 let program = result.unwrap();
5992 match &program.statements[0] {
5993 Stmt::Exit(Some(expr)) => {
5994 match expr.as_ref() {
5995 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
5996 other => panic!("expected Int(1), got {:?}", other),
5997 }
5998 }
5999 other => panic!("expected Exit(1), got {:?}", other),
6000 }
6001 }
6002
6003 #[test]
6010 fn spanned_literal_only_records_byte_range() {
6011 let parts = parse_interpolated_string_spanned("hello world", 100).unwrap();
6012 assert_eq!(parts.len(), 1);
6013 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
6014 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
6015 assert_eq!(parts[0].len, 11);
6016 }
6017
6018 #[test]
6019 fn spanned_braced_var_at_zero() {
6020 let parts = parse_interpolated_string_spanned("${X}", 50).unwrap();
6021 assert_eq!(parts.len(), 1);
6022 assert!(matches!(&parts[0].part, StringPart::Var(_)));
6023 assert_eq!(parts[0].offset, 50);
6024 assert_eq!(parts[0].len, 4); }
6026
6027 #[test]
6028 fn spanned_simple_var_then_literal() {
6029 let parts = parse_interpolated_string_spanned("$X end", 10).unwrap();
6030 assert_eq!(parts.len(), 2);
6031 assert!(matches!(&parts[0].part, StringPart::Var(_)));
6032 assert_eq!(parts[0].offset, 10);
6033 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
6035 assert_eq!(parts[1].offset, 12);
6036 assert_eq!(parts[1].len, 4);
6037 }
6038
6039 #[test]
6040 fn spanned_mixed_literal_var_literal() {
6041 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0).unwrap();
6042 assert_eq!(parts.len(), 3);
6043 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
6045 assert_eq!(parts[0].offset, 0);
6046 assert_eq!(parts[0].len, 3);
6047 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6049 assert_eq!(parts[1].offset, 3);
6050 assert_eq!(parts[1].len, 4);
6051 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
6053 assert_eq!(parts[2].offset, 7);
6054 assert_eq!(parts[2].len, 4);
6055 }
6056
6057 #[test]
6058 fn spanned_positional_param() {
6059 let parts = parse_interpolated_string_spanned("$1 done", 0).unwrap();
6060 assert_eq!(parts.len(), 2);
6061 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
6062 assert_eq!(parts[0].offset, 0);
6063 assert_eq!(parts[0].len, 2); }
6065
6066 #[test]
6067 fn spanned_special_dollar_dollar() {
6068 let parts = parse_interpolated_string_spanned("$$", 5).unwrap();
6069 assert_eq!(parts.len(), 1);
6070 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
6071 assert_eq!(parts[0].offset, 5);
6072 assert_eq!(parts[0].len, 2);
6073 }
6074
6075 #[test]
6076 fn spanned_arithmetic_marker_recognised() {
6077 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0).unwrap();
6081 assert_eq!(parts.len(), 1);
6082 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
6083 }
6084
6085 #[test]
6086 fn spanned_default_separator_yields_var_with_default() {
6087 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0).unwrap();
6088 assert_eq!(parts.len(), 1);
6089 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
6090 assert_eq!(parts[0].offset, 0);
6091 assert_eq!(parts[0].len, 14); }
6093
6094 #[test]
6095 fn spanned_no_dollar_runs_one_literal() {
6096 let parts = parse_interpolated_string_spanned("plain text only", 7).unwrap();
6097 assert_eq!(parts.len(), 1);
6098 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
6099 assert_eq!(parts[0].offset, 7);
6100 assert_eq!(parts[0].len, 15);
6101 }
6102
6103 #[test]
6104 fn spanned_matches_unspanned_part_count() {
6105 let cases = [
6108 "hello",
6109 "$X",
6110 "${X}",
6111 "${X:-d}",
6112 "hi $A and $B",
6113 "$0 $1 $2",
6114 "$$ $? $#",
6115 ];
6116 for s in &cases {
6117 let unspanned = parse_interpolated_string(s).expect("test input parses");
6118 let spanned = parse_interpolated_string_spanned(s, 0).unwrap();
6119 assert_eq!(
6120 unspanned.len(),
6121 spanned.len(),
6122 "part count differs for {:?}",
6123 s
6124 );
6125 }
6126 }
6127
6128 #[test]
6129 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
6130 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0).unwrap();
6135 assert_eq!(parts.len(), 2);
6136
6137 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
6138 assert_eq!(parts[0].offset, 0);
6139 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
6140
6141 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6142 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
6143 assert_eq!(parts[1].len, 4);
6144 }
6145
6146 #[test]
6147 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
6148 let parts = parse_interpolated_string_spanned("hello 世界 world", 0).unwrap();
6151 assert_eq!(parts.len(), 1);
6152 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
6153 assert_eq!(parts[0].offset, 0);
6154 assert_eq!(parts[0].len, 18);
6155 }
6156
6157 #[test]
6158 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
6159 let parts = parse_interpolated_string_spanned("\\$", 0).unwrap();
6162 assert_eq!(parts.len(), 1);
6163 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
6164 assert_eq!(parts[0].offset, 0);
6165 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
6166 }
6167
6168 #[test]
6169 fn spanned_escape_backslash_collapses_pair_to_one() {
6170 let parts = parse_interpolated_string_spanned("\\\\", 0).unwrap();
6171 assert_eq!(parts.len(), 1);
6172 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
6173 assert_eq!(parts[0].len, 2);
6174 }
6175
6176 #[test]
6177 fn spanned_standalone_cr_continuation_realigns_span_start() {
6178 let parts = parse_interpolated_string_spanned("\\\rCD${x}", 0).unwrap();
6186 assert_eq!(parts.len(), 2);
6187 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "CD"));
6188 assert_eq!(parts[0].offset, 2, "literal run must start after the consumed \\+CR");
6189 assert_eq!(parts[0].len, 2);
6190 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6191 assert_eq!(parts[1].offset, 4);
6192 assert_eq!(parts[1].len, 4); }
6194
6195 #[test]
6196 fn spanned_standalone_cr_continuation_mid_run_keeps_span_start() {
6197 let parts = parse_interpolated_string_spanned("AB\\\rCD${x}", 0).unwrap();
6202 assert_eq!(parts.len(), 2);
6203 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "ABCD"));
6204 assert_eq!(parts[0].offset, 0);
6205 assert_eq!(parts[0].len, 6); assert!(matches!(&parts[1].part, StringPart::Var(_)));
6207 assert_eq!(parts[1].offset, 6);
6208 assert_eq!(parts[1].len, 4); }
6210
6211 fn assignment_value(source: &str) -> Expr {
6215 let program = parse(source).unwrap_or_else(|e| panic!("parse {source:?}: {e:?}"));
6216 match program.statements.as_slice() {
6217 [Stmt::Assignment(a)] => a.value.clone(),
6218 other => panic!("expected a single assignment, got {other:?}"),
6219 }
6220 }
6221
6222 #[test]
6223 fn list_literal_three_elements() {
6224 let expr = assignment_value("xs=[a b c]");
6225 match expr {
6226 Expr::ListLiteral(elems) => {
6227 assert_eq!(elems.len(), 3);
6228 assert!(elems.iter().all(|e| matches!(e, ListElem::Item(_))));
6229 }
6230 other => panic!("expected ListLiteral, got {other:?}"),
6231 }
6232 }
6233
6234 #[test]
6235 fn list_literal_empty() {
6236 let expr = assignment_value("xs=[]");
6237 assert!(matches!(expr, Expr::ListLiteral(elems) if elems.is_empty()));
6238 }
6239
6240 #[test]
6241 fn list_literal_single_glued_dog() {
6242 let expr = assignment_value("xs=[dog]");
6246 match expr {
6247 Expr::ListLiteral(elems) => assert_eq!(elems.len(), 1),
6248 other => panic!("expected ListLiteral, got {other:?}"),
6249 }
6250 }
6251
6252 #[test]
6253 fn list_literal_single_int() {
6254 let expr = assignment_value("xs=[1]");
6255 match expr {
6256 Expr::ListLiteral(elems) => match elems.as_slice() {
6257 [ListElem::Item(Expr::Literal(Value::Int(1)))] => {}
6258 other => panic!("expected one Int(1) item, got {other:?}"),
6259 },
6260 other => panic!("expected ListLiteral, got {other:?}"),
6261 }
6262 }
6263
6264 #[test]
6265 fn record_literal_unspaced_colon_equals_spaced() {
6266 let spaced = assignment_value("x={port: 8080}");
6267 let unspaced = assignment_value("x={port:8080}");
6268 assert_eq!(spaced, unspaced, "{{port:8080}} must parse identically to {{port: 8080}}");
6269 match spaced {
6270 Expr::RecordLiteral(entries) => match entries.as_slice() {
6271 [RecordEntry { key: RecordKey::Bare(k), value: Expr::Literal(Value::Int(8080)) }] => {
6272 assert_eq!(k, "port");
6273 }
6274 other => panic!("expected one port:8080 entry, got {other:?}"),
6275 },
6276 other => panic!("expected RecordLiteral, got {other:?}"),
6277 }
6278 }
6279
6280 #[test]
6281 fn record_literal_name_role() {
6282 let expr = assignment_value("u={name: amy, role: maintainer}");
6283 match expr {
6284 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2),
6285 other => panic!("expected RecordLiteral, got {other:?}"),
6286 }
6287 }
6288
6289 #[test]
6290 fn record_literal_multiline_trailing_comma() {
6291 let source = "services={\n web: {port: 8080, replicas: 3, healthy: true},\n api: {port: 9000, replicas: 2, healthy: false},\n}";
6292 let expr = assignment_value(source);
6293 match expr {
6294 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2, "web + api entries"),
6295 other => panic!("expected RecordLiteral, got {other:?}"),
6296 }
6297 }
6298
6299 #[test]
6300 fn record_literal_quoted_key() {
6301 let expr = assignment_value(r#"r={"content-type": x}"#);
6302 match expr {
6303 Expr::RecordLiteral(entries) => match entries.as_slice() {
6304 [RecordEntry { key: RecordKey::Quoted(k), .. }] => assert_eq!(k, "content-type"),
6305 other => panic!("expected one quoted-key entry, got {other:?}"),
6306 },
6307 other => panic!("expected RecordLiteral, got {other:?}"),
6308 }
6309 }
6310
6311 #[test]
6312 fn nested_list_and_record_in_record() {
6313 let expr = assignment_value("x={tags: [a b], meta: {active: true}}");
6314 match expr {
6315 Expr::RecordLiteral(entries) => {
6316 assert_eq!(entries.len(), 2);
6317 assert!(matches!(entries[0].value, Expr::ListLiteral(_)));
6318 assert!(matches!(entries[1].value, Expr::RecordLiteral(_)));
6319 }
6320 other => panic!("expected RecordLiteral, got {other:?}"),
6321 }
6322 }
6323
6324 #[test]
6325 fn spread_and_item_elements() {
6326 let expr = assignment_value("new=[...$xs date]");
6327 match expr {
6328 Expr::ListLiteral(elems) => match elems.as_slice() {
6329 [ListElem::Spread(Expr::VarRef(_)), ListElem::Item(Expr::Literal(Value::String(s)))] => {
6330 assert_eq!(s, "date");
6331 }
6332 other => panic!("expected [Spread($xs), Item(date)], got {other:?}"),
6333 },
6334 other => panic!("expected ListLiteral, got {other:?}"),
6335 }
6336 }
6337
6338 #[test]
6339 fn spread_of_two_variables() {
6340 let expr = assignment_value("c=[...$a ...$b]");
6341 match expr {
6342 Expr::ListLiteral(elems) => {
6343 assert_eq!(elems.len(), 2);
6344 assert!(elems.iter().all(|e| matches!(e, ListElem::Spread(_))));
6345 }
6346 other => panic!("expected ListLiteral, got {other:?}"),
6347 }
6348 }
6349
6350 #[test]
6351 fn in_rhs_accepts_a_list_literal() {
6352 let program = parse("if [[ $a not in [dog] ]]; then echo hit; fi")
6353 .unwrap_or_else(|e| panic!("parse: {e:?}"));
6354 assert_eq!(program.statements.len(), 1);
6355 }
6356
6357 #[test]
6358 fn multiword_bareword_record_value_is_a_parse_error() {
6359 assert!(parse("x={msg: hello world}").is_err());
6362 }
6363
6364 #[test]
6367 fn argv_bracket_glob_stays_a_glob_pattern() {
6368 let program = parse("ls [dog]").unwrap_or_else(|e| panic!("parse: {e:?}"));
6372 assert_eq!(program.statements.len(), 1);
6373 }
6374
6375 #[test]
6376 fn brace_expansion_at_argv_position_is_unaffected() {
6377 let program = parse("cmd *.{rs,go}").unwrap_or_else(|e| panic!("parse: {e:?}"));
6382 assert_eq!(program.statements.len(), 1);
6383 }
6384
6385 #[test]
6386 fn for_head_item_is_not_a_literal() {
6387 let program = parse("for x in [a]; do echo $x; done")
6390 .unwrap_or_else(|e| panic!("parse: {e:?}"));
6391 match program.statements.as_slice() {
6392 [Stmt::For(for_loop)] => {
6393 assert_eq!(for_loop.items.len(), 1);
6394 assert!(
6395 !matches!(for_loop.items[0], Expr::ListLiteral(_)),
6396 "for-head item must not be a ListLiteral: {:?}",
6397 for_loop.items[0]
6398 );
6399 }
6400 other => panic!("expected a single For statement, got {other:?}"),
6401 }
6402 }
6403
6404 #[derive(Debug, Clone, Copy)]
6410 enum NestingLayer {
6411 CmdSubst,
6413 QuotedCmdSubst,
6415 Case,
6417 If,
6418 For,
6419 }
6420
6421 fn wrap_in_layer(inner: &str, layer: NestingLayer) -> String {
6422 match layer {
6423 NestingLayer::CmdSubst => format!("x=$({inner})"),
6424 NestingLayer::QuotedCmdSubst => format!("x=\"pre $({inner}) post\""),
6425 NestingLayer::Case => format!("case v in v) {inner};; esac"),
6426 NestingLayer::If => format!("if true; then {inner}; fi"),
6427 NestingLayer::For => format!("for f in a; do {inner}; done"),
6428 }
6429 }
6430
6431 proptest::proptest! {
6432 #[test]
6450 fn nested_compound_constructs_always_parse(
6451 layers in proptest::collection::vec(
6452 proptest::prop_oneof![
6453 proptest::strategy::Just(NestingLayer::CmdSubst),
6454 proptest::strategy::Just(NestingLayer::QuotedCmdSubst),
6455 proptest::strategy::Just(NestingLayer::Case),
6456 proptest::strategy::Just(NestingLayer::If),
6457 proptest::strategy::Just(NestingLayer::For),
6458 ],
6459 1..=4,
6460 ).prop_filter("at most one QuotedCmdSubst layer", |layers| {
6461 layers.iter().filter(|l| matches!(l, NestingLayer::QuotedCmdSubst)).count() <= 1
6462 })
6463 ) {
6464 let source = layers
6465 .iter()
6466 .fold("echo x".to_string(), |inner, &layer| wrap_in_layer(&inner, layer));
6467 proptest::prop_assert!(
6468 parse(&source).is_ok(),
6469 "grammar-nested construct failed to parse: {source:?}"
6470 );
6471 }
6472 }
6473}