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::error::RichReason;
15use chumsky::input::{MappedInput, Stream, ValueInput};
16use chumsky::prelude::*;
17
18pub type Span = SimpleSpan;
20
21type ParserInput = MappedInput<'static, Token, Span, Stream<std::vec::IntoIter<(Token, Span)>>, PairFn>;
34
35type PairFn = fn((Token, Span)) -> (Token, Span);
39
40fn keep_pair(pair: (Token, Span)) -> (Token, Span) {
41 pair
42}
43
44thread_local! {
45 static CACHED_PARSER: Boxed<
57 'static,
58 'static,
59 ParserInput,
60 Program,
61 extra::Err<Rich<'static, Token, Span>>,
62 > = program_parser().boxed();
63}
64
65fn parse_var_expr(raw: &str) -> Expr {
72 if raw == "${?}" {
74 return Expr::LastExitCode;
75 }
76
77 if raw == "${$}" {
79 return Expr::CurrentPid;
80 }
81
82 if let Some(colon_idx) = find_default_separator(raw) {
85 let path = parse_varpath(&format!("${{{}}}", &raw[2..colon_idx]));
87 let default_str = &raw[colon_idx + 2..raw.len() - 1];
90 let default_word = unquote_default_word(default_str);
99 let default = parse_interpolated_string(&default_word)
100 .unwrap_or_else(|_| vec![StringPart::Literal(default_word.clone())]);
101 return Expr::VarWithDefault { path, default };
102 }
103
104 Expr::VarRef(parse_varpath(raw))
106}
107
108pub(crate) fn bash_substring_hint(var_content: &str) -> Option<String> {
120 let mut depth = 0usize;
121 let colon = var_content.char_indices().find_map(|(i, c)| match c {
122 '[' => {
123 depth += 1;
124 None
125 }
126 ']' => {
127 depth = depth.saturating_sub(1);
128 None
129 }
130 ':' if depth == 0 => Some(i),
131 _ => None,
132 })?;
133 let (name, rest) = var_content.split_at(colon);
134 let after_offset = &rest[1..];
136 let suggestion = if let Some(length) = after_offset.strip_prefix(':') {
139 format!("${{{name}[0:{length}]}}")
140 } else if after_offset.contains(':') {
141 format!("${{{name}[{after_offset}]}}")
142 } else {
143 format!("${{{name}[{after_offset}:]}}")
144 };
145 Some(format!(
146 "${{{var_content}}}: kaish slices with brackets, not `:offset:length` — \
147 write {suggestion}. Brackets are start:end and end-exclusive, so \
148 ${{{name}[0:5]}} is the first five characters and ${{{name}[-3:]}} the last three."
149 ))
150}
151
152fn unquote_default_word(word: &str) -> String {
182 let mut out = String::with_capacity(word.len());
183 let mut in_single = false;
184 let mut in_double = false;
185 let chars: Vec<char> = word.chars().collect();
186 let mut i = 0;
187 while i < chars.len() {
188 let ch = chars[i];
189 if ch == '\\' && !in_single {
193 let run_start = i;
194 while i < chars.len() && chars[i] == '\\' {
195 i += 1;
196 }
197 let run_len = i - run_start;
198 let next_is_quote =
201 chars.get(i).is_some_and(|c| *c == '"' || (*c == '\'' && !in_double));
202 if next_is_quote {
203 if run_len / 2 > 0 {
204 out.push_str(&"\\".repeat(run_len / 2));
205 }
206 if run_len % 2 == 1 {
207 out.push(chars[i]);
211 i += 1;
212 }
213 } else {
216 out.push_str(&"\\".repeat(run_len));
217 }
218 continue;
219 }
220 i += 1;
221 match ch {
222 '\'' if !in_double => in_single = !in_single,
225 '"' if !in_single => in_double = !in_double,
226 '$' if in_single => out.push_str("__KAISH_ESCAPED_DOLLAR__"),
228 _ => out.push(ch),
229 }
230 }
231 out
232}
233
234fn find_default_separator(raw: &str) -> Option<usize> {
236 let bytes = raw.as_bytes();
237 let mut depth = 0;
238 let mut bracket_depth = 0;
239 let mut i = 0;
240
241 while i < bytes.len() {
242 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
243 depth += 1;
244 i += 2;
245 continue;
246 }
247 if bytes[i] == b'}' && depth > 0 {
248 depth -= 1;
249 i += 1;
250 continue;
251 }
252 if bytes[i] == b'[' {
255 bracket_depth += 1;
256 } else if bytes[i] == b']' && bracket_depth > 0 {
257 bracket_depth -= 1;
258 }
259 if depth == 1
262 && bracket_depth == 0
263 && i + 1 < bytes.len()
264 && bytes[i] == b':'
265 && bytes[i + 1] == b'-'
266 {
267 return Some(i);
268 }
269 i += 1;
270 }
271 None
272}
273
274fn find_default_separator_in_content(content: &str) -> Option<usize> {
276 let bytes = content.as_bytes();
277 let mut depth = 0;
278 let mut bracket_depth = 0;
279 let mut i = 0;
280
281 while i < bytes.len() {
282 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
283 depth += 1;
284 i += 2;
285 continue;
286 }
287 if bytes[i] == b'}' && depth > 0 {
288 depth -= 1;
289 i += 1;
290 continue;
291 }
292 if bytes[i] == b'[' {
295 bracket_depth += 1;
296 } else if bytes[i] == b']' && bracket_depth > 0 {
297 bracket_depth -= 1;
298 }
299 if depth == 0
301 && bracket_depth == 0
302 && i + 1 < bytes.len()
303 && bytes[i] == b':'
304 && bytes[i + 1] == b'-'
305 {
306 return Some(i);
307 }
308 i += 1;
309 }
310 None
311}
312
313fn is_name_char(c: char) -> bool {
325 c.is_ascii_alphanumeric() || c == '_' || !c.is_ascii()
326}
327
328fn is_name_start(c: char) -> bool {
331 c.is_ascii_alphabetic() || c == '_' || !c.is_ascii()
332}
333
334
335fn name_in_token_kind<'a>(
350 tok: &'a Token,
351 prev: Option<&Token>,
352 next: Option<&Token>,
353) -> Option<(&'a str, bool)> {
354 match tok {
355 Token::SimpleVarRef(name) => Some((name.as_str(), false)),
356 Token::VarLength(inner) => Some((root_of(inner), false)),
357 Token::VarRef(raw) => raw
358 .strip_prefix("${")
359 .and_then(|s| s.strip_suffix('}'))
360 .map(|r| (root_of(r), false)),
361 Token::Ident(name)
367 if matches!(next, Some(Token::Eq))
368 && match prev {
369 None => true,
370 Some(p) => crate::lexer::is_statement_boundary(p) || matches!(p, Token::Local),
371 } =>
372 {
373 Some((name.as_str(), true))
374 }
375 Token::Ident(name) if matches!(prev, Some(Token::For)) => Some((name.as_str(), false)),
379 _ => None,
380 }
381}
382
383fn bad_name_in_parts(parts: &[StringPart]) -> Option<crate::name::NameError> {
388 fn root(path: &VarPath) -> Option<&str> {
389 match path.segments.first() {
390 Some(VarSegment::Field(name)) => Some(name.as_str()),
391 _ => None,
392 }
393 }
394 for part in parts {
395 let bad = match part {
396 StringPart::Var(path) | StringPart::VarLength(path) => {
397 root(path).and_then(|n| crate::name::validate(n).err())
398 }
399 StringPart::VarWithDefault { path, default } => root(path)
400 .and_then(|n| crate::name::validate(n).err())
401 .or_else(|| bad_name_in_parts(default)),
402 _ => None,
405 };
406 if bad.is_some() {
407 return bad;
408 }
409 }
410 None
411}
412
413fn root_of(inner: &str) -> &str {
416 let end = inner.find(['[', '.', ':', '-']).unwrap_or(inner.len());
417 &inner[..end]
418}
419
420pub(crate) fn parse_varpath(raw: &str) -> VarPath {
421 let segment_strs = lexer::parse_var_ref(raw).unwrap_or_default();
422 let segments = segment_strs
423 .into_iter()
424 .enumerate()
425 .map(|(i, s)| {
426 if i == 0 {
427 VarSegment::Field(crate::ast::normalize_name(s))
430 } else if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
431 parse_subscript(inner)
432 } else {
433 VarSegment::Field(s)
436 }
437 })
438 .collect();
439 VarPath { segments }
440}
441
442fn parse_subscript(inner: &str) -> VarSegment {
449 if let Some(var) = inner.strip_prefix('$') {
451 return VarSegment::Dynamic(var.to_string());
452 }
453 if inner.len() >= 2
455 && ((inner.starts_with('"') && inner.ends_with('"'))
456 || (inner.starts_with('\'') && inner.ends_with('\'')))
457 {
458 return VarSegment::Key(inner[1..inner.len() - 1].to_string());
459 }
460 if let Some((lhs, rhs)) = inner.split_once(':') {
464 let bound = |s: &str| -> Option<Option<i64>> {
468 if s.is_empty() {
469 Some(None)
470 } else if lexer::is_leading_zero_numeral(s) {
471 None
472 } else {
473 s.parse::<i64>().ok().map(Some)
474 }
475 };
476 if let (Some(start), Some(end)) = (bound(lhs), bound(rhs)) {
477 return VarSegment::Slice(start, end);
478 }
479 }
480 if !lexer::is_leading_zero_numeral(inner)
483 && let Ok(i) = inner.parse::<i64>()
484 {
485 return VarSegment::Index(i);
486 }
487 VarSegment::Key(inner.to_string())
489}
490
491fn strip_empty_stmts(statements: Vec<Stmt>) -> Vec<Stmt> {
494 statements
495 .into_iter()
496 .filter(|s| !matches!(s, Stmt::Empty))
497 .collect()
498}
499
500fn parse_interpolated_string_spanned(
519 s: &str,
520 base_offset: usize,
521) -> Result<Vec<SpannedPart>, String> {
522 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
523
524 let chars_vec: Vec<char> = s.chars().collect();
525 let mut i = 0;
526 let mut pos: usize = 0;
527
528 let mut parts: Vec<SpannedPart> = Vec::new();
529 let mut current_text = String::new();
530 let mut current_text_start: usize = pos;
531
532 let push_literal =
533 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
534 if !current_text.is_empty() {
535 parts.push(SpannedPart {
536 part: StringPart::Literal(std::mem::take(current_text)),
537 offset: base_offset + *start,
538 len: end - *start,
539 });
540 *start = end;
541 }
542 };
543
544 while i < chars_vec.len() {
545 let ch = chars_vec[i];
546
547 if ch == '\x00' {
548 let start = pos;
550 i += 1;
551 pos += 1;
552 let mut marker = String::new();
553 while let Some(&c) = chars_vec.get(i) {
554 if c == '\x00' {
555 i += 1;
556 pos += 1;
557 break;
558 }
559 marker.push(c);
560 i += 1;
561 pos += c.len_utf8();
562 }
563 if marker == "DOLLAR" {
564 if current_text.is_empty() {
565 current_text_start = start;
566 }
567 current_text.push('$');
568 }
569 } else if ch == '\\' {
570 let next = chars_vec.get(i + 1).copied();
576 match next {
577 Some('$') => {
578 if current_text.is_empty() {
579 current_text_start = pos;
580 }
581 current_text.push('$');
582 i += 2;
583 pos += 2;
584 }
585 Some('\\') => {
586 if current_text.is_empty() {
587 current_text_start = pos;
588 }
589 current_text.push('\\');
590 i += 2;
591 pos += 2;
592 }
593 Some('\n') => {
594 i += 2;
597 pos += 2;
598 if current_text.is_empty() {
599 current_text_start = pos;
600 }
601 }
602 Some('\r') => {
603 i += 2;
605 pos += 2;
606 if chars_vec.get(i) == Some(&'\n') {
607 i += 1;
608 pos += 1;
609 }
610 if current_text.is_empty() {
611 current_text_start = pos;
612 }
613 }
614 _ => {
615 if current_text.is_empty() {
619 current_text_start = pos;
620 }
621 current_text.push('\\');
622 i += 1;
623 pos += 1;
624 }
625 }
626 } else if ch == '$' {
627 let part_start = pos;
629 let next = chars_vec.get(i + 1).copied();
630
631 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
632 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
634 i += 2; pos += 2;
636 let mut cmd_content = String::new();
637 let mut depth = 1;
638 let mut closed = false;
639 while let Some(&c) = chars_vec.get(i) {
640 i += 1;
641 pos += c.len_utf8();
642 if c == '(' {
643 depth += 1;
644 cmd_content.push(c);
645 } else if c == ')' {
646 depth -= 1;
647 if depth == 0 {
648 closed = true;
649 break;
650 }
651 cmd_content.push(c);
652 } else {
653 cmd_content.push(c);
654 }
655 }
656 if !closed {
657 return Err("unterminated command substitution: missing `)`".to_string());
658 }
659 let inserted = if let Ok(program) = parse(&cmd_content) {
666 let stmts = strip_empty_stmts(program.statements);
669 if stmts.is_empty() {
670 false
671 } else {
672 parts.push(SpannedPart {
673 part: StringPart::CommandSubst(stmts),
674 offset: base_offset + part_start,
675 len: pos - part_start,
676 });
677 true
678 }
679 } else {
680 return Err(format!(
681 "syntax error in command substitution: $({cmd_content})"
682 ));
683 };
684 if inserted {
685 current_text_start = pos;
688 } else {
689 if current_text.is_empty() {
694 current_text_start = part_start;
695 }
696 current_text.push_str("$(");
697 current_text.push_str(&cmd_content);
698 current_text.push(')');
699 }
700 } else if next == Some('{') {
701 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
702 i += 2; pos += 2;
704 let mut var_content = String::new();
705 let mut depth = 1;
706 while let Some(&c) = chars_vec.get(i) {
707 i += 1;
708 pos += c.len_utf8();
709 if c == '{' && var_content.ends_with('$') {
710 depth += 1;
711 var_content.push(c);
712 } else if c == '}' {
713 depth -= 1;
714 if depth == 0 {
715 break;
716 }
717 var_content.push(c);
718 } else {
719 var_content.push(c);
720 }
721 }
722 let part = if let Some(name) = var_content.strip_prefix('#') {
723 if find_default_separator_in_content(name).is_some() {
730 return Err(format!(
731 "${{#{name}}}: a length cannot carry a default — \
732 ${{#NAME}} counts, ${{NAME:-default}} substitutes. \
733 Write ${{#NAME}} on a name you have set, or test it \
734 first."
735 ));
736 }
737 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
738 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
739 let expr = var_content
740 .strip_prefix("__ARITH:")
741 .and_then(|s| s.strip_suffix("__"))
742 .unwrap_or("");
743 StringPart::Arithmetic(expr.to_string())
744 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
745 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
746 let default_str = &var_content[colon_idx + 2..];
747 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
758 StringPart::VarWithDefault { path, default }
759 } else if let Some(msg) = bash_substring_hint(&var_content) {
760 return Err(msg);
761 } else {
762 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
763 };
764 parts.push(SpannedPart {
765 part,
766 offset: base_offset + part_start,
767 len: pos - part_start,
768 });
769 current_text_start = pos;
770 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
771 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
772 i += 1; pos += 1;
774 if let Some(&digit) = chars_vec.get(i) {
775 let n = digit.to_digit(10).unwrap_or(0) as usize;
776 i += 1;
777 pos += digit.len_utf8();
778 parts.push(SpannedPart {
779 part: StringPart::Positional(n),
780 offset: base_offset + part_start,
781 len: pos - part_start,
782 });
783 }
784 current_text_start = pos;
785 } else if next == Some('@') {
786 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
787 i += 2; pos += 2;
789 parts.push(SpannedPart {
790 part: StringPart::AllArgs,
791 offset: base_offset + part_start,
792 len: pos - part_start,
793 });
794 current_text_start = pos;
795 } else if next == Some('#') {
796 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
797 i += 2; pos += 2;
799 parts.push(SpannedPart {
800 part: StringPart::ArgCount,
801 offset: base_offset + part_start,
802 len: pos - part_start,
803 });
804 current_text_start = pos;
805 } else if next == Some('?') {
806 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
807 i += 2; pos += 2;
809 parts.push(SpannedPart {
810 part: StringPart::LastExitCode,
811 offset: base_offset + part_start,
812 len: pos - part_start,
813 });
814 current_text_start = pos;
815 } else if next == Some('$') {
816 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
817 i += 2; pos += 2;
819 parts.push(SpannedPart {
820 part: StringPart::CurrentPid,
821 offset: base_offset + part_start,
822 len: pos - part_start,
823 });
824 current_text_start = pos;
825 } else if next.map(is_name_start).unwrap_or(false) {
826 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
827 i += 1; pos += 1;
829 let mut var_name = String::new();
830 while let Some(&c) = chars_vec.get(i) {
831 if is_name_char(c) {
832 var_name.push(c);
833 i += 1;
834 pos += c.len_utf8();
835 } else {
836 break;
837 }
838 }
839 parts.push(SpannedPart {
840 part: StringPart::Var(VarPath::simple(var_name)),
841 offset: base_offset + part_start,
842 len: pos - part_start,
843 });
844 current_text_start = pos;
845 } else {
846 if current_text.is_empty() {
848 current_text_start = pos;
849 }
850 current_text.push(ch);
851 i += 1;
852 pos += 1;
853 }
854 } else {
855 if current_text.is_empty() {
856 current_text_start = pos;
857 }
858 current_text.push(ch);
859 i += 1;
860 pos += ch.len_utf8();
861 }
862 }
863
864 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
865
866 Ok(parts)
867}
868
869fn parse_interpolated_string(s: &str) -> Result<Vec<StringPart>, String> {
870 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
873
874 let mut parts = Vec::new();
875 let mut current_text = String::new();
876 let mut chars = s.chars().peekable();
877
878 while let Some(ch) = chars.next() {
879 if ch == '\x00' {
880 let mut marker = String::new();
882 while let Some(&c) = chars.peek() {
883 if c == '\x00' {
884 chars.next(); break;
886 }
887 if let Some(c) = chars.next() {
888 marker.push(c);
889 }
890 }
891 if marker == "DOLLAR" {
892 current_text.push('$');
893 }
894 } else if ch == '$' {
895 if chars.peek() == Some(&'(') {
897 if !current_text.is_empty() {
899 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
900 }
901
902 chars.next();
904
905 let remainder: String = chars.clone().collect();
915 let close = lexer::tokenize(&remainder).ok().and_then(|toks| {
916 let toks: Vec<(Token, Span)> = toks
917 .into_iter()
918 .map(|sp| (sp.token, (sp.span.start..sp.span.end).into()))
919 .collect();
920 find_cmd_subst_close(&toks).map(|idx| toks[idx].1)
921 });
922 let Some(rparen_span) = close else {
929 return Err("unterminated command substitution: missing `)`".to_string());
930 };
931 let (cmd_content, consume_bytes) =
932 (remainder[..rparen_span.start].to_string(), rparen_span.end);
933 let mut consumed = 0usize;
934 while consumed < consume_bytes {
935 match chars.next() {
936 Some(c) => consumed += c.len_utf8(),
937 None => break,
938 }
939 }
940
941 match parse(&cmd_content) {
944 Ok(program) => {
945 let stmts = strip_empty_stmts(program.statements);
946 if stmts.is_empty() {
947 current_text.push_str("$(");
950 current_text.push_str(&cmd_content);
951 current_text.push(')');
952 } else {
953 parts.push(StringPart::CommandSubst(stmts));
954 }
955 }
956 Err(_) => {
957 return Err(format!(
961 "syntax error in command substitution: $({cmd_content})"
962 ));
963 }
964 }
965 } else if chars.peek() == Some(&'{') {
966 if !current_text.is_empty() {
968 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
969 }
970
971 chars.next();
973
974 let mut var_content = String::new();
976 let mut depth = 1;
977 for c in chars.by_ref() {
978 if c == '{' && var_content.ends_with('$') {
979 depth += 1;
980 var_content.push(c);
981 } else if c == '}' {
982 depth -= 1;
983 if depth == 0 {
984 break;
985 }
986 var_content.push(c);
987 } else {
988 var_content.push(c);
989 }
990 }
991
992 let part = if let Some(name) = var_content.strip_prefix('#') {
994 if find_default_separator_in_content(name).is_some() {
1002 return Err(format!(
1003 "${{#{name}}}: a length cannot carry a default — \
1004 ${{#NAME}} counts, ${{NAME:-default}} substitutes. \
1005 Write ${{#NAME}} on a name you have set, or test it \
1006 first."
1007 ));
1008 }
1009 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
1010 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
1011 let expr = var_content
1013 .strip_prefix("__ARITH:")
1014 .and_then(|s| s.strip_suffix("__"))
1015 .unwrap_or("");
1016 StringPart::Arithmetic(expr.to_string())
1017 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
1018 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
1020 let default_str = &var_content[colon_idx + 2..];
1021 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
1022 StringPart::VarWithDefault { path, default }
1023 } else if let Some(msg) = bash_substring_hint(&var_content) {
1024 return Err(msg);
1025 } else {
1026 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
1028 };
1029 parts.push(part);
1030 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
1031 if !current_text.is_empty() {
1033 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1034 }
1035 if let Some(digit) = chars.next() {
1036 let n = digit.to_digit(10).unwrap_or(0) as usize;
1037 parts.push(StringPart::Positional(n));
1038 }
1039 } else if chars.peek() == Some(&'@') {
1040 if !current_text.is_empty() {
1042 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1043 }
1044 chars.next(); parts.push(StringPart::AllArgs);
1046 } else if chars.peek() == Some(&'#') {
1047 if !current_text.is_empty() {
1049 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1050 }
1051 chars.next(); parts.push(StringPart::ArgCount);
1053 } else if chars.peek() == Some(&'?') {
1054 if !current_text.is_empty() {
1056 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1057 }
1058 chars.next(); parts.push(StringPart::LastExitCode);
1060 } else if chars.peek() == Some(&'$') {
1061 if !current_text.is_empty() {
1063 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1064 }
1065 chars.next(); parts.push(StringPart::CurrentPid);
1067 } else if chars.peek().copied().map(is_name_start).unwrap_or(false) {
1068 if !current_text.is_empty() {
1070 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
1071 }
1072
1073 let mut var_name = String::new();
1075 while let Some(&c) = chars.peek() {
1076 if is_name_char(c) {
1077 if let Some(c) = chars.next() {
1078 var_name.push(c);
1079 }
1080 } else {
1081 break;
1082 }
1083 }
1084
1085 parts.push(StringPart::Var(VarPath::simple(var_name)));
1086 } else {
1087 current_text.push(ch);
1089 }
1090 } else {
1091 current_text.push(ch);
1092 }
1093 }
1094
1095 if !current_text.is_empty() {
1096 parts.push(StringPart::Literal(current_text));
1097 }
1098
1099 Ok(parts)
1100}
1101
1102#[derive(Debug, Clone)]
1104pub struct ParseError {
1105 pub span: Span,
1106 pub message: String,
1107}
1108
1109impl ParseError {
1110 pub fn format(&self, source: &str) -> String {
1115 let start = self.span.start;
1116 let mut line = 1usize;
1117 let mut col = 1usize;
1118 for (i, ch) in source.char_indices() {
1119 if i >= start {
1120 break;
1121 }
1122 if ch == '\n' {
1123 line += 1;
1124 col = 1;
1125 } else {
1126 col += 1;
1127 }
1128 }
1129 let line_content = {
1130 let line_start = source[..start.min(source.len())]
1131 .rfind('\n')
1132 .map_or(0, |i| i + 1);
1133 let line_end = source[start.min(source.len())..]
1134 .find('\n')
1135 .map_or(source.len(), |i| start + i);
1136 source.get(line_start..line_end).unwrap_or("")
1137 };
1138 if line_content.is_empty() {
1139 format!("{}:{} [parse]: {}", line, col, self.message)
1140 } else {
1141 format!(
1142 "{}:{} [parse]: {}\n | {}",
1143 line, col, self.message, line_content
1144 )
1145 }
1146 }
1147}
1148
1149impl std::fmt::Display for ParseError {
1150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1151 write!(f, "{} at {:?}", self.message, self.span)
1152 }
1153}
1154
1155impl std::error::Error for ParseError {}
1156
1157pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
1159 let tokens = lexer::tokenize(source).map_err(|errs| {
1161 errs.into_iter()
1162 .map(|e| ParseError {
1163 span: (e.span.start..e.span.end).into(),
1164 message: format!("lexer error: {}", e.token),
1165 })
1166 .collect::<Vec<_>>()
1167 })?;
1168
1169 let tokens: Vec<(Token, Span)> = tokens
1171 .into_iter()
1172 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
1173 .collect();
1174
1175 for (i, (tok, span)) in tokens.iter().enumerate() {
1186 let prev = i.checked_sub(1).and_then(|j| tokens.get(j)).map(|(t, _)| t);
1187 if let Some((name, is_target)) =
1188 name_in_token_kind(tok, prev, tokens.get(i + 1).map(|(t, _)| t))
1189 {
1190 if let Err(bad) = crate::name::validate(name) {
1191 let defer = is_target && matches!(bad.ch, '.' | '#');
1200 if !defer {
1201 return Err(vec![ParseError { span: *span, message: bad.to_string() }]);
1202 }
1203 }
1204 }
1205 if let Token::String(s) = tok {
1211 if !s.is_ascii() && s.contains('$') {
1212 if let Ok(parts) = parse_interpolated_string(s) {
1213 if let Some(bad) = bad_name_in_parts(&parts) {
1214 return Err(vec![ParseError { span: *span, message: bad.to_string() }]);
1215 }
1216 }
1217 }
1218 }
1219 let message = match tok {
1220 Token::VarRef(raw) => raw
1221 .strip_prefix("${")
1222 .and_then(|s| s.strip_suffix('}'))
1223 .filter(|_| find_default_separator(raw).is_none())
1224 .and_then(bash_substring_hint),
1225 Token::String(s) if s.contains("${") && s.contains(':') => {
1229 parse_interpolated_string(s).err()
1230 }
1231 _ => None,
1232 };
1233 if let Some(message) = message {
1234 return Err(vec![ParseError {
1235 span: *span,
1236 message,
1237 }]);
1238 }
1239 }
1240
1241 let end_span: Span = (source.len()..source.len()).into();
1243
1244 parse_tokens(tokens, end_span, (0..0).into())
1245}
1246
1247fn parse_tokens(
1260 tokens: Vec<(Token, Span)>,
1261 end_span: Span,
1262 stdin_anchor: Span,
1263) -> Result<Program, Vec<ParseError>> {
1264 let input = Stream::from_iter(tokens.clone()).map(end_span, keep_pair as PairFn);
1277 let result = CACHED_PARSER.with(|parser| parser.parse(input));
1278
1279 let program = result.into_result().map_err(|errs| {
1280 if let Err(specific) = validate_cmd_subst_bodies(&tokens) {
1288 return specific;
1289 }
1290 if let Err(specific) = validate_interpolated_strings(&tokens) {
1294 return specific;
1295 }
1296 if let Err(specific) = validate_heredoc_bodies(&tokens) {
1298 return specific;
1299 }
1300 let error_starts: Vec<usize> = errs.iter().map(|e| e.span().start).collect();
1303 if let Err(specific) = validate_leading_zero_counts(&tokens, &error_starts) {
1304 return specific;
1305 }
1306 if errs.iter().all(is_glued_args_error)
1317 && let Some(from_offset) = errs.iter().map(|e| e.span().start).min()
1318 && let Err(specific) = validate_glued_args(&tokens, from_offset)
1319 {
1320 return specific;
1321 }
1322 errs.into_iter()
1323 .map(|e| ParseError {
1324 span: *e.span(),
1325 message: e.to_string(),
1326 })
1327 .collect::<Vec<_>>()
1328 })?;
1329
1330 if first_ambiguous_stdin(&program.statements) {
1335 return Err(vec![ParseError {
1336 span: stdin_anchor,
1340 message: "multiple stdin redirects on one command are ambiguous; \
1341 use exactly one of `<`, `<<`, or `<<<`"
1342 .to_string(),
1343 }]);
1344 }
1345
1346 Ok(program)
1347}
1348
1349pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
1351 let program = parse(source)?;
1352 program
1353 .statements
1354 .into_iter()
1355 .find(|s| !matches!(s, Stmt::Empty))
1356 .ok_or_else(|| {
1357 vec![ParseError {
1358 span: (0..source.len()).into(),
1359 message: "empty input".to_string(),
1360 }]
1361 })
1362}
1363
1364fn program_parser<'tokens, 'src: 'tokens, I>(
1370) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
1371where
1372 I: ValueInput<'tokens, Token = Token, Span = Span>,
1373{
1374 statement_parser()
1375 .repeated()
1376 .collect::<Vec<_>>()
1377 .map(|statements| Program { statements })
1378}
1379
1380fn statement_parser<'tokens, I>(
1383) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1384where
1385 I: ValueInput<'tokens, Token = Token, Span = Span>,
1386{
1387 recursive(|stmt| {
1388 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
1389
1390 let loop_count = select! {
1395 Token::Int(n) => n as usize,
1396 Token::NumericLiteral(data) if matches!(data.value, Value::Int(_)) => {
1397 match data.value {
1398 Value::Int(n) => n as usize,
1399 _ => unreachable!("guarded by the select! pattern above"),
1400 }
1401 },
1402 };
1403
1404 let break_stmt = just(Token::Break)
1406 .ignore_then(loop_count.or_not())
1407 .map(Stmt::Break);
1408
1409 let continue_stmt = just(Token::Continue)
1411 .ignore_then(loop_count.or_not())
1412 .map(Stmt::Continue);
1413
1414 let return_stmt = just(Token::Return)
1416 .ignore_then(primary_expr_parser().or_not())
1417 .map(|e| Stmt::Return(e.map(Box::new)));
1418
1419 let exit_stmt = just(Token::Exit)
1421 .ignore_then(primary_expr_parser().or_not())
1422 .map(|e| Stmt::Exit(e.map(Box::new)));
1423
1424 let set_flag_arg = choice((
1433 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
1434 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
1435 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
1437 ));
1438
1439 let option_value_str = select! {
1443 Token::NumberIdent(s) => s,
1444 Token::Int(n) => n.to_string(),
1445 Token::Ident(s) => s,
1446 };
1447
1448 let set_option_assign = ident_parser()
1452 .then_ignore(just(Token::Eq))
1453 .then(option_value_str)
1454 .map(|(name, value)| {
1455 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
1456 });
1457
1458 let set_quoted_arg = select! {
1462 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1463 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1464 };
1465
1466 let set_with_flags = just(Token::Set)
1468 .then(set_flag_arg)
1469 .then(
1470 choice((
1471 set_flag_arg,
1472 set_option_assign,
1474 set_quoted_arg,
1475 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
1477 ))
1478 .repeated()
1479 .collect::<Vec<_>>(),
1480 )
1481 .map(|((_, first_arg), mut rest_args)| {
1482 let mut args = vec![first_arg];
1483 args.append(&mut rest_args);
1484 Stmt::Command(Command {
1485 name: "set".to_string(),
1486 args,
1487 redirects: vec![],
1488 })
1489 });
1490
1491 let set_no_args = just(Token::Set)
1494 .then(
1495 choice((
1496 just(Token::Newline).to(()),
1497 just(Token::Semi).to(()),
1498 just(Token::And).to(()),
1499 just(Token::Or).to(()),
1500 end(),
1501 ))
1502 .rewind(),
1503 )
1504 .map(|_| Stmt::Command(Command {
1505 name: "set".to_string(),
1506 args: vec![],
1507 redirects: vec![],
1508 }));
1509
1510 let set_command = set_with_flags.or(set_no_args);
1514
1515 let env_prefix_assign = ident_parser()
1532 .then_ignore(just(Token::Eq))
1533 .then(value_expr_parser())
1534 .map(|(name, value)| Assignment { path: VarPath::simple(name), value, local: false });
1535 let env_scoped = env_prefix_assign
1536 .repeated()
1537 .at_least(1)
1538 .collect::<Vec<_>>()
1539 .then(pipeline_parser(command_stage_parser()).map(pipeline_into_stmt))
1540 .map(|(assignments, body)| Stmt::EnvScoped {
1541 assignments,
1542 body: Box::new(body),
1543 });
1544
1545 let compound = choice((
1553 if_parser(stmt.clone()).map(Stmt::If),
1554 for_parser(stmt.clone()).map(Stmt::For),
1555 while_parser(stmt.clone()).map(Stmt::While),
1556 case_parser(stmt.clone()).map(Stmt::Case),
1557 ))
1558 .boxed();
1559
1560 let base_statement = choice((
1562 just(Token::Newline).to(Stmt::Empty),
1563 set_command,
1564 env_scoped,
1565 assignment_parser().map(Stmt::Assignment),
1566 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), break_stmt,
1570 continue_stmt,
1571 return_stmt,
1572 exit_stmt,
1573 test_expr_stmt_parser().map(Stmt::Test),
1574 arith_cond_parser().map(Stmt::Arith),
1575 pipeline_parser(choice((
1577 compound.map(|s| PipelineStage::Compound(Box::new(s))),
1578 command_stage_parser(),
1579 )))
1580 .map(pipeline_into_stmt),
1581 ))
1582 .boxed();
1583
1584 base_statement
1590 .clone()
1591 .foldl(
1592 choice((
1593 just(Token::And).to(true), just(Token::Or).to(false), ))
1596 .then(base_statement)
1597 .repeated(),
1598 |left, (is_and, right): (bool, Stmt)| {
1599 if is_and {
1600 Stmt::AndChain {
1601 left: Box::new(left),
1602 right: Box::new(right),
1603 }
1604 } else {
1605 Stmt::OrChain {
1606 left: Box::new(left),
1607 right: Box::new(right),
1608 }
1609 }
1610 },
1611 )
1612 .then_ignore(terminator)
1613 })
1614}
1615
1616fn lvalue_subscript_parser<'tokens, I>(
1626) -> impl Parser<'tokens, I, VarSegment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1627where
1628 I: ValueInput<'tokens, Token = Token, Span = Span>,
1629{
1630 let interior = choice((
1631 select! { Token::SimpleVarRef(name) => VarSegment::Dynamic(name) },
1632 select! { Token::String(s) => VarSegment::Key(s) },
1633 select! { Token::SingleString(s) => VarSegment::Key(s) },
1634 select! { Token::Int(n) => VarSegment::Index(n) },
1635 select! { Token::Ident(s) => parse_subscript(&s) },
1636 select! { Token::NumberIdent(s) => parse_subscript(&s) },
1639 select! { Token::NumericLiteral(d) => parse_subscript(&d.raw) },
1643 ));
1644
1645 just(Token::LBracket)
1646 .ignore_then(interior)
1647 .then_ignore(just(Token::RBracket))
1648 .labelled("subscript")
1649}
1650
1651fn lvalue_path_parser<'tokens, I>(
1657) -> impl Parser<'tokens, I, VarPath, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1658where
1659 I: ValueInput<'tokens, Token = Token, Span = Span>,
1660{
1661 ident_parser()
1662 .then(lvalue_subscript_parser().repeated().collect::<Vec<_>>())
1663 .map(|(name, subscripts)| {
1664 let mut segments = vec![VarSegment::Field(crate::ast::normalize_name(name))];
1667 segments.extend(subscripts);
1668 VarPath { segments }
1669 })
1670 .labelled("lvalue path")
1671}
1672
1673fn assignment_parser<'tokens, I>(
1679) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1680where
1681 I: ValueInput<'tokens, Token = Token, Span = Span>,
1682{
1683 let local_assignment = just(Token::Local)
1685 .ignore_then(lvalue_path_parser())
1686 .then_ignore(just(Token::Eq))
1687 .then(value_expr_parser())
1688 .map(|(path, value)| Assignment {
1689 path,
1690 value,
1691 local: true,
1692 });
1693
1694 let bash_assignment = lvalue_path_parser()
1697 .then_ignore(just(Token::Eq))
1698 .then(value_expr_parser())
1699 .map(|(path, value)| Assignment {
1700 path,
1701 value,
1702 local: false,
1703 });
1704
1705 choice((local_assignment, bash_assignment))
1706 .labelled("assignment")
1707 .boxed()
1708}
1709
1710fn posix_function_parser<'tokens, I, S>(
1714 stmt: S,
1715) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1716where
1717 I: ValueInput<'tokens, Token = Token, Span = Span>,
1718 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1719{
1720 ident_parser()
1721 .then_ignore(just(Token::LParen))
1722 .then_ignore(just(Token::RParen))
1723 .then_ignore(just(Token::LBrace))
1724 .then_ignore(just(Token::Newline).repeated())
1725 .then(
1726 stmt.repeated()
1727 .collect::<Vec<_>>()
1728 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1729 )
1730 .then_ignore(just(Token::Newline).repeated())
1731 .then_ignore(just(Token::RBrace))
1732 .map(|(name, body)| ToolDef { name, params: vec![], body })
1733 .labelled("POSIX function")
1734 .boxed()
1735}
1736
1737fn bash_function_parser<'tokens, I, S>(
1741 stmt: S,
1742) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1743where
1744 I: ValueInput<'tokens, Token = Token, Span = Span>,
1745 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1746{
1747 just(Token::Function)
1748 .ignore_then(ident_parser())
1749 .then_ignore(just(Token::LBrace))
1750 .then_ignore(just(Token::Newline).repeated())
1751 .then(
1752 stmt.repeated()
1753 .collect::<Vec<_>>()
1754 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1755 )
1756 .then_ignore(just(Token::Newline).repeated())
1757 .then_ignore(just(Token::RBrace))
1758 .map(|(name, body)| ToolDef { name, params: vec![], body })
1759 .labelled("bash function")
1760 .boxed()
1761}
1762
1763fn if_parser<'tokens, I, S>(
1770 stmt: S,
1771) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1772where
1773 I: ValueInput<'tokens, Token = Token, Span = Span>,
1774 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1775{
1776 let branch = condition_parser()
1778 .then_ignore(just(Token::Semi).or_not())
1779 .then_ignore(just(Token::Newline).repeated())
1780 .then_ignore(just(Token::Then))
1781 .then_ignore(just(Token::Newline).repeated())
1782 .then(
1783 stmt.clone()
1784 .repeated()
1785 .collect::<Vec<_>>()
1786 .map(|stmts: Vec<Stmt>| {
1787 stmts
1788 .into_iter()
1789 .filter(|s| !matches!(s, Stmt::Empty))
1790 .collect::<Vec<_>>()
1791 }),
1792 );
1793
1794 let elif_branch = just(Token::Elif)
1796 .ignore_then(condition_parser())
1797 .then_ignore(just(Token::Semi).or_not())
1798 .then_ignore(just(Token::Newline).repeated())
1799 .then_ignore(just(Token::Then))
1800 .then_ignore(just(Token::Newline).repeated())
1801 .then(
1802 stmt.clone()
1803 .repeated()
1804 .collect::<Vec<_>>()
1805 .map(|stmts: Vec<Stmt>| {
1806 stmts
1807 .into_iter()
1808 .filter(|s| !matches!(s, Stmt::Empty))
1809 .collect::<Vec<_>>()
1810 }),
1811 );
1812
1813 let else_branch = just(Token::Else)
1815 .ignore_then(just(Token::Newline).repeated())
1816 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1817 .map(|stmts: Vec<Stmt>| {
1818 stmts
1819 .into_iter()
1820 .filter(|s| !matches!(s, Stmt::Empty))
1821 .collect::<Vec<_>>()
1822 });
1823
1824 just(Token::If)
1825 .ignore_then(branch)
1826 .then(elif_branch.repeated().collect::<Vec<_>>())
1827 .then(else_branch.or_not())
1828 .then_ignore(just(Token::Fi))
1829 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1830 build_if_chain(condition, then_branch, elif_branches, else_branch)
1832 })
1833 .labelled("if statement")
1834 .boxed()
1835}
1836
1837fn build_if_chain(
1844 condition: Expr,
1845 then_branch: Vec<Stmt>,
1846 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1847 else_branch: Option<Vec<Stmt>>,
1848) -> IfStmt {
1849 if elif_branches.is_empty() {
1850 IfStmt {
1852 condition: Box::new(condition),
1853 then_branch,
1854 else_branch,
1855 }
1856 } else {
1857 let (elif_cond, elif_then) = elif_branches.remove(0);
1859 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1860 IfStmt {
1861 condition: Box::new(condition),
1862 then_branch,
1863 else_branch: Some(vec![Stmt::If(nested_if)]),
1864 }
1865 }
1866}
1867
1868fn for_parser<'tokens, I, S>(
1870 stmt: S,
1871) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1872where
1873 I: ValueInput<'tokens, Token = Token, Span = Span>,
1874 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1875{
1876 just(Token::For)
1877 .ignore_then(ident_parser())
1878 .then_ignore(just(Token::In))
1879 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1880 .then_ignore(just(Token::Semi).or_not())
1881 .then_ignore(just(Token::Newline).repeated())
1882 .then_ignore(just(Token::Do))
1883 .then_ignore(just(Token::Newline).repeated())
1884 .then(
1885 stmt.repeated()
1886 .collect::<Vec<_>>()
1887 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1888 )
1889 .then_ignore(just(Token::Done))
1890 .map(|((variable, items), body)| ForLoop {
1891 variable,
1892 items,
1893 body,
1894 })
1895 .labelled("for loop")
1896 .boxed()
1897}
1898
1899fn while_parser<'tokens, I, S>(
1901 stmt: S,
1902) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1903where
1904 I: ValueInput<'tokens, Token = Token, Span = Span>,
1905 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1906{
1907 just(Token::While)
1908 .ignore_then(condition_parser())
1909 .then_ignore(just(Token::Semi).or_not())
1910 .then_ignore(just(Token::Newline).repeated())
1911 .then_ignore(just(Token::Do))
1912 .then_ignore(just(Token::Newline).repeated())
1913 .then(
1914 stmt.repeated()
1915 .collect::<Vec<_>>()
1916 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1917 )
1918 .then_ignore(just(Token::Done))
1919 .map(|(condition, body)| WhileLoop {
1920 condition: Box::new(condition),
1921 body,
1922 })
1923 .labelled("while loop")
1924 .boxed()
1925}
1926
1927fn case_parser<'tokens, I, S>(
1934 stmt: S,
1935) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1936where
1937 I: ValueInput<'tokens, Token = Token, Span = Span>,
1938 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1939{
1940 let pattern_part = choice((
1943 select! { Token::GlobWord(s) => s },
1944 select! { Token::Ident(s) => s },
1945 select! { Token::NumberIdent(s) => s },
1946 select! { Token::DashNumWord(s) => s },
1947 select! { Token::AtWord(s) => s },
1948 select! { Token::DottedIdent(s) => s },
1949 select! { Token::String(s) => s },
1950 select! { Token::SingleString(s) => s },
1951 select! { Token::Int(n) => n.to_string() },
1952 select! { Token::Star => "*".to_string() },
1953 select! { Token::Question => "?".to_string() },
1954 select! { Token::Dot => ".".to_string() },
1955 select! { Token::DotDot => "..".to_string() },
1956 select! { Token::Tilde => "~".to_string() },
1957 select! { Token::TildePath(s) => s },
1958 select! { Token::RelativePath(s) => s },
1959 select! { Token::DotSlashPath(s) => s },
1960 select! { Token::Path(p) => p },
1961 select! { Token::VarRef(v) => v },
1962 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1963 choice((
1971 select! { Token::DoubleDashBare(s) => s },
1972 select! { Token::PlusBare(s) => s },
1973 select! { Token::MinusBare(s) => s },
1974 select! { Token::MinusAlone => "-".to_string() },
1975 select! { Token::DoubleDash => "--".to_string() },
1976 select! { Token::ShortFlag(s) => format!("-{}", s) },
1977 select! { Token::LongFlag(s) => format!("--{}", s) },
1978 select! { Token::PlusFlag(s) => format!("+{}", s) },
1979 )),
1980 just(Token::LBracket)
1982 .ignore_then(
1983 choice((
1984 select! { Token::Ident(s) => s },
1985 select! { Token::Int(n) => n.to_string() },
1986 just(Token::Colon).to(":".to_string()),
1987 just(Token::Bang).to("!".to_string()),
1989 select! { Token::ShortFlag(s) => format!("-{}", s) },
1991 ))
1992 .repeated()
1993 .at_least(1)
1994 .collect::<Vec<String>>()
1995 )
1996 .then_ignore(just(Token::RBracket))
1997 .map(|parts| format!("[{}]", parts.join(""))),
1998 just(Token::LBrace)
2000 .ignore_then(
2001 choice((
2002 select! { Token::Ident(s) => s },
2003 select! { Token::Int(n) => n.to_string() },
2004 ))
2005 .separated_by(just(Token::Comma))
2006 .at_least(1)
2007 .collect::<Vec<String>>()
2008 )
2009 .then_ignore(just(Token::RBrace))
2010 .map(|parts| format!("{{{}}}", parts.join(","))),
2011 ));
2012
2013 let pattern = pattern_part
2016 .repeated()
2017 .at_least(1)
2018 .collect::<Vec<String>>()
2019 .map(|parts| parts.join(""))
2020 .labelled("case pattern");
2021
2022 let patterns = pattern
2024 .separated_by(just(Token::Pipe))
2025 .at_least(1)
2026 .collect::<Vec<String>>()
2027 .labelled("case patterns");
2028
2029 let branch = just(Token::LParen)
2031 .or_not()
2032 .ignore_then(just(Token::Newline).repeated())
2033 .ignore_then(patterns)
2034 .then_ignore(just(Token::RParen))
2035 .then_ignore(just(Token::Newline).repeated())
2036 .then(
2037 stmt.clone()
2038 .repeated()
2039 .collect::<Vec<_>>()
2040 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
2041 )
2042 .then_ignore(just(Token::DoubleSemi))
2043 .then_ignore(just(Token::Newline).repeated())
2044 .map(|(patterns, body)| CaseBranch { patterns, body })
2045 .labelled("case branch");
2046
2047 just(Token::Case)
2048 .ignore_then(expr_parser())
2049 .then_ignore(just(Token::In))
2050 .then_ignore(just(Token::Newline).repeated())
2051 .then(branch.repeated().collect::<Vec<_>>())
2052 .then_ignore(just(Token::Esac))
2053 .map(|(expr, branches)| CaseStmt { expr, branches })
2054 .labelled("case statement")
2055 .boxed()
2056}
2057
2058fn pipeline_parser<'tokens, I, S>(
2067 stage: S,
2068) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2069where
2070 I: ValueInput<'tokens, Token = Token, Span = Span>,
2071 S: Parser<'tokens, I, PipelineStage, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2072{
2073 stage
2074 .separated_by(just(Token::Pipe))
2075 .at_least(1)
2076 .collect::<Vec<_>>()
2077 .then(just(Token::Amp).or_not())
2078 .map(|(stages, bg)| Pipeline {
2079 stages,
2080 background: bg.is_some(),
2081 })
2082 .labelled("pipeline")
2083 .boxed()
2084}
2085
2086fn command_stage_parser<'tokens, I>(
2088) -> impl Parser<'tokens, I, PipelineStage, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2089where
2090 I: ValueInput<'tokens, Token = Token, Span = Span>,
2091{
2092 command_parser().map(PipelineStage::Command)
2093}
2094
2095fn command_parser<'tokens, I>(
2099) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2100where
2101 I: ValueInput<'tokens, Token = Token, Span = Span>,
2102{
2103 let command_name = choice((
2109 ident_parser(),
2110 path_parser(),
2111 select! { Token::DotSlashPath(s) => s },
2112 just(Token::True).to("true".to_string()),
2113 just(Token::False).to("false".to_string()),
2114 just(Token::Colon).to(":".to_string()),
2115 just(Token::Dot).to(".".to_string()),
2116 ));
2117
2118 command_name
2128 .then(args_list_parser())
2129 .then(redirect_parser(primary_expr_parser()).repeated().collect::<Vec<_>>())
2130 .map(|((name, args), redirects)| Command {
2131 name,
2132 args,
2133 redirects,
2134 })
2135 .labelled("command")
2136 .boxed()
2137}
2138
2139fn pipeline_into_stmt(p: Pipeline) -> Stmt {
2144 if p.stages.len() == 1 && !p.background && p.stages[0].redirects().is_empty() {
2145 match p.stages.into_iter().next() {
2146 Some(PipelineStage::Compound(stmt)) => *stmt,
2150 Some(PipelineStage::Command(cmd)) => Stmt::Command(cmd),
2151 None => Stmt::Empty, }
2153 } else {
2154 Stmt::Pipeline(p)
2155 }
2156}
2157
2158fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
2162 cmd.redirects
2163 .iter()
2164 .filter(|r| {
2165 matches!(
2166 r.kind,
2167 RedirectKind::Stdin | RedirectKind::HereDoc(_) | RedirectKind::HereString
2168 )
2169 })
2170 .count()
2171 > 1
2172}
2173
2174fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
2178 stmts.iter().any(stmt_has_ambiguous_stdin)
2179}
2180
2181fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
2182 match stmt {
2183 Stmt::Command(c) => command_has_ambiguous_stdin(c),
2184 Stmt::Pipeline(p) => p.stages.iter().any(|stage| match stage {
2185 PipelineStage::Command(cmd) => command_has_ambiguous_stdin(cmd),
2186 PipelineStage::Compound(inner) => stmt_has_ambiguous_stdin(inner),
2187 }),
2188 Stmt::If(i) => {
2189 first_ambiguous_stdin(&i.then_branch)
2190 || i.else_branch
2191 .as_deref()
2192 .is_some_and(first_ambiguous_stdin)
2193 }
2194 Stmt::For(f) => first_ambiguous_stdin(&f.body),
2195 Stmt::While(w) => first_ambiguous_stdin(&w.body),
2196 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
2197 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
2198 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
2199 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
2200 }
2201 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
2202 Stmt::Assignment(_)
2203 | Stmt::Break(_)
2204 | Stmt::Continue(_)
2205 | Stmt::Return(_)
2206 | Stmt::Exit(_)
2207 | Stmt::Test(_)
2208 | Stmt::Arith(_)
2209 | Stmt::Empty => false,
2210 }
2211}
2212
2213fn is_glue_candidate(arg: &Arg) -> bool {
2225 matches!(
2226 arg,
2227 Arg::Positional(_) | Arg::LongFlag(_) | Arg::Named { .. } | Arg::WordAssign { .. }
2228 )
2229}
2230
2231fn reject_glued_args<'src>(
2249 args: Vec<(Arg, Span)>,
2250) -> Result<Vec<Arg>, Rich<'src, Token, Span>> {
2251 for pair in args.windows(2) {
2252 let (prev, prev_span) = &pair[0];
2253 let (next, next_span) = &pair[1];
2254 if is_glue_candidate(prev) && is_glue_candidate(next) && prev_span.end == next_span.start {
2255 return Err(Rich::custom(*next_span, GLUED_ARGS_MESSAGE));
2256 }
2257 }
2258 Ok(args.into_iter().map(|(arg, _)| arg).collect())
2259}
2260
2261const GLUED_ARGS_MESSAGE: &str = "adjacent words with no space between them are not joined into \
2265 one argument (kaish does no token pasting); quote the whole word, e.g. \
2266 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\"";
2267
2268fn is_glued_args_error(e: &Rich<'_, Token, Span>) -> bool {
2277 matches!(e.reason(), RichReason::Custom(msg) if msg.as_str() == GLUED_ARGS_MESSAGE)
2278}
2279
2280fn args_list_parser<'tokens, I>(
2284) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2285where
2286 I: ValueInput<'tokens, Token = Token, Span = Span>,
2287{
2288 let pre_dash = arg_before_double_dash_parser()
2296 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
2297 .repeated()
2298 .collect::<Vec<(Arg, Span)>>()
2299 .try_map(|args, _span| reject_glued_args(args));
2300
2301 let double_dash = select! {
2303 Token::DoubleDash => Arg::DoubleDash,
2304 };
2305
2306 let post_dash_arg = choice((
2308 post_dash_flag_value_parser(),
2316 select! {
2318 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
2319 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
2320 },
2321 word_assign_arg_parser(),
2328 test_operator_arg_parser(),
2330 primary_expr_parser().map(Arg::Positional),
2332 ));
2333
2334 let post_dash = post_dash_arg
2338 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
2339 .repeated()
2340 .collect::<Vec<(Arg, Span)>>()
2341 .try_map(|args, _span| reject_glued_args(args));
2342
2343 pre_dash
2345 .then(double_dash.then(post_dash).or_not())
2346 .map(|(mut args, maybe_dd)| {
2347 if let Some((dd, post)) = maybe_dd {
2348 args.push(dd);
2349 args.extend(post);
2350 }
2351 args
2352 })
2353}
2354
2355fn keyword_word<'tokens, I>(
2365) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2366where
2367 I: ValueInput<'tokens, Token = Token, Span = Span>,
2368{
2369 select! {
2370 Token::Set => "set",
2371 Token::Local => "local",
2372 Token::If => "if",
2373 Token::Then => "then",
2374 Token::Else => "else",
2375 Token::Elif => "elif",
2376 Token::Fi => "fi",
2377 Token::For => "for",
2378 Token::While => "while",
2379 Token::In => "in",
2380 Token::Do => "do",
2381 Token::Done => "done",
2382 Token::Case => "case",
2383 Token::Esac => "esac",
2384 Token::Function => "function",
2385 Token::Break => "break",
2386 Token::Continue => "continue",
2387 Token::Return => "return",
2388 Token::Exit => "exit",
2389 }
2390 .map(|s| s.to_string())
2391}
2392
2393fn post_dash_flag_value_parser<'tokens, I>(
2417) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2418where
2419 I: ValueInput<'tokens, Token = Token, Span = Span>,
2420{
2421 select! { Token::LongFlag(name) => name }
2422 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
2423 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
2424 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
2425 .try_map(
2426 |(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)),
2427 span| {
2428 if key_span.end != eq_span.start || eq_span.end != value_span.start {
2429 Err(Rich::custom(
2430 span,
2431 "a flag and its value must not have spaces around '=' \
2432 (use '--flag=value' not '--flag = value')",
2433 ))
2434 } else {
2435 Ok(Arg::Named { key, value })
2436 }
2437 },
2438 )
2439}
2440
2441fn word_assign_arg_parser<'tokens, I>(
2442) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2443where
2444 I: ValueInput<'tokens, Token = Token, Span = Span>,
2445{
2446 choice((
2447 select! { Token::Ident(s) => s },
2448 keyword_word(),
2449 ))
2450 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
2451 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
2452 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
2453 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
2454 if key_span.end != eq_span.start || eq_span.end != value_span.start {
2456 Err(Rich::custom(
2457 span,
2458 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
2459 ))
2460 } else {
2461 Ok(Arg::WordAssign { key, value })
2462 }
2463 })
2464}
2465
2466fn test_operator_arg_parser<'tokens, I>(
2483) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2484where
2485 I: ValueInput<'tokens, Token = Token, Span = Span>,
2486{
2487 select! {
2488 Token::Eq => "=",
2489 Token::EqEq => "==",
2490 Token::NotEq => "!=",
2491 Token::Bang => "!",
2492 }
2493 .map(|s| Arg::Positional(Expr::Literal(Value::String(s.to_string()))))
2494}
2495
2496fn arg_before_double_dash_parser<'tokens, I>(
2498) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2499where
2500 I: ValueInput<'tokens, Token = Token, Span = Span>,
2501{
2502 let long_flag_with_value = select! {
2504 Token::LongFlag(name) => name,
2505 }
2506 .then_ignore(just(Token::Eq))
2507 .then(primary_expr_parser())
2508 .map(|(key, value)| Arg::Named { key, value });
2509
2510 let long_flag = select! {
2512 Token::LongFlag(name) => Arg::LongFlag(name),
2513 };
2514
2515 let short_flag = select! {
2517 Token::ShortFlag(name) => Arg::ShortFlag(name),
2518 };
2519
2520 let named = word_assign_arg_parser();
2522
2523 let positional = primary_expr_parser().map(Arg::Positional);
2525
2526 let test_operator = test_operator_arg_parser();
2530
2531 choice((
2534 long_flag_with_value,
2535 long_flag,
2536 short_flag,
2537 named,
2538 test_operator,
2539 positional,
2540 ))
2541 .boxed()
2542}
2543
2544fn redirect_parser<'tokens, I, T>(
2558 target: T,
2559) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2560where
2561 I: ValueInput<'tokens, Token = Token, Span = Span>,
2562 T: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2563{
2564 let target = target
2577 .clone()
2578 .map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) })
2579 .then(target.clone().map_with(|_, e| e.span()).rewind().or_not())
2580 .try_map(|((expr, span), glued), _| match glued {
2581 Some(next_span) if next_span.start == span.end => Err(Rich::custom(
2582 next_span,
2583 "adjacent words with no space between them are not joined into the redirect \
2584 target (kaish does no token pasting); quote the whole target, e.g. \
2585 \"/tmp/$(echo x).txt\"",
2586 )),
2587 _ => Ok(expr),
2588 })
2589 .boxed();
2590
2591 let regular_redirect = select! {
2593 Token::GtGt => RedirectKind::StdoutAppend,
2594 Token::Gt => RedirectKind::StdoutOverwrite,
2595 Token::Lt => RedirectKind::Stdin,
2596 Token::Stderr => RedirectKind::Stderr,
2597 Token::Both => RedirectKind::Both,
2598 }
2599 .then(target.clone())
2600 .map(|(kind, target)| Redirect { kind, target });
2601
2602 let heredoc_redirect = just(Token::HereDocStart)
2610 .ignore_then(select! { Token::HereDoc(data) => data })
2611 .try_map(|data: HereDocData, span| {
2612 let meta = HereDocMeta {
2615 delimiter: data.delimiter.clone(),
2616 literal: data.literal,
2617 strip_tabs: data.strip_tabs,
2618 body: data.source_body.clone(),
2619 body_offset: data.body_start_offset,
2620 };
2621 let target = if data.literal {
2622 let body = if data.strip_tabs {
2623 crate::interpreter::strip_leading_tabs(&data.content)
2624 } else {
2625 data.content
2626 };
2627 Expr::Literal(Value::String(body))
2628 } else {
2629 let parts =
2630 parse_interpolated_string_spanned(&data.content, data.body_start_offset)
2631 .map_err(|msg| Rich::custom(span, msg))?;
2632 if parts.len() == 1 && !data.strip_tabs {
2636 if let StringPart::Literal(text) = &parts[0].part {
2637 return Ok(Redirect {
2638 kind: RedirectKind::HereDoc(meta),
2639 target: Expr::Literal(Value::String(text.clone())),
2640 });
2641 }
2642 }
2643 Expr::HereDocBody {
2644 parts,
2645 strip_tabs: data.strip_tabs,
2646 }
2647 };
2648 Ok(Redirect {
2649 kind: RedirectKind::HereDoc(meta),
2650 target,
2651 })
2652 });
2653
2654 let herestring_redirect = just(Token::HereString)
2658 .ignore_then(target.clone())
2659 .map(|target| Redirect {
2660 kind: RedirectKind::HereString,
2661 target,
2662 });
2663
2664 let merge_stderr_redirect = just(Token::StderrToStdout)
2666 .map(|_| Redirect {
2667 kind: RedirectKind::MergeStderr,
2668 target: Expr::Literal(Value::Null),
2670 });
2671
2672 let merge_stdout_redirect = choice((
2674 just(Token::StdoutToStderr),
2675 just(Token::StdoutToStderr2),
2676 ))
2677 .map(|_| Redirect {
2678 kind: RedirectKind::MergeStdout,
2679 target: Expr::Literal(Value::Null),
2681 });
2682
2683 choice((
2684 heredoc_redirect,
2685 herestring_redirect,
2686 merge_stderr_redirect,
2687 merge_stdout_redirect,
2688 regular_redirect,
2689 ))
2690 .labelled("redirect")
2691 .boxed()
2692}
2693
2694fn arith_cond_parser<'tokens, I>(
2709) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2710where
2711 I: ValueInput<'tokens, Token = Token, Span = Span>,
2712{
2713 select! { Token::ArithCond(expr) => expr }.labelled("arithmetic condition")
2714}
2715
2716fn test_expr_stmt_parser<'tokens, I>(
2718) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2719where
2720 I: ValueInput<'tokens, Token = Token, Span = Span>,
2721{
2722 let file_test_op = select! {
2724 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
2725 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
2726 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
2727 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
2728 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
2729 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
2730 Token::ShortFlag(s) if s == "L" => FileTestOp::IsSymlink,
2731 Token::ShortFlag(s) if s == "h" => FileTestOp::IsSymlink,
2732 };
2733
2734 let string_test_op = select! {
2738 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
2739 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
2740 Token::ShortFlag(s) if s == "list" => StringTestOp::IsList,
2741 Token::ShortFlag(s) if s == "record" => StringTestOp::IsRecord,
2742 };
2743
2744 let cmp_op = choice((
2747 just(Token::EqEq).to(TestCmpOp::Eq),
2748 just(Token::Eq).to(TestCmpOp::Eq),
2749 just(Token::NotEq).to(TestCmpOp::NotEq),
2750 just(Token::Match).to(TestCmpOp::Match),
2751 just(Token::NotMatch).to(TestCmpOp::NotMatch),
2752 just(Token::Gt).to(TestCmpOp::Gt),
2753 just(Token::Lt).to(TestCmpOp::Lt),
2754 just(Token::GtEq).to(TestCmpOp::GtEq),
2755 just(Token::LtEq).to(TestCmpOp::LtEq),
2756 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
2757 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
2758 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
2759 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
2760 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
2761 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
2762 ));
2763
2764 let file_test = file_test_op
2766 .then(primary_expr_parser())
2767 .map(|(op, path)| TestExpr::FileTest {
2768 op,
2769 path: Box::new(path),
2770 });
2771
2772 let string_test = string_test_op
2774 .then(primary_expr_parser())
2775 .map(|(op, value)| TestExpr::StringTest {
2776 op,
2777 value: Box::new(value),
2778 });
2779
2780 let comparison = primary_expr_parser()
2782 .then(cmp_op)
2783 .then(primary_expr_parser())
2784 .map(|((left, op), right)| TestExpr::Comparison {
2785 left: Box::new(left),
2786 op,
2787 right: Box::new(right),
2788 });
2789
2790 let not_in = primary_expr_parser()
2796 .then_ignore(select! { Token::Ident(s) if s == "not" => () })
2797 .then_ignore(just(Token::In))
2798 .then(value_primary_parser())
2799 .map(|(left, right)| TestExpr::NotIn {
2800 left: Box::new(left),
2801 right: Box::new(right),
2802 });
2803
2804 let in_ = primary_expr_parser()
2805 .then_ignore(just(Token::In))
2806 .then(value_primary_parser())
2807 .map(|(left, right)| TestExpr::In {
2808 left: Box::new(left),
2809 right: Box::new(right),
2810 });
2811
2812 let primary_test = choice((file_test, string_test, not_in, in_, comparison));
2814
2815 let unary = recursive(|unary| {
2829 let not_expr = just(Token::Bang)
2830 .ignore_then(unary)
2831 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
2832 choice((not_expr, primary_test.clone()))
2833 });
2834
2835 let and_expr = unary.clone().foldl(
2837 just(Token::And).ignore_then(unary).repeated(),
2838 |left, right| TestExpr::And {
2839 left: Box::new(left),
2840 right: Box::new(right),
2841 },
2842 );
2843
2844 let compound_test = and_expr.clone().foldl(
2846 just(Token::Or).ignore_then(and_expr).repeated(),
2847 |left, right| TestExpr::Or {
2848 left: Box::new(left),
2849 right: Box::new(right),
2850 },
2851 );
2852
2853 just(Token::LBracket)
2856 .then(just(Token::LBracket))
2857 .ignore_then(compound_test)
2858 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
2859 .labelled("test expression")
2860 .boxed()
2861}
2862
2863fn condition_parser<'tokens, I>(
2878) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2879where
2880 I: ValueInput<'tokens, Token = Token, Span = Span>,
2881{
2882 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
2884
2885 let arith_condition = arith_cond_parser().map(Expr::Arith);
2887
2888 let command_condition = command_parser().map(Expr::Command);
2891
2892 let base = choice((test_expr_condition, arith_condition, command_condition));
2894
2895 let base = just(Token::Bang)
2899 .repeated()
2900 .foldr(base, |_, inner| Expr::Not(Box::new(inner)));
2901
2902 let and_expr = base.clone().foldl(
2905 just(Token::And).ignore_then(base).repeated(),
2906 |left, right| Expr::BinaryOp {
2907 left: Box::new(left),
2908 op: BinaryOp::And,
2909 right: Box::new(right),
2910 },
2911 );
2912
2913 and_expr
2915 .clone()
2916 .foldl(
2917 just(Token::Or).ignore_then(and_expr).repeated(),
2918 |left, right| Expr::BinaryOp {
2919 left: Box::new(left),
2920 op: BinaryOp::Or,
2921 right: Box::new(right),
2922 },
2923 )
2924 .labelled("condition")
2925 .boxed()
2926}
2927
2928fn expr_parser<'tokens, I>(
2935) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2936where
2937 I: ValueInput<'tokens, Token = Token, Span = Span>,
2938{
2939 primary_expr_parser()
2941}
2942
2943fn value_expr_parser<'tokens, I>(
2948) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2949where
2950 I: ValueInput<'tokens, Token = Token, Span = Span>,
2951{
2952 value_literal_parser()
2953}
2954
2955fn value_primary_parser<'tokens, I>(
2961) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2962where
2963 I: ValueInput<'tokens, Token = Token, Span = Span>,
2964{
2965 value_literal_parser()
2966}
2967
2968fn value_literal_parser<'tokens, I>(
2980) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2981where
2982 I: ValueInput<'tokens, Token = Token, Span = Span>,
2983{
2984 recursive(|value| {
2985 choice((
2986 list_literal_parser(value.clone()),
2987 record_literal_parser(value.clone()),
2988 primary_expr_parser(),
2989 ))
2990 })
2991 .boxed()
2992}
2993
2994fn list_literal_parser<'tokens, I, V>(
3002 value: V,
3003) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3004where
3005 I: ValueInput<'tokens, Token = Token, Span = Span>,
3006 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
3007{
3008 let spread_elem = just(Token::DotDotDot)
3009 .ignore_then(value.clone())
3010 .map(ListElem::Spread);
3011 let item_elem = value.map(ListElem::Item);
3012 let elem = choice((spread_elem, item_elem));
3013
3014 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
3015
3016 just(Token::LBracket)
3017 .ignore_then(just(Token::Newline).repeated())
3018 .ignore_then(elem.then_ignore(sep).repeated().collect::<Vec<_>>())
3019 .then_ignore(just(Token::RBracket))
3020 .map(Expr::ListLiteral)
3021 .labelled("list literal")
3022}
3023
3024fn record_literal_parser<'tokens, I, V>(
3032 value: V,
3033) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3034where
3035 I: ValueInput<'tokens, Token = Token, Span = Span>,
3036 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
3037{
3038 let bare_key = select! { Token::Ident(s) => RecordKey::Bare(s) };
3039 let double_key = select! { Token::String(s) => s }.try_map(|s, span| {
3045 let parts = parse_interpolated_string(&s)
3046 .map_err(|e| Rich::custom(span, format!("record key: {e}")))?;
3047 Ok(match parts.as_slice() {
3048 [] => RecordKey::Quoted(String::new()),
3049 [StringPart::Literal(lit)] => RecordKey::Quoted(lit.clone()),
3050 _ => RecordKey::Interpolated(parts),
3051 })
3052 });
3053 let single_key = select! { Token::SingleString(s) => RecordKey::Quoted(s) };
3054 let key = choice((double_key, single_key, bare_key)).labelled("record key");
3055
3056 let stray_bareword_after_value = select! { Token::Ident(s) => s }
3068 .then(just(Token::Colon).or_not())
3069 .rewind()
3070 .or_not()
3071 .try_map(|maybe, span| match maybe {
3072 Some((word, None)) => Err(Rich::custom(
3073 span,
3074 format!(
3075 "record value: unexpected word \"{word}\" after the value — a multi-word \
3076 value must be quoted, e.g. {{key: \"hello world\"}}"
3077 ),
3078 )),
3079 _ => Ok(()),
3080 });
3081
3082 let entry = key
3083 .then_ignore(just(Token::Colon))
3084 .then(value)
3085 .then_ignore(stray_bareword_after_value)
3086 .map(|(key, value)| RecordEntry { key, value });
3087
3088 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
3089
3090 just(Token::LBrace)
3091 .ignore_then(just(Token::Newline).repeated())
3092 .ignore_then(entry.then_ignore(sep).repeated().collect::<Vec<_>>())
3093 .then_ignore(just(Token::RBrace))
3094 .map(Expr::RecordLiteral)
3095 .labelled("record literal")
3096}
3097
3098fn primary_expr_parser<'tokens, I>(
3102) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3103where
3104 I: ValueInput<'tokens, Token = Token, Span = Span>,
3105{
3106 let positional = select! {
3108 Token::Positional(n) => Expr::Positional(n),
3109 Token::AllArgs => Expr::AllArgs,
3110 Token::ArgCount => Expr::ArgCount,
3111 Token::VarLength(name) => Expr::VarLength(parse_varpath(&format!("${{{name}}}"))),
3112 Token::LastExitCode => Expr::LastExitCode,
3113 Token::CurrentPid => Expr::CurrentPid,
3114 };
3115
3116 let arithmetic = select! {
3118 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
3119 };
3120
3121 let keyword_as_bareword = select! {
3124 Token::Done => "done",
3125 Token::Fi => "fi",
3126 Token::Then => "then",
3127 Token::Else => "else",
3128 Token::Elif => "elif",
3129 Token::In => "in",
3130 Token::Do => "do",
3131 Token::Esac => "esac",
3132 Token::Set => "set",
3137 }
3138 .map(|s| Expr::Literal(Value::String(s.to_string())));
3139
3140 let plus_minus_bare = select! {
3144 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
3145 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
3146 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
3147 Token::DoubleDashBare(s) => Expr::Literal(Value::String(s)),
3148 };
3149
3150 let glob_pattern = select! {
3152 Token::GlobWord(s) => Expr::GlobPattern(s),
3153 Token::Star => Expr::GlobPattern("*".to_string()),
3154 Token::Question => Expr::GlobPattern("?".to_string()),
3155 };
3156
3157 choice((
3164 positional,
3165 arithmetic,
3166 cmd_subst_parser(),
3167 var_expr_parser(),
3168 interpolated_string_parser(),
3169 literal_parser().map(Expr::Literal),
3170 numeric_literal_parser(),
3171 glob_pattern,
3173 ident_parser().map(|s| Expr::Literal(Value::String(s))),
3175 path_parser().map(|s| Expr::Literal(Value::String(s))),
3177 select! {
3180 Token::Dot => Expr::Literal(Value::String(".".into())),
3186 Token::DotDot => Expr::Literal(Value::String("..".into())),
3187 Token::Comma => Expr::Literal(Value::String(",".into())),
3199 Token::Colon => Expr::Literal(Value::String(":".into())),
3206 Token::Tilde => Expr::Literal(Value::String("~".into())),
3207 Token::TildePath(s) => Expr::Literal(Value::String(s)),
3208 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
3209 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
3210 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
3212 Token::DashNumWord(s) => Expr::Literal(Value::String(s)),
3215 Token::AtWord(s) => Expr::Literal(Value::String(s)),
3217 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
3222 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
3225 },
3226 plus_minus_bare,
3227 keyword_as_bareword,
3229 ))
3230 .labelled("expression")
3231 .boxed()
3232}
3233
3234fn var_expr_parser<'tokens, I>(
3237) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3238where
3239 I: ValueInput<'tokens, Token = Token, Span = Span>,
3240{
3241 choice((
3242 select! { Token::VarRef(raw) => raw }.try_map(|raw, span| {
3243 let inner = raw
3247 .strip_prefix("${")
3248 .and_then(|s| s.strip_suffix('}'))
3249 .unwrap_or(&raw);
3250 if !raw.starts_with("${?}")
3251 && !raw.starts_with("${$}")
3252 && find_default_separator(&raw).is_none()
3253 && let Some(msg) = bash_substring_hint(inner)
3254 {
3255 return Err(Rich::custom(span, msg));
3256 }
3257 if let Some(colon) = find_default_separator(&raw)
3265 && raw.len() > colon + 3
3266 && let Err(msg) =
3267 parse_interpolated_string(&unquote_default_word(&raw[colon + 2..raw.len() - 1]))
3268 {
3269 return Err(Rich::custom(span, msg));
3270 }
3271 Ok(parse_var_expr(&raw))
3272 }),
3273 select! { Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)) },
3274 ))
3275 .labelled("variable reference")
3276}
3277
3278type CmdSubstBody = (Vec<(Token, Span)>, Span);
3298
3299fn cmd_subst_body_tokens<'tokens, I>(
3300) -> impl Parser<'tokens, I, CmdSubstBody, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3301where
3302 I: ValueInput<'tokens, Token = Token, Span = Span>,
3303{
3304 custom(|inp| {
3305 let mut tracker = CmdSubstFrames::default();
3306 let mut body: Vec<(Token, Span)> = Vec::new();
3307 loop {
3308 let before = inp.cursor();
3309 match inp.next() {
3310 None => {
3311 let span = inp.span_since(&before);
3312 return Err(Rich::custom(
3313 span,
3314 "unterminated command substitution: missing `)`",
3315 ));
3316 }
3317 Some(tok) => {
3318 let span = inp.span_since(&before);
3319 let next = inp.peek();
3323 if tracker.step(&tok, next.as_ref()) {
3324 return Ok((body, span));
3325 }
3326 body.push((tok, span));
3327 }
3328 }
3329 }
3330 })
3331}
3332
3333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3335enum CmdSubstFrame {
3336 Subst,
3338 Paren,
3344 Case { awaiting_pattern: bool },
3353}
3354
3355#[derive(Default)]
3400struct CmdSubstFrames(Vec<CmdSubstFrame>);
3401
3402impl CmdSubstFrames {
3403 fn step(&mut self, tok: &Token, next: Option<&Token>) -> bool {
3410 match tok {
3411 Token::RParen => match self.0.last_mut() {
3412 None => return true,
3413 Some(CmdSubstFrame::Case { awaiting_pattern }) => {
3414 *awaiting_pattern = false;
3415 }
3416 Some(CmdSubstFrame::Paren) => {
3417 self.0.pop();
3418 if let Some(CmdSubstFrame::Case { awaiting_pattern }) = self.0.last_mut() {
3422 *awaiting_pattern = false;
3423 }
3424 }
3425 Some(CmdSubstFrame::Subst) => {
3426 self.0.pop();
3427 }
3428 },
3429 Token::LParen => self.0.push(CmdSubstFrame::Paren),
3430 Token::CmdSubstStart => self.0.push(CmdSubstFrame::Subst),
3431 Token::Case if !matches!(next, Some(Token::Eq)) => {
3438 self.0.push(CmdSubstFrame::Case { awaiting_pattern: true });
3439 }
3440 Token::Case => {}
3441 Token::DoubleSemi => {
3442 if let Some(CmdSubstFrame::Case { awaiting_pattern }) = self.0.last_mut() {
3443 *awaiting_pattern = true;
3444 }
3445 }
3446 Token::Esac
3447 if matches!(
3448 self.0.last(),
3449 Some(CmdSubstFrame::Case { awaiting_pattern: true })
3450 ) =>
3451 {
3452 self.0.pop();
3453 }
3454 _ => {}
3455 }
3456 false
3457 }
3458}
3459
3460fn find_cmd_subst_close(tokens: &[(Token, Span)]) -> Option<usize> {
3468 let mut tracker = CmdSubstFrames::default();
3469 (0..tokens.len()).find(|&i| {
3470 let next = tokens.get(i + 1).map(|(t, _)| t);
3471 tracker.step(&tokens[i].0, next)
3472 })
3473}
3474
3475fn validate_cmd_subst_bodies(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3496 let mut i = 0;
3497 while i < tokens.len() {
3498 if !matches!(tokens[i].0, Token::CmdSubstStart) {
3499 i += 1;
3500 continue;
3501 }
3502 let start_span = tokens[i].1;
3503 let rest = &tokens[i + 1..];
3504 let Some(close_rel) = find_cmd_subst_close(rest) else {
3505 return Err(vec![ParseError {
3506 span: start_span,
3507 message: "unterminated command substitution: missing `)`".to_string(),
3508 }]);
3509 };
3510 let body = &rest[..close_rel];
3511 let rparen_span = rest[close_rel].1;
3512 let end_span: Span = (rparen_span.start..rparen_span.start).into();
3513 parse_tokens(body.to_vec(), end_span, start_span)?;
3517 i += 1 + close_rel + 1;
3518 }
3519 Ok(())
3520}
3521
3522fn validate_interpolated_strings(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3534 for (tok, span) in tokens {
3535 let owned;
3536 let body = match tok {
3537 Token::String(s) => Some(s.as_str()),
3538 Token::VarRef(raw) => match find_default_separator(raw) {
3542 Some(colon) if raw.len() > colon + 3 => {
3543 owned = unquote_default_word(&raw[colon + 2..raw.len() - 1]);
3544 Some(owned.as_str())
3545 }
3546 _ => None,
3547 },
3548 _ => None,
3549 };
3550 if let Some(body) = body
3551 && let Err(message) = parse_interpolated_string(body)
3552 {
3553 return Err(vec![ParseError { span: *span, message }]);
3554 }
3555 }
3556 Ok(())
3557}
3558
3559fn validate_heredoc_bodies(tokens: &[(Token, Span)]) -> Result<(), Vec<ParseError>> {
3569 for (tok, span) in tokens {
3570 if let Token::HereDoc(d) = tok
3571 && !d.literal
3572 && let Err(message) = parse_interpolated_string_spanned(&d.content, 0)
3573 {
3574 return Err(vec![ParseError { span: *span, message }]);
3575 }
3576 }
3577 Ok(())
3578}
3579
3580fn is_word_token(tok: &Token) -> bool {
3588 match tok {
3589 Token::True | Token::False | Token::EqEq | Token::NotEq | Token::Eq
3590 | Token::Colon | Token::Comma | Token::DotDot | Token::Dot | Token::TildePath(_)
3591 | Token::Tilde | Token::RelativePath(_) | Token::DotSlashPath(_)
3592 | Token::DottedIdent(_) | Token::Star | Token::Bang | Token::Question
3593 | Token::GlobWord(_) | Token::Arithmetic(_) | Token::LongFlag(_)
3594 | Token::DoubleDashBare(_) | Token::PlusBare(_) | Token::MinusBare(_)
3595 | Token::JobSpec(_) | Token::MinusAlone | Token::String(_)
3596 | Token::SingleString(_) | Token::VarRef(_) | Token::SimpleVarRef(_)
3597 | Token::Positional(_) | Token::AllArgs | Token::ArgCount | Token::LastExitCode
3598 | Token::CurrentPid | Token::VarLength(_) | Token::Int(_) | Token::Float(_)
3599 | Token::NumericLiteral(_)
3600 | Token::NumberIdent(_) | Token::DashNumWord(_) | Token::AtWord(_)
3601 | Token::Path(_) | Token::Ident(_) => true,
3602
3603 Token::Done | Token::Fi | Token::Then | Token::Else | Token::Elif | Token::In
3610 | Token::Do | Token::Esac | Token::Set => false,
3611
3612 Token::Local | Token::If | Token::For | Token::While | Token::Case
3614 | Token::Function | Token::Break | Token::Continue | Token::Return | Token::Exit => false,
3615
3616 Token::TypeString | Token::TypeInt | Token::TypeFloat | Token::TypeBool => false,
3618
3619 Token::And | Token::Or | Token::Match | Token::NotMatch | Token::GtEq
3621 | Token::LtEq => false,
3622
3623 Token::Gt | Token::GtGt | Token::Lt | Token::Stderr | Token::Both
3627 | Token::HereString | Token::HereDocStart | Token::StderrToStdout
3628 | Token::StdoutToStderr | Token::StdoutToStderr2 => false,
3629
3630 Token::Pipe | Token::Amp | Token::Semi | Token::DoubleSemi | Token::DotDotDot
3632 | Token::LBrace | Token::RBrace | Token::LBracket | Token::RBracket
3633 | Token::LParen | Token::RParen => false,
3634
3635 Token::ArithCond(_) => false,
3638
3639 Token::CmdSubstStart => false,
3642
3643 Token::ShortFlag(_) | Token::PlusFlag(_) | Token::DoubleDash => false,
3646
3647 Token::HereDoc(_) | Token::InvalidFloatNoLeading | Token::InvalidFloatNoTrailing
3649 | Token::Comment | Token::Newline | Token::LineContinuation
3650 | Token::BacktickRejected => false,
3651 }
3652}
3653
3654fn is_assign_key_token(tok: &Token) -> bool {
3662 matches!(tok, Token::Ident(_) | Token::LongFlag(_))
3663}
3664
3665fn word_unit(tokens: &[(Token, Span)], i: usize) -> Option<(Span, usize)> {
3670 let (tok, span) = tokens.get(i)?;
3671 if matches!(tok, Token::CmdSubstStart) {
3672 let close_rel = find_cmd_subst_close(&tokens[i + 1..])?;
3673 let close_idx = i + 1 + close_rel;
3674 return Some(((span.start..tokens[close_idx].1.end).into(), close_idx + 1));
3675 }
3676 if is_word_token(tok) {
3677 return Some((*span, i + 1));
3678 }
3679 None
3680}
3681
3682fn glue_candidate_units(tokens: &[(Token, Span)]) -> Vec<Span> {
3691 let mut units = Vec::new();
3692 let mut i = 0;
3693 while i < tokens.len() {
3694 let (tok, span) = &tokens[i];
3695
3696 if is_assign_key_token(tok)
3697 && let Some((Token::Eq, eq_span)) = tokens.get(i + 1)
3698 && eq_span.start == span.end
3699 && let Some((value_span, next_i)) = word_unit(tokens, i + 2)
3700 && eq_span.end == value_span.start
3701 {
3702 units.push((span.start..value_span.end).into());
3703 i = next_i;
3704 continue;
3705 }
3706
3707 if let Some((unit_span, next_i)) = word_unit(tokens, i) {
3708 units.push(unit_span);
3709 i = next_i;
3710 continue;
3711 }
3712
3713 i += 1;
3714 }
3715 units
3716}
3717
3718fn validate_leading_zero_counts(
3743 tokens: &[(Token, Span)],
3744 error_starts: &[usize],
3745) -> Result<(), Vec<ParseError>> {
3746 for (i, pair) in tokens.windows(2).enumerate() {
3747 let keyword = match &pair[0].0 {
3748 Token::Break => "break",
3749 Token::Continue => "continue",
3750 _ => continue,
3751 };
3752 let at_statement_start = match i.checked_sub(1).map(|prev| &tokens[prev].0) {
3756 None => true,
3757 Some(
3758 Token::Newline
3759 | Token::Semi
3760 | Token::DoubleSemi
3761 | Token::Do
3762 | Token::Then
3763 | Token::Else
3764 | Token::LBrace
3765 | Token::And
3766 | Token::Or,
3767 ) => true,
3768 Some(_) => false,
3769 };
3770 if !at_statement_start {
3771 continue;
3772 }
3773 let Token::NumberIdent(word) = &pair[1].0 else {
3774 continue;
3775 };
3776 if !lexer::is_leading_zero_numeral(word) {
3777 continue;
3778 }
3779 if !error_starts.contains(&pair[1].1.start) {
3782 continue;
3783 }
3784 let sign = if word.starts_with('-') { "-" } else { "" };
3786 let digits = word.trim_start_matches('-').trim_start_matches('0');
3787 let count = format!("{sign}{}", if digits.is_empty() { "0" } else { digits });
3788 let message = if let Some((int_part, _)) = count.split_once('.') {
3792 let int_part = if int_part.is_empty() || int_part == "-" { "0" } else { int_part };
3793 format!(
3794 "`{keyword}` takes a whole-number loop count and `{word}` is text (leading \
3795 zero) — write a whole number such as `{keyword} {int_part}`"
3796 )
3797 } else {
3798 format!(
3799 "`{keyword}` takes a loop count and `{word}` is text (leading zero) — write \
3800 `{keyword} {count}`"
3801 )
3802 };
3803 return Err(vec![ParseError { span: pair[1].1, message }]);
3804 }
3805 Ok(())
3806}
3807
3808fn validate_glued_args(
3809 tokens: &[(Token, Span)],
3810 from_offset: usize,
3811) -> Result<(), Vec<ParseError>> {
3812 let units = glue_candidate_units(tokens);
3813 for i in 0..units.len().saturating_sub(1) {
3814 if units[i].end != units[i + 1].start {
3815 continue;
3816 }
3817 let mut start_idx = i;
3818 while start_idx > 0 && units[start_idx - 1].end == units[start_idx].start {
3819 start_idx -= 1;
3820 }
3821 let mut end_idx = i + 1;
3822 while end_idx + 1 < units.len() && units[end_idx].end == units[end_idx + 1].start {
3823 end_idx += 1;
3824 }
3825 if units[start_idx].start < from_offset {
3831 continue;
3832 }
3833 let span: Span = (units[start_idx].start..units[end_idx].end).into();
3834 return Err(vec![ParseError {
3835 span,
3836 message: GLUED_ARGS_MESSAGE.to_string(),
3837 }]);
3838 }
3839 Ok(())
3842}
3843
3844fn cmd_subst_parser<'tokens, I>(
3867) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3868where
3869 I: ValueInput<'tokens, Token = Token, Span = Span>,
3870{
3871 just(Token::CmdSubstStart)
3872 .ignore_then(cmd_subst_body_tokens())
3873 .try_map(|(body_tokens, rparen_span), outer_span| {
3874 let end_span: Span = (rparen_span.start..rparen_span.start).into();
3875 parse_tokens(body_tokens, end_span, outer_span)
3876 .map(|program| Expr::CommandSubst(program.statements))
3877 .map_err(|errs| {
3878 let first = errs.into_iter().next().unwrap_or_else(|| ParseError {
3879 span: outer_span,
3880 message: "command substitution failed to parse".to_string(),
3881 });
3882 Rich::custom(first.span, first.message)
3883 })
3884 })
3885 .labelled("command substitution")
3886}
3887
3888fn interpolated_string_parser<'tokens, I>(
3890) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3891where
3892 I: ValueInput<'tokens, Token = Token, Span = Span>,
3893{
3894 let double_quoted = select! {
3896 Token::String(s) => s,
3897 }
3898 .try_map(|s, span| {
3899 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
3901 let parts = parse_interpolated_string(&s)
3904 .map_err(|msg| Rich::custom(span, msg))?;
3905 if parts.len() == 1
3906 && let StringPart::Literal(text) = &parts[0] {
3907 return Ok(Expr::Literal(Value::String(text.clone())));
3908 }
3909 Ok(Expr::Interpolated(parts))
3910 } else {
3911 Ok(Expr::Literal(Value::String(s)))
3912 }
3913 });
3914
3915 let single_quoted = select! {
3917 Token::SingleString(s) => Expr::Literal(Value::String(s)),
3918 };
3919
3920 choice((single_quoted, double_quoted)).labelled("string")
3921}
3922
3923fn literal_parser<'tokens, I>(
3925) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3926where
3927 I: ValueInput<'tokens, Token = Token, Span = Span>,
3928{
3929 choice((
3930 select! {
3931 Token::True => Value::Bool(true),
3932 Token::False => Value::Bool(false),
3933 },
3934 select! {
3935 Token::Int(n) => Value::Int(n),
3936 Token::Float(f) => Value::Float(f),
3937 },
3938 ))
3939 .labelled("literal")
3940 .boxed()
3941}
3942
3943fn numeric_literal_parser<'tokens, I>(
3950) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3951where
3952 I: ValueInput<'tokens, Token = Token, Span = Span>,
3953{
3954 select! {
3955 Token::NumericLiteral(d) => Expr::NumericLiteral { value: d.value, raw: d.raw },
3956 }
3957 .labelled("literal")
3958}
3959
3960fn ident_parser<'tokens, I>(
3962) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3963where
3964 I: ValueInput<'tokens, Token = Token, Span = Span>,
3965{
3966 select! {
3967 Token::Ident(s) => s,
3968 }
3969 .labelled("identifier")
3970}
3971
3972fn path_parser<'tokens, I>(
3974) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
3975where
3976 I: ValueInput<'tokens, Token = Token, Span = Span>,
3977{
3978 select! {
3979 Token::Path(s) => s,
3980 }
3981 .labelled("path")
3982}
3983
3984#[cfg(test)]
3985#[allow(clippy::approx_constant)]
3986mod tests {
3987 use super::*;
3988 use proptest::strategy::Strategy;
3989
3990 fn pipeline_commands(p: &Pipeline) -> Vec<&Command> {
3994 p.stages
3995 .iter()
3996 .map(|stage| stage.as_command().expect("expected a command stage"))
3997 .collect()
3998 }
3999
4000 fn subst_cmd(expr: &Expr) -> &Command {
4002 match expr {
4003 Expr::CommandSubst(stmts) => match stmts.as_slice() {
4004 [Stmt::Command(cmd)] => cmd,
4005 other => panic!("expected a single command in $(), got {other:?}"),
4006 },
4007 other => panic!("expected command subst, got {other:?}"),
4008 }
4009 }
4010
4011 fn subst_pipeline(expr: &Expr) -> &Pipeline {
4013 match expr {
4014 Expr::CommandSubst(stmts) => match stmts.as_slice() {
4015 [Stmt::Pipeline(p)] => p,
4016 other => panic!("expected a single pipeline in $(), got {other:?}"),
4017 },
4018 other => panic!("expected command subst, got {other:?}"),
4019 }
4020 }
4021
4022 #[test]
4023 fn parse_empty() {
4024 let result = parse("");
4025 assert!(result.is_ok());
4026 assert_eq!(result.expect("ok").statements.len(), 0);
4027 }
4028
4029 #[test]
4030 fn parse_newlines_only() {
4031 let result = parse("\n\n\n");
4032 assert!(result.is_ok());
4033 }
4034
4035 #[test]
4036 fn parse_simple_command() {
4037 let result = parse("echo");
4038 assert!(result.is_ok());
4039 let program = result.expect("ok");
4040 assert_eq!(program.statements.len(), 1);
4041 assert!(matches!(&program.statements[0], Stmt::Command(_)));
4042 }
4043
4044 #[test]
4045 fn parse_command_with_string_arg() {
4046 let result = parse(r#"echo "hello""#);
4047 assert!(result.is_ok());
4048 let program = result.expect("ok");
4049 match &program.statements[0] {
4050 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
4051 _ => panic!("expected Command"),
4052 }
4053 }
4054
4055 #[test]
4056 fn parse_assignment() {
4057 let result = parse("X=5");
4058 assert!(result.is_ok());
4059 let program = result.expect("ok");
4060 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
4061 }
4062
4063 #[test]
4064 fn parse_pipeline() {
4065 let result = parse("a | b | c");
4066 assert!(result.is_ok());
4067 let program = result.expect("ok");
4068 match &program.statements[0] {
4069 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 3),
4070 _ => panic!("expected Pipeline"),
4071 }
4072 }
4073
4074 #[test]
4075 fn parse_background_job() {
4076 let result = parse("cmd &");
4077 assert!(result.is_ok());
4078 let program = result.expect("ok");
4079 match &program.statements[0] {
4080 Stmt::Pipeline(p) => assert!(p.background),
4081 _ => panic!("expected Pipeline with background"),
4082 }
4083 }
4084
4085 #[test]
4086 fn parse_if_simple() {
4087 let result = parse("if true; then echo; fi");
4088 assert!(result.is_ok());
4089 let program = result.expect("ok");
4090 assert!(matches!(&program.statements[0], Stmt::If(_)));
4091 }
4092
4093 #[test]
4094 fn parse_if_else() {
4095 let result = parse("if true; then echo; else echo; fi");
4096 assert!(result.is_ok());
4097 let program = result.expect("ok");
4098 match &program.statements[0] {
4099 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
4100 _ => panic!("expected If"),
4101 }
4102 }
4103
4104 #[test]
4105 fn parse_elif_simple() {
4106 let result = parse("if true; then echo a; elif false; then echo b; fi");
4107 assert!(result.is_ok(), "parse failed: {:?}", result);
4108 let program = result.expect("ok");
4109 match &program.statements[0] {
4110 Stmt::If(if_stmt) => {
4111 assert!(if_stmt.else_branch.is_some());
4113 let else_branch = if_stmt.else_branch.as_ref().unwrap();
4114 assert_eq!(else_branch.len(), 1);
4115 assert!(matches!(&else_branch[0], Stmt::If(_)));
4116 }
4117 _ => panic!("expected If"),
4118 }
4119 }
4120
4121 #[test]
4122 fn parse_elif_with_else() {
4123 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
4124 assert!(result.is_ok(), "parse failed: {:?}", result);
4125 let program = result.expect("ok");
4126 match &program.statements[0] {
4127 Stmt::If(outer_if) => {
4128 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
4130 assert_eq!(else_branch.len(), 1);
4131 match &else_branch[0] {
4132 Stmt::If(inner_if) => {
4133 assert!(inner_if.else_branch.is_some());
4135 }
4136 _ => panic!("expected nested If from elif"),
4137 }
4138 }
4139 _ => panic!("expected If"),
4140 }
4141 }
4142
4143 #[test]
4144 fn parse_multiple_elif() {
4145 let result = parse(
4147 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
4148 );
4149 assert!(result.is_ok(), "parse failed: {:?}", result);
4150 }
4151
4152 #[test]
4153 fn parse_for_loop() {
4154 let result = parse("for X in items; do echo; done");
4155 assert!(result.is_ok());
4156 let program = result.expect("ok");
4157 assert!(matches!(&program.statements[0], Stmt::For(_)));
4158 }
4159
4160 #[test]
4161 fn parse_brackets_not_array_literal() {
4162 let result = parse("cmd [1");
4164 let _ = result;
4167 }
4168
4169 #[test]
4170 fn parse_named_arg() {
4171 let result = parse("cmd foo=5");
4175 assert!(result.is_ok());
4176 let program = result.expect("ok");
4177 match &program.statements[0] {
4178 Stmt::Command(cmd) => {
4179 assert_eq!(cmd.args.len(), 1);
4180 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
4181 }
4182 _ => panic!("expected Command"),
4183 }
4184 }
4185
4186 #[test]
4187 fn parse_short_flag() {
4188 let result = parse("ls -l");
4189 assert!(result.is_ok());
4190 let program = result.expect("ok");
4191 match &program.statements[0] {
4192 Stmt::Command(cmd) => {
4193 assert_eq!(cmd.name, "ls");
4194 assert_eq!(cmd.args.len(), 1);
4195 match &cmd.args[0] {
4196 Arg::ShortFlag(name) => assert_eq!(name, "l"),
4197 _ => panic!("expected ShortFlag"),
4198 }
4199 }
4200 _ => panic!("expected Command"),
4201 }
4202 }
4203
4204 #[test]
4205 fn parse_long_flag() {
4206 let result = parse("git push --force");
4207 assert!(result.is_ok());
4208 let program = result.expect("ok");
4209 match &program.statements[0] {
4210 Stmt::Command(cmd) => {
4211 assert_eq!(cmd.name, "git");
4212 assert_eq!(cmd.args.len(), 2);
4213 match &cmd.args[0] {
4214 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
4215 _ => panic!("expected Positional push"),
4216 }
4217 match &cmd.args[1] {
4218 Arg::LongFlag(name) => assert_eq!(name, "force"),
4219 _ => panic!("expected LongFlag"),
4220 }
4221 }
4222 _ => panic!("expected Command"),
4223 }
4224 }
4225
4226 #[test]
4227 fn parse_long_flag_with_value() {
4228 let result = parse(r#"git commit --message="hello""#);
4229 assert!(result.is_ok());
4230 let program = result.expect("ok");
4231 match &program.statements[0] {
4232 Stmt::Command(cmd) => {
4233 assert_eq!(cmd.name, "git");
4234 assert_eq!(cmd.args.len(), 2);
4235 match &cmd.args[1] {
4236 Arg::Named { key, value } => {
4237 assert_eq!(key, "message");
4238 match value {
4239 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
4240 _ => panic!("expected String value"),
4241 }
4242 }
4243 _ => panic!("expected Named from --flag=value"),
4244 }
4245 }
4246 _ => panic!("expected Command"),
4247 }
4248 }
4249
4250 #[test]
4251 fn parse_mixed_flags_and_args() {
4252 let result = parse(r#"git commit -m "message" --amend"#);
4253 assert!(result.is_ok());
4254 let program = result.expect("ok");
4255 match &program.statements[0] {
4256 Stmt::Command(cmd) => {
4257 assert_eq!(cmd.name, "git");
4258 assert_eq!(cmd.args.len(), 4);
4259 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
4261 match &cmd.args[1] {
4263 Arg::ShortFlag(name) => assert_eq!(name, "m"),
4264 _ => panic!("expected ShortFlag -m"),
4265 }
4266 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
4268 match &cmd.args[3] {
4270 Arg::LongFlag(name) => assert_eq!(name, "amend"),
4271 _ => panic!("expected LongFlag --amend"),
4272 }
4273 }
4274 _ => panic!("expected Command"),
4275 }
4276 }
4277
4278 #[test]
4279 fn parse_redirect_stdout() {
4280 let result = parse("cmd > file");
4281 assert!(result.is_ok());
4282 let program = result.expect("ok");
4283 match &program.statements[0] {
4285 Stmt::Pipeline(p) => {
4286 assert_eq!(pipeline_commands(p).len(), 1);
4287 let cmd = pipeline_commands(p)[0];
4288 assert_eq!(cmd.redirects.len(), 1);
4289 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
4290 }
4291 _ => panic!("expected Pipeline"),
4292 }
4293 }
4294
4295 #[test]
4296 fn parse_var_ref() {
4297 let result = parse("echo ${VAR}");
4298 assert!(result.is_ok());
4299 let program = result.expect("ok");
4300 match &program.statements[0] {
4301 Stmt::Command(cmd) => {
4302 assert_eq!(cmd.args.len(), 1);
4303 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
4304 }
4305 _ => panic!("expected Command"),
4306 }
4307 }
4308
4309 #[test]
4310 fn parse_multiple_statements() {
4311 let result = parse("a\nb\nc");
4312 assert!(result.is_ok());
4313 let program = result.expect("ok");
4314 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
4315 assert_eq!(non_empty.len(), 3);
4316 }
4317
4318 #[test]
4319 fn parse_semicolon_separated() {
4320 let result = parse("a; b; c");
4321 assert!(result.is_ok());
4322 let program = result.expect("ok");
4323 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
4324 assert_eq!(non_empty.len(), 3);
4325 }
4326
4327 #[test]
4328 fn parse_complex_pipeline() {
4329 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
4330 assert!(result.is_ok());
4331 let program = result.expect("ok");
4332 match &program.statements[0] {
4333 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 3),
4334 _ => panic!("expected Pipeline"),
4335 }
4336 }
4337
4338 #[test]
4339 fn parse_json_as_string_arg() {
4340 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
4342 assert!(result.is_ok());
4343 }
4344
4345 #[test]
4346 fn parse_mixed_args() {
4347 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
4348 assert!(result.is_ok());
4349 let program = result.expect("ok");
4350 match &program.statements[0] {
4351 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
4352 _ => panic!("expected Command"),
4353 }
4354 }
4355
4356 #[test]
4357 fn error_unterminated_string() {
4358 let result = parse(r#"echo "hello"#);
4359 assert!(result.is_err());
4360 }
4361
4362 #[test]
4363 fn error_unterminated_var_ref() {
4364 let result = parse("echo ${VAR");
4365 assert!(result.is_err());
4366 }
4367
4368 #[test]
4369 fn error_missing_fi() {
4370 let result = parse("if true; then echo");
4371 assert!(result.is_err());
4372 }
4373
4374 #[test]
4375 fn error_missing_done() {
4376 let result = parse("for X in items; do echo");
4377 assert!(result.is_err());
4378 }
4379
4380 #[test]
4381 fn parse_lvalue_single_index() {
4382 let result = parse("xs[0]=9").unwrap();
4383 match &result.statements[0] {
4384 Stmt::Assignment(a) => {
4385 assert_eq!(a.name(), "xs");
4386 assert_eq!(
4387 a.path.segments,
4388 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
4389 );
4390 assert!(!a.local);
4391 }
4392 other => panic!("expected assignment, got {:?}", other),
4393 }
4394 }
4395
4396 #[test]
4397 fn parse_lvalue_negative_index() {
4398 let result = parse("xs[-1]=7").unwrap();
4399 match &result.statements[0] {
4400 Stmt::Assignment(a) => assert_eq!(
4401 a.path.segments,
4402 vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)]
4403 ),
4404 other => panic!("expected assignment, got {:?}", other),
4405 }
4406 }
4407
4408 #[test]
4409 fn parse_lvalue_bareword_key() {
4410 let result = parse("user[email]=x").unwrap();
4411 match &result.statements[0] {
4412 Stmt::Assignment(a) => assert_eq!(
4413 a.path.segments,
4414 vec![
4415 VarSegment::Field("user".into()),
4416 VarSegment::Key("email".into())
4417 ]
4418 ),
4419 other => panic!("expected assignment, got {:?}", other),
4420 }
4421 }
4422
4423 #[test]
4424 fn parse_lvalue_chained_keys() {
4425 let result = parse("s[web][port]=9000").unwrap();
4426 match &result.statements[0] {
4427 Stmt::Assignment(a) => assert_eq!(
4428 a.path.segments,
4429 vec![
4430 VarSegment::Field("s".into()),
4431 VarSegment::Key("web".into()),
4432 VarSegment::Key("port".into())
4433 ]
4434 ),
4435 other => panic!("expected assignment, got {:?}", other),
4436 }
4437 }
4438
4439 #[test]
4440 fn parse_lvalue_dynamic_key() {
4441 let result = parse("r[$k]=v").unwrap();
4442 match &result.statements[0] {
4443 Stmt::Assignment(a) => assert_eq!(
4444 a.path.segments,
4445 vec![
4446 VarSegment::Field("r".into()),
4447 VarSegment::Dynamic("k".into())
4448 ]
4449 ),
4450 other => panic!("expected assignment, got {:?}", other),
4451 }
4452 }
4453
4454 #[test]
4455 fn parse_local_lvalue_spaced() {
4456 let result = parse("local xs[0] = 9").unwrap();
4457 match &result.statements[0] {
4458 Stmt::Assignment(a) => {
4459 assert!(a.local);
4460 assert_eq!(
4461 a.path.segments,
4462 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
4463 );
4464 }
4465 other => panic!("expected assignment, got {:?}", other),
4466 }
4467 }
4468
4469 #[test]
4470 fn env_prefix_subscripted_target_is_not_captured_as_env_scoped() {
4471 let result = parse("user={}\nuser[email]=x echo hi").unwrap();
4481 for stmt in &result.statements {
4482 assert!(
4483 !matches!(stmt, Stmt::EnvScoped { .. }),
4484 "a subscripted assignment must never be captured into EnvScoped: {stmt:?}"
4485 );
4486 }
4487 assert!(matches!(&result.statements[1], Stmt::Assignment(a) if a.name() == "user"));
4489 assert!(matches!(&result.statements[2], Stmt::Command(c) if c.name == "echo"));
4490 }
4491
4492 #[test]
4493 fn parse_nested_cmd_subst() {
4494 let result = parse("X=$(echo $(date))").unwrap();
4496 match &result.statements[0] {
4497 Stmt::Assignment(a) => {
4498 assert_eq!(a.name(), "X");
4499 let outer = subst_cmd(&a.value);
4500 assert_eq!(outer.name, "echo");
4501 match &outer.args[0] {
4503 Arg::Positional(inner_expr) => {
4504 assert_eq!(subst_cmd(inner_expr).name, "date");
4505 }
4506 other => panic!("expected nested cmd subst arg, got {:?}", other),
4507 }
4508 }
4509 other => panic!("expected assignment, got {:?}", other),
4510 }
4511 }
4512
4513 #[test]
4514 fn parse_deeply_nested_cmd_subst() {
4515 let result = parse("X=$(a $(b $(c)))").unwrap();
4517 match &result.statements[0] {
4518 Stmt::Assignment(a) => {
4519 let level1 = subst_cmd(&a.value);
4520 assert_eq!(level1.name, "a");
4521 match &level1.args[0] {
4522 Arg::Positional(level2_expr) => {
4523 let level2 = subst_cmd(level2_expr);
4524 assert_eq!(level2.name, "b");
4525 match &level2.args[0] {
4526 Arg::Positional(level3_expr) => {
4527 assert_eq!(subst_cmd(level3_expr).name, "c");
4528 }
4529 other => panic!("expected level3 cmd subst, got {:?}", other),
4530 }
4531 }
4532 other => panic!("expected level2 cmd subst, got {:?}", other),
4533 }
4534 }
4535 other => panic!("expected assignment, got {:?}", other),
4536 }
4537 }
4538
4539 #[test]
4544 fn value_int_preserved() {
4545 let result = parse("X=42").unwrap();
4546 match &result.statements[0] {
4547 Stmt::Assignment(a) => {
4548 assert_eq!(a.name(), "X");
4549 match &a.value {
4550 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
4551 other => panic!("expected int literal, got {:?}", other),
4552 }
4553 }
4554 other => panic!("expected assignment, got {:?}", other),
4555 }
4556 }
4557
4558 #[test]
4559 fn value_negative_int_preserved() {
4560 let result = parse("X=-99").unwrap();
4561 match &result.statements[0] {
4562 Stmt::Assignment(a) => match &a.value {
4563 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
4564 other => panic!("expected int, got {:?}", other),
4565 },
4566 other => panic!("expected assignment, got {:?}", other),
4567 }
4568 }
4569
4570 #[test]
4571 fn value_float_preserved() {
4572 let result = parse("PI=3.14").unwrap();
4573 match &result.statements[0] {
4574 Stmt::Assignment(a) => match &a.value {
4575 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
4576 other => panic!("expected float, got {:?}", other),
4577 },
4578 other => panic!("expected assignment, got {:?}", other),
4579 }
4580 }
4581
4582 #[test]
4583 fn value_string_preserved() {
4584 let result = parse(r#"echo "hello world""#).unwrap();
4585 match &result.statements[0] {
4586 Stmt::Command(cmd) => {
4587 assert_eq!(cmd.name, "echo");
4588 match &cmd.args[0] {
4589 Arg::Positional(Expr::Literal(Value::String(s))) => {
4590 assert_eq!(s, "hello world");
4591 }
4592 other => panic!("expected string arg, got {:?}", other),
4593 }
4594 }
4595 other => panic!("expected command, got {:?}", other),
4596 }
4597 }
4598
4599 #[test]
4600 fn value_string_with_escapes_preserved() {
4601 let result = parse(r#"echo "line1\nline2""#).unwrap();
4602 match &result.statements[0] {
4603 Stmt::Command(cmd) => match &cmd.args[0] {
4604 Arg::Positional(Expr::Literal(Value::String(s))) => {
4605 assert_eq!(s, "line1\nline2");
4606 }
4607 other => panic!("expected string, got {:?}", other),
4608 },
4609 other => panic!("expected command, got {:?}", other),
4610 }
4611 }
4612
4613 #[test]
4614 fn value_command_name_preserved() {
4615 let result = parse("my-command").unwrap();
4616 match &result.statements[0] {
4617 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
4618 other => panic!("expected command, got {:?}", other),
4619 }
4620 }
4621
4622 #[test]
4623 fn value_assignment_name_preserved() {
4624 let result = parse("MY_VAR=1").unwrap();
4625 match &result.statements[0] {
4626 Stmt::Assignment(a) => assert_eq!(a.name(), "MY_VAR"),
4627 other => panic!("expected assignment, got {:?}", other),
4628 }
4629 }
4630
4631 #[test]
4632 fn value_for_variable_preserved() {
4633 let result = parse("for ITEM in items; do echo; done").unwrap();
4634 match &result.statements[0] {
4635 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
4636 other => panic!("expected for, got {:?}", other),
4637 }
4638 }
4639
4640 #[test]
4641 fn value_varref_name_preserved() {
4642 let result = parse("echo ${MESSAGE}").unwrap();
4643 match &result.statements[0] {
4644 Stmt::Command(cmd) => match &cmd.args[0] {
4645 Arg::Positional(Expr::VarRef(path)) => {
4646 assert_eq!(path.segments.len(), 1);
4647 let VarSegment::Field(name) = &path.segments[0] else {
4648 panic!("expected root field, got {:?}", path.segments[0]);
4649 };
4650 assert_eq!(name, "MESSAGE");
4651 }
4652 other => panic!("expected varref, got {:?}", other),
4653 },
4654 other => panic!("expected command, got {:?}", other),
4655 }
4656 }
4657
4658 #[test]
4659 fn value_varref_field_access_preserved() {
4660 let result = parse("echo ${RESULT.data}").unwrap();
4661 match &result.statements[0] {
4662 Stmt::Command(cmd) => match &cmd.args[0] {
4663 Arg::Positional(Expr::VarRef(path)) => {
4664 assert_eq!(path.segments.len(), 2);
4668 let VarSegment::Field(a) = &path.segments[0] else {
4669 panic!("expected field, got {:?}", path.segments[0]);
4670 };
4671 let VarSegment::Field(b) = &path.segments[1] else {
4672 panic!("expected field, got {:?}", path.segments[1]);
4673 };
4674 assert_eq!(a, "RESULT");
4675 assert_eq!(b, "data");
4676 }
4677 other => panic!("expected varref, got {:?}", other),
4678 },
4679 other => panic!("expected command, got {:?}", other),
4680 }
4681 }
4682
4683 #[test]
4684 fn value_varref_index_parsed() {
4685 let result = parse("echo ${ITEMS[0]}").unwrap();
4688 match &result.statements[0] {
4689 Stmt::Command(cmd) => match &cmd.args[0] {
4690 Arg::Positional(Expr::VarRef(path)) => {
4691 assert_eq!(path.segments.len(), 2);
4692 let VarSegment::Field(name) = &path.segments[0] else {
4693 panic!("expected root field, got {:?}", path.segments[0]);
4694 };
4695 assert_eq!(name, "ITEMS");
4696 assert_eq!(path.segments[1], VarSegment::Index(0));
4697 }
4698 other => panic!("expected varref, got {:?}", other),
4699 },
4700 other => panic!("expected command, got {:?}", other),
4701 }
4702 }
4703
4704 #[test]
4705 fn value_named_arg_preserved() {
4706 let result = parse("cmd count=42").unwrap();
4710 match &result.statements[0] {
4711 Stmt::Command(cmd) => {
4712 assert_eq!(cmd.name, "cmd");
4713 match &cmd.args[0] {
4714 Arg::WordAssign { key, value } => {
4715 assert_eq!(key, "count");
4716 match value {
4717 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
4718 other => panic!("expected int, got {:?}", other),
4719 }
4720 }
4721 other => panic!("expected WordAssign arg, got {:?}", other),
4722 }
4723 }
4724 other => panic!("expected command, got {:?}", other),
4725 }
4726 }
4727
4728 #[test]
4729 fn value_function_def_name_preserved() {
4730 let result = parse("greet() { echo }").unwrap();
4731 match &result.statements[0] {
4732 Stmt::ToolDef(t) => {
4733 assert_eq!(t.name, "greet");
4734 assert!(t.params.is_empty());
4735 }
4736 other => panic!("expected function def, got {:?}", other),
4737 }
4738 }
4739
4740 #[test]
4745 fn parse_comparison_equals() {
4746 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
4748 match &result.statements[0] {
4749 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4750 Expr::Test(test) => match test.as_ref() {
4751 TestExpr::Comparison { left, op, right } => {
4752 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
4753 assert_eq!(*op, TestCmpOp::Eq);
4754 match right.as_ref() {
4755 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
4756 other => panic!("expected int, got {:?}", other),
4757 }
4758 }
4759 other => panic!("expected comparison, got {:?}", other),
4760 },
4761 other => panic!("expected test expr, got {:?}", other),
4762 },
4763 other => panic!("expected if, got {:?}", other),
4764 }
4765 }
4766
4767 #[test]
4768 fn parse_comparison_not_equals() {
4769 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
4770 match &result.statements[0] {
4771 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4772 Expr::Test(test) => match test.as_ref() {
4773 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
4774 other => panic!("expected comparison, got {:?}", other),
4775 },
4776 other => panic!("expected test expr, got {:?}", other),
4777 },
4778 other => panic!("expected if, got {:?}", other),
4779 }
4780 }
4781
4782 #[test]
4783 fn parse_comparison_less_than() {
4784 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
4785 match &result.statements[0] {
4786 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4787 Expr::Test(test) => match test.as_ref() {
4788 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
4789 other => panic!("expected comparison, got {:?}", other),
4790 },
4791 other => panic!("expected test expr, got {:?}", other),
4792 },
4793 other => panic!("expected if, got {:?}", other),
4794 }
4795 }
4796
4797 #[test]
4798 fn parse_comparison_greater_than() {
4799 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
4800 match &result.statements[0] {
4801 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4802 Expr::Test(test) => match test.as_ref() {
4803 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
4804 other => panic!("expected comparison, got {:?}", other),
4805 },
4806 other => panic!("expected test expr, got {:?}", other),
4807 },
4808 other => panic!("expected if, got {:?}", other),
4809 }
4810 }
4811
4812 #[test]
4813 fn parse_comparison_less_equal() {
4814 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
4815 match &result.statements[0] {
4816 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4817 Expr::Test(test) => match test.as_ref() {
4818 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
4819 other => panic!("expected comparison, got {:?}", other),
4820 },
4821 other => panic!("expected test expr, got {:?}", other),
4822 },
4823 other => panic!("expected if, got {:?}", other),
4824 }
4825 }
4826
4827 #[test]
4828 fn parse_comparison_greater_equal() {
4829 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
4830 match &result.statements[0] {
4831 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4832 Expr::Test(test) => match test.as_ref() {
4833 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
4834 other => panic!("expected comparison, got {:?}", other),
4835 },
4836 other => panic!("expected test expr, got {:?}", other),
4837 },
4838 other => panic!("expected if, got {:?}", other),
4839 }
4840 }
4841
4842 #[test]
4843 fn parse_regex_match() {
4844 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
4845 match &result.statements[0] {
4846 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4847 Expr::Test(test) => match test.as_ref() {
4848 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
4849 other => panic!("expected comparison, got {:?}", other),
4850 },
4851 other => panic!("expected test expr, got {:?}", other),
4852 },
4853 other => panic!("expected if, got {:?}", other),
4854 }
4855 }
4856
4857 #[test]
4858 fn parse_regex_not_match() {
4859 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
4860 match &result.statements[0] {
4861 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4862 Expr::Test(test) => match test.as_ref() {
4863 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
4864 other => panic!("expected comparison, got {:?}", other),
4865 },
4866 other => panic!("expected test expr, got {:?}", other),
4867 },
4868 other => panic!("expected if, got {:?}", other),
4869 }
4870 }
4871
4872 #[test]
4873 fn parse_string_interpolation() {
4874 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
4875 match &result.statements[0] {
4876 Stmt::Command(cmd) => match &cmd.args[0] {
4877 Arg::Positional(Expr::Interpolated(parts)) => {
4878 assert_eq!(parts.len(), 3);
4879 match &parts[0] {
4880 StringPart::Literal(s) => assert_eq!(s, "Hello "),
4881 other => panic!("expected literal, got {:?}", other),
4882 }
4883 match &parts[1] {
4884 StringPart::Var(path) => {
4885 assert_eq!(path.segments.len(), 1);
4886 let VarSegment::Field(name) = &path.segments[0] else {
4887 panic!("expected root field, got {:?}", path.segments[0]);
4888 };
4889 assert_eq!(name, "NAME");
4890 }
4891 other => panic!("expected var, got {:?}", other),
4892 }
4893 match &parts[2] {
4894 StringPart::Literal(s) => assert_eq!(s, "!"),
4895 other => panic!("expected literal, got {:?}", other),
4896 }
4897 }
4898 other => panic!("expected interpolated, got {:?}", other),
4899 },
4900 other => panic!("expected command, got {:?}", other),
4901 }
4902 }
4903
4904 #[test]
4905 fn parse_string_interpolation_multiple_vars() {
4906 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
4907 match &result.statements[0] {
4908 Stmt::Command(cmd) => match &cmd.args[0] {
4909 Arg::Positional(Expr::Interpolated(parts)) => {
4910 assert_eq!(parts.len(), 3);
4912 assert!(matches!(&parts[0], StringPart::Var(_)));
4913 assert!(matches!(&parts[1], StringPart::Literal(_)));
4914 assert!(matches!(&parts[2], StringPart::Var(_)));
4915 }
4916 other => panic!("expected interpolated, got {:?}", other),
4917 },
4918 other => panic!("expected command, got {:?}", other),
4919 }
4920 }
4921
4922 #[test]
4923 fn parse_empty_function_body() {
4924 let result = parse("empty() { }").unwrap();
4925 match &result.statements[0] {
4926 Stmt::ToolDef(t) => {
4927 assert_eq!(t.name, "empty");
4928 assert!(t.params.is_empty());
4929 assert!(t.body.is_empty());
4930 }
4931 other => panic!("expected function def, got {:?}", other),
4932 }
4933 }
4934
4935 #[test]
4936 fn parse_bash_style_function() {
4937 let result = parse("function greet { echo hello }").unwrap();
4938 match &result.statements[0] {
4939 Stmt::ToolDef(t) => {
4940 assert_eq!(t.name, "greet");
4941 assert!(t.params.is_empty());
4942 assert_eq!(t.body.len(), 1);
4943 }
4944 other => panic!("expected function def, got {:?}", other),
4945 }
4946 }
4947
4948 #[test]
4949 fn parse_comparison_string_values() {
4950 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
4951 match &result.statements[0] {
4952 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4953 Expr::Test(test) => match test.as_ref() {
4954 TestExpr::Comparison { left, op, right } => {
4955 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
4956 assert_eq!(*op, TestCmpOp::Eq);
4957 match right.as_ref() {
4958 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
4959 other => panic!("expected string, got {:?}", other),
4960 }
4961 }
4962 other => panic!("expected comparison, got {:?}", other),
4963 },
4964 other => panic!("expected test expr, got {:?}", other),
4965 },
4966 other => panic!("expected if, got {:?}", other),
4967 }
4968 }
4969
4970 #[test]
4975 fn parse_cmd_subst_simple() {
4976 let result = parse("X=$(echo)").unwrap();
4977 match &result.statements[0] {
4978 Stmt::Assignment(a) => {
4979 assert_eq!(a.name(), "X");
4980 assert_eq!(subst_cmd(&a.value).name, "echo");
4981 }
4982 other => panic!("expected assignment, got {:?}", other),
4983 }
4984 }
4985
4986 #[test]
4987 fn parse_cmd_subst_with_args() {
4988 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
4989 match &result.statements[0] {
4990 Stmt::Assignment(a) => {
4991 let cmd = subst_cmd(&a.value);
4992 assert_eq!(cmd.name, "fetch");
4993 assert_eq!(cmd.args.len(), 1);
4994 match &cmd.args[0] {
4995 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
4996 other => panic!("expected WordAssign arg, got {:?}", other),
4997 }
4998 }
4999 other => panic!("expected assignment, got {:?}", other),
5000 }
5001 }
5002
5003 #[test]
5004 fn parse_cmd_subst_pipeline() {
5005 let result = parse("X=$(cat file | grep pattern)").unwrap();
5006 match &result.statements[0] {
5007 Stmt::Assignment(a) => {
5008 let pipeline = subst_pipeline(&a.value);
5009 assert_eq!(pipeline_commands(pipeline).len(), 2);
5010 assert_eq!(pipeline_commands(pipeline)[0].name, "cat");
5011 assert_eq!(pipeline_commands(pipeline)[1].name, "grep");
5012 }
5013 other => panic!("expected assignment, got {:?}", other),
5014 }
5015 }
5016
5017 #[test]
5018 fn parse_cmd_subst_with_redirect() {
5019 let result = parse("X=$(echo hi > out.txt)").unwrap();
5024 match &result.statements[0] {
5025 Stmt::Assignment(a) => {
5026 let pipeline = subst_pipeline(&a.value);
5027 assert_eq!(pipeline_commands(pipeline).len(), 1);
5028 let cmd = pipeline_commands(pipeline)[0];
5029 assert_eq!(cmd.name, "echo");
5030 assert_eq!(cmd.redirects.len(), 1);
5031 assert!(matches!(
5032 cmd.redirects[0].kind,
5033 RedirectKind::StdoutOverwrite
5034 ));
5035 }
5036 other => panic!("expected assignment, got {:?}", other),
5037 }
5038 }
5039
5040 #[test]
5041 fn parse_cmd_subst_redirect_target_with_nested_subst() {
5042 let result = parse("X=$(echo hi > $(echo f))").unwrap();
5048 match &result.statements[0] {
5049 Stmt::Assignment(a) => {
5050 let pipeline = subst_pipeline(&a.value);
5051 assert_eq!(pipeline_commands(pipeline).len(), 1);
5052 let cmd = pipeline_commands(pipeline)[0];
5053 assert_eq!(cmd.name, "echo");
5054 assert_eq!(cmd.redirects.len(), 1);
5055 assert!(
5056 matches!(cmd.redirects[0].target, Expr::CommandSubst(_)),
5057 "redirect target should be a nested command substitution, got {:?}",
5058 cmd.redirects[0].target
5059 );
5060 }
5061 other => panic!("expected assignment, got {:?}", other),
5062 }
5063 }
5064
5065 #[test]
5066 fn parse_cmd_subst_chain_with_redirect() {
5067 let result = parse("X=$(echo a && echo b > out.txt)").unwrap();
5071 let stmts = match &result.statements[0] {
5072 Stmt::Assignment(a) => match &a.value {
5073 Expr::CommandSubst(s) => s,
5074 other => panic!("expected command subst, got {:?}", other),
5075 },
5076 other => panic!("expected assignment, got {:?}", other),
5077 };
5078 match stmts.as_slice() {
5079 [Stmt::AndChain { left, right }] => {
5080 assert!(
5082 matches!(**left, Stmt::Command(_)),
5083 "left of && should be a bare command, got {:?}",
5084 left
5085 );
5086 match &**right {
5088 Stmt::Pipeline(p) => {
5089 assert_eq!(pipeline_commands(p).len(), 1);
5090 assert_eq!(pipeline_commands(p)[0].name, "echo");
5091 assert_eq!(pipeline_commands(p)[0].redirects.len(), 1);
5092 }
5093 other => panic!("right should be a redirect-bearing pipeline, got {:?}", other),
5094 }
5095 }
5096 other => panic!("expected a single AndChain, got {:?}", other),
5097 }
5098 }
5099
5100 #[test]
5101 fn parse_cmd_subst_in_condition() {
5102 let result = parse("if kaish-validate; then echo; fi").unwrap();
5104 match &result.statements[0] {
5105 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5106 Expr::Command(cmd) => {
5107 assert_eq!(cmd.name, "kaish-validate");
5108 }
5109 other => panic!("expected command, got {:?}", other),
5110 },
5111 other => panic!("expected if, got {:?}", other),
5112 }
5113 }
5114
5115 #[test]
5130 fn parse_cmd_subst_unquoted_for_loop() {
5131 let result = parse("X=$(for f in a b; do echo $f; done)").unwrap();
5132 let stmts = match &result.statements[0] {
5133 Stmt::Assignment(a) => match &a.value {
5134 Expr::CommandSubst(s) => s,
5135 other => panic!("expected command subst, got {:?}", other),
5136 },
5137 other => panic!("expected assignment, got {:?}", other),
5138 };
5139 match stmts.as_slice() {
5140 [Stmt::For(f)] => {
5141 assert_eq!(f.variable, "f");
5142 assert_eq!(f.items.len(), 2);
5143 assert!(matches!(f.body.as_slice(), [Stmt::Command(c)] if c.name == "echo"));
5144 }
5145 other => panic!("expected a single For statement, got {:?}", other),
5146 }
5147 }
5148
5149 #[test]
5150 fn parse_cmd_subst_unquoted_while_loop() {
5151 let result = parse("X=$(while false; do echo x; done)").unwrap();
5152 let stmts = match &result.statements[0] {
5153 Stmt::Assignment(a) => match &a.value {
5154 Expr::CommandSubst(s) => s,
5155 other => panic!("expected command subst, got {:?}", other),
5156 },
5157 other => panic!("expected assignment, got {:?}", other),
5158 };
5159 assert!(
5160 matches!(stmts.as_slice(), [Stmt::While(w)] if matches!(w.body.as_slice(), [Stmt::Command(c)] if c.name == "echo")),
5161 "expected a single While statement, got {stmts:?}"
5162 );
5163 }
5164
5165 #[test]
5166 fn parse_cmd_subst_unquoted_if_else() {
5167 let result = parse("X=$(if true; then echo one; else echo two; fi)").unwrap();
5168 let stmts = match &result.statements[0] {
5169 Stmt::Assignment(a) => match &a.value {
5170 Expr::CommandSubst(s) => s,
5171 other => panic!("expected command subst, got {:?}", other),
5172 },
5173 other => panic!("expected assignment, got {:?}", other),
5174 };
5175 match stmts.as_slice() {
5176 [Stmt::If(i)] => {
5177 assert!(i.else_branch.is_some(), "expected an else branch");
5178 assert!(matches!(i.then_branch.as_slice(), [Stmt::Command(c)] if c.name == "echo"));
5179 }
5180 other => panic!("expected a single If statement, got {:?}", other),
5181 }
5182 }
5183
5184 #[test]
5185 fn parse_cmd_subst_unquoted_case() {
5186 let result = parse("X=$(case a in a) echo hit;; esac)").unwrap();
5191 let stmts = match &result.statements[0] {
5192 Stmt::Assignment(a) => match &a.value {
5193 Expr::CommandSubst(s) => s,
5194 other => panic!("expected command subst, got {:?}", other),
5195 },
5196 other => panic!("expected assignment, got {:?}", other),
5197 };
5198 match stmts.as_slice() {
5199 [Stmt::Case(c)] => {
5200 assert_eq!(c.branches.len(), 1);
5201 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5202 }
5203 other => panic!("expected a single Case statement, got {:?}", other),
5204 }
5205 }
5206
5207 #[test]
5208 fn parse_cmd_subst_unquoted_case_with_parenthesized_pattern() {
5209 let result = parse("X=$(case a in (a) echo hit;; esac)").unwrap();
5212 let stmts = match &result.statements[0] {
5213 Stmt::Assignment(a) => match &a.value {
5214 Expr::CommandSubst(s) => s,
5215 other => panic!("expected command subst, got {:?}", other),
5216 },
5217 other => panic!("expected assignment, got {:?}", other),
5218 };
5219 assert!(
5220 matches!(stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
5221 "expected a single Case statement, got {stmts:?}"
5222 );
5223 }
5224
5225 #[test]
5226 fn parse_cmd_subst_unquoted_case_parenthesized_pattern_with_bareword_esac_in_body() {
5227 let result = parse("X=$(case a in (a) y=esac;; b) echo two;; esac)").unwrap();
5238 let stmts = match &result.statements[0] {
5239 Stmt::Assignment(a) => match &a.value {
5240 Expr::CommandSubst(s) => s,
5241 other => panic!("expected command subst, got {:?}", other),
5242 },
5243 other => panic!("expected assignment, got {:?}", other),
5244 };
5245 match stmts.as_slice() {
5246 [Stmt::Case(c)] => {
5247 assert_eq!(c.branches.len(), 2);
5248 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5249 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
5250 }
5251 other => panic!("expected a single Case statement with two branches, got {:?}", other),
5252 }
5253 }
5254
5255 #[test]
5256 fn parse_cmd_subst_unquoted_nested_case_parenthesized_pattern_esac_in_outer_body() {
5257 let result = parse(
5268 "X=$(case a in (a) case b in (b) echo z;; esac; y=esac;; c) echo two;; esac)",
5269 )
5270 .unwrap();
5271 let stmts = match &result.statements[0] {
5272 Stmt::Assignment(a) => match &a.value {
5273 Expr::CommandSubst(s) => s,
5274 other => panic!("expected command subst, got {:?}", other),
5275 },
5276 other => panic!("expected assignment, got {:?}", other),
5277 };
5278 match stmts.as_slice() {
5279 [Stmt::Case(c)] => {
5280 assert_eq!(c.branches.len(), 2);
5281 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5282 assert_eq!(c.branches[1].patterns, vec!["c".to_string()]);
5283 }
5284 other => panic!("expected a single Case statement with two branches, got {:?}", other),
5285 }
5286 }
5287
5288 #[test]
5289 fn parse_cmd_subst_unquoted_case_eq_argv_key() {
5290 let result = parse("X=$(echo case=x)").unwrap();
5299 let stmts = match &result.statements[0] {
5300 Stmt::Assignment(a) => match &a.value {
5301 Expr::CommandSubst(s) => s,
5302 other => panic!("expected command subst, got {:?}", other),
5303 },
5304 other => panic!("expected assignment, got {:?}", other),
5305 };
5306 let cmd = match stmts.as_slice() {
5307 [Stmt::Command(c)] => c,
5308 other => panic!("expected a single echo command, got {:?}", other),
5309 };
5310 assert_eq!(cmd.name, "echo");
5311 match &cmd.args[0] {
5312 Arg::WordAssign { key, value } => {
5313 assert_eq!(key, "case");
5314 match value {
5315 Expr::Literal(Value::String(s)) => assert_eq!(s, "x"),
5316 other => panic!("expected string \"x\", got {:?}", other),
5317 }
5318 }
5319 other => panic!("expected WordAssign arg, got {:?}", other),
5320 }
5321 }
5322
5323 #[test]
5324 fn parse_cmd_subst_unquoted_case_eq_argv_key_with_sibling_keyword_keys() {
5325 let result = parse("X=$(tool case=x do=y)").unwrap();
5329 let stmts = match &result.statements[0] {
5330 Stmt::Assignment(a) => match &a.value {
5331 Expr::CommandSubst(s) => s,
5332 other => panic!("expected command subst, got {:?}", other),
5333 },
5334 other => panic!("expected assignment, got {:?}", other),
5335 };
5336 let cmd = match stmts.as_slice() {
5337 [Stmt::Command(c)] => c,
5338 other => panic!("expected a single tool command, got {:?}", other),
5339 };
5340 assert_eq!(cmd.name, "tool");
5341 assert_eq!(cmd.args.len(), 2);
5342 assert!(matches!(&cmd.args[0], Arg::WordAssign { key, .. } if key == "case"));
5343 assert!(matches!(&cmd.args[1], Arg::WordAssign { key, .. } if key == "do"));
5344 }
5345
5346 #[test]
5347 fn parse_cmd_subst_unquoted_case_inside_nested_subst() {
5348 let result = parse("X=$(echo $(case b in b) echo x;; esac))").unwrap();
5355 let outer_stmts = match &result.statements[0] {
5356 Stmt::Assignment(a) => match &a.value {
5357 Expr::CommandSubst(s) => s,
5358 other => panic!("expected command subst, got {:?}", other),
5359 },
5360 other => panic!("expected assignment, got {:?}", other),
5361 };
5362 let outer_cmd = match outer_stmts.as_slice() {
5363 [Stmt::Command(c)] => c,
5364 other => panic!("expected a single echo command, got {:?}", other),
5365 };
5366 assert_eq!(outer_cmd.name, "echo");
5367 let inner_stmts = match &outer_cmd.args[0] {
5368 Arg::Positional(Expr::CommandSubst(s)) => s,
5369 other => panic!("expected nested command subst arg, got {:?}", other),
5370 };
5371 assert!(
5372 matches!(inner_stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
5373 "expected a single Case statement inside the inner $(), got {inner_stmts:?}"
5374 );
5375 }
5376
5377 #[test]
5378 fn parse_cmd_subst_unquoted_esac_as_bareword() {
5379 let result = parse("X=$(echo esac)").unwrap();
5384 let stmts = match &result.statements[0] {
5385 Stmt::Assignment(a) => match &a.value {
5386 Expr::CommandSubst(s) => s,
5387 other => panic!("expected command subst, got {:?}", other),
5388 },
5389 other => panic!("expected assignment, got {:?}", other),
5390 };
5391 let cmd = match stmts.as_slice() {
5392 [Stmt::Command(c)] => c,
5393 other => panic!("expected a single echo command, got {:?}", other),
5394 };
5395 assert_eq!(cmd.name, "echo");
5396 assert!(
5397 matches!(&cmd.args[0], Arg::Positional(Expr::Literal(Value::String(s))) if s == "esac"),
5398 "expected \"esac\" as a literal argument, got {:?}",
5399 cmd.args[0]
5400 );
5401 }
5402
5403 #[test]
5404 fn parse_cmd_subst_unquoted_esac_as_bareword_inside_still_open_case() {
5405 let result = parse("X=$(case a in a) y=esac;; b) echo two;; esac)").unwrap();
5414 let stmts = match &result.statements[0] {
5415 Stmt::Assignment(a) => match &a.value {
5416 Expr::CommandSubst(s) => s,
5417 other => panic!("expected command subst, got {:?}", other),
5418 },
5419 other => panic!("expected assignment, got {:?}", other),
5420 };
5421 match stmts.as_slice() {
5422 [Stmt::Case(c)] => {
5423 assert_eq!(c.branches.len(), 2);
5424 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5425 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
5426 }
5427 other => panic!("expected a single Case statement with two branches, got {:?}", other),
5428 }
5429 }
5430
5431 #[test]
5432 fn parse_quoted_cmd_subst_case_pattern_paren_not_miscounted() {
5433 let result = parse(r#"X="pre $(case v in v) echo x;; esac) post""#).unwrap();
5442 let parts = match &result.statements[0] {
5443 Stmt::Assignment(a) => match &a.value {
5444 Expr::Interpolated(parts) => parts,
5445 other => panic!("expected an interpolated string, got {:?}", other),
5446 },
5447 other => panic!("expected assignment, got {:?}", other),
5448 };
5449 let stmts = match parts.as_slice() {
5450 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5451 {
5452 assert_eq!(pre, "pre ");
5453 assert_eq!(post, " post");
5454 stmts
5455 }
5456 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5457 };
5458 assert!(
5459 matches!(stmts.as_slice(), [Stmt::Case(c)] if c.branches.len() == 1),
5460 "expected a single Case statement inside the quoted $(...), got {stmts:?}"
5461 );
5462 }
5463
5464 #[test]
5465 fn parse_quoted_cmd_subst_case_parenthesized_pattern_esac_in_body_not_miscounted() {
5466 let result = parse(r#"X="pre $(case a in (a) y=esac;; b) echo two;; esac) post""#).unwrap();
5470 let parts = match &result.statements[0] {
5471 Stmt::Assignment(a) => match &a.value {
5472 Expr::Interpolated(parts) => parts,
5473 other => panic!("expected an interpolated string, got {:?}", other),
5474 },
5475 other => panic!("expected assignment, got {:?}", other),
5476 };
5477 let stmts = match parts.as_slice() {
5478 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5479 {
5480 assert_eq!(pre, "pre ");
5481 assert_eq!(post, " post");
5482 stmts
5483 }
5484 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5485 };
5486 match stmts.as_slice() {
5487 [Stmt::Case(c)] => {
5488 assert_eq!(c.branches.len(), 2);
5489 assert_eq!(c.branches[0].patterns, vec!["a".to_string()]);
5490 assert_eq!(c.branches[1].patterns, vec!["b".to_string()]);
5491 }
5492 other => panic!("expected a single Case statement with two branches, got {:?}", other),
5493 }
5494 }
5495
5496 #[test]
5497 fn parse_quoted_cmd_subst_case_eq_argv_key_not_miscounted() {
5498 let result = parse(r#"X="pre $(echo case=x) post""#).unwrap();
5501 let parts = match &result.statements[0] {
5502 Stmt::Assignment(a) => match &a.value {
5503 Expr::Interpolated(parts) => parts,
5504 other => panic!("expected an interpolated string, got {:?}", other),
5505 },
5506 other => panic!("expected assignment, got {:?}", other),
5507 };
5508 let stmts = match parts.as_slice() {
5509 [StringPart::Literal(pre), StringPart::CommandSubst(stmts), StringPart::Literal(post)] =>
5510 {
5511 assert_eq!(pre, "pre ");
5512 assert_eq!(post, " post");
5513 stmts
5514 }
5515 other => panic!("expected [literal, command subst, literal], got {:?}", other),
5516 };
5517 let cmd = match stmts.as_slice() {
5518 [Stmt::Command(c)] => c,
5519 other => panic!("expected a single echo command, got {:?}", other),
5520 };
5521 assert_eq!(cmd.name, "echo");
5522 assert!(matches!(&cmd.args[0], Arg::WordAssign { key, .. } if key == "case"));
5523 }
5524
5525 #[test]
5526 fn parse_cmd_subst_unquoted_nested_with_control_structure() {
5527 let result = parse("X=$(echo $(for f in a; do echo $f; done))").unwrap();
5530 let outer_stmts = match &result.statements[0] {
5531 Stmt::Assignment(a) => match &a.value {
5532 Expr::CommandSubst(s) => s,
5533 other => panic!("expected command subst, got {:?}", other),
5534 },
5535 other => panic!("expected assignment, got {:?}", other),
5536 };
5537 let outer_cmd = match outer_stmts.as_slice() {
5538 [Stmt::Command(c)] => c,
5539 other => panic!("expected a single echo command, got {:?}", other),
5540 };
5541 assert_eq!(outer_cmd.name, "echo");
5542 let inner_stmts = match &outer_cmd.args[0] {
5543 Arg::Positional(Expr::CommandSubst(s)) => s,
5544 other => panic!("expected nested command subst arg, got {:?}", other),
5545 };
5546 assert!(
5547 matches!(inner_stmts.as_slice(), [Stmt::For(f)] if f.variable == "f"),
5548 "expected a single For statement inside the inner $(), got {inner_stmts:?}"
5549 );
5550 }
5551
5552 #[test]
5553 fn parse_cmd_subst_unquoted_pipeline() {
5554 let result = parse("X=$(cat f | grep pat | wc -l)").unwrap();
5559 let value = match &result.statements[0] {
5560 Stmt::Assignment(a) => a.value.clone(),
5561 other => panic!("expected assignment, got {:?}", other),
5562 };
5563 let pipeline = subst_pipeline(&value);
5564 assert_eq!(pipeline_commands(pipeline).len(), 3);
5565 assert_eq!(pipeline_commands(pipeline)[2].name, "wc");
5566 }
5567
5568 #[test]
5569 fn parse_quoted_cmd_subst_with_for_loop_still_works() {
5570 let result = parse(r#"out="$(for f in a b; do echo $f; done)""#).unwrap();
5574 match &result.statements[0] {
5575 Stmt::Assignment(a) => assert_eq!(a.name(), "out"),
5576 other => panic!("expected assignment, got {:?}", other),
5577 }
5578 }
5579
5580 #[test]
5581 fn parse_cmd_subst_body_error_reports_span_inside_body_not_at_dollar_paren() {
5582 let source = "echo $(for f in a; do echo $f done)";
5590 let errs = parse(source).expect_err("missing loop terminator must be a parse error");
5591 let dollar_paren = source.find("$(").expect("fixture contains $(");
5592 assert!(
5593 errs.iter().all(|e| e.span.start > dollar_paren + 1),
5594 "error span must point inside the $() body, not at '$(' itself: {errs:?}"
5595 );
5596 assert!(
5599 errs.iter().any(|e| e.message.contains("done")),
5600 "expected the missing-`done` diagnostic, got: {errs:?}"
5601 );
5602 }
5603
5604 #[test]
5605 fn parse_cmd_subst_unterminated_reports_error() {
5606 let result = parse("echo $(for f in a; do echo $f; done");
5607 assert!(result.is_err(), "a missing `)` must be a parse error");
5608 }
5609
5610 #[test]
5615 fn parse_env_prefix_single() {
5616 let result = parse("FOO=bar echo hi").unwrap();
5617 match &result.statements[0] {
5618 Stmt::EnvScoped { assignments, body } => {
5619 assert_eq!(assignments.len(), 1);
5620 assert_eq!(assignments[0].name(), "FOO");
5621 assert!(!assignments[0].local);
5622 match body.as_ref() {
5623 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
5624 other => panic!("expected command body, got {other:?}"),
5625 }
5626 }
5627 other => panic!("expected env-scoped, got {other:?}"),
5628 }
5629 }
5630
5631 #[test]
5632 fn parse_env_prefix_multiple() {
5633 let result = parse("A=1 B=2 run").unwrap();
5634 match &result.statements[0] {
5635 Stmt::EnvScoped { assignments, body } => {
5636 assert_eq!(assignments.len(), 2);
5637 assert_eq!(assignments[0].name(), "A");
5638 assert_eq!(assignments[1].name(), "B");
5639 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
5640 }
5641 other => panic!("expected env-scoped, got {other:?}"),
5642 }
5643 }
5644
5645 #[test]
5646 fn parse_bare_assignment_is_not_env_scoped() {
5647 let result = parse("FOO=bar").unwrap();
5649 assert!(
5650 matches!(&result.statements[0], Stmt::Assignment(a) if a.name() == "FOO"),
5651 "got {:?}",
5652 result.statements[0]
5653 );
5654 }
5655
5656 #[test]
5657 fn parse_assignment_then_and_chain_does_not_over_capture() {
5658 let result = parse("FOO=bar && echo hi").unwrap();
5661 match &result.statements[0] {
5662 Stmt::AndChain { left, right } => {
5663 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name() == "FOO"));
5664 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
5665 }
5666 other => panic!("expected and-chain, got {other:?}"),
5667 }
5668 }
5669
5670 #[test]
5671 fn parse_env_prefix_pipeline_body() {
5672 let result = parse("FOO=bar cat | grep x").unwrap();
5673 match &result.statements[0] {
5674 Stmt::EnvScoped { assignments, body } => {
5675 assert_eq!(assignments[0].name(), "FOO");
5676 match body.as_ref() {
5677 Stmt::Pipeline(p) => assert_eq!(pipeline_commands(p).len(), 2),
5678 other => panic!("expected pipeline body, got {other:?}"),
5679 }
5680 }
5681 other => panic!("expected env-scoped, got {other:?}"),
5682 }
5683 }
5684
5685 fn parse_err_message(source: &str) -> String {
5690 parse(source)
5691 .expect_err("expected a parse error")
5692 .iter()
5693 .map(|e| e.message.clone())
5694 .collect::<Vec<_>>()
5695 .join(" ")
5696 }
5697
5698 #[test]
5699 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
5700 let msg = parse_err_message("echo /tmp/$(echo x).txt");
5703 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
5704 }
5705
5706 #[test]
5707 fn argv_splat_var_glued_to_path_is_rejected() {
5708 assert!(parse("echo $dir/out.txt").is_err());
5709 }
5710
5711 #[test]
5712 fn argv_splat_three_way_glue_is_rejected() {
5713 assert!(parse("echo foo$(echo bar)baz").is_err());
5714 }
5715
5716 #[test]
5717 fn argv_splat_quoted_word_is_accepted() {
5718 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
5720 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
5721 }
5722
5723 #[test]
5724 fn argv_single_token_words_are_not_splat() {
5725 assert!(parse("echo file.txt").is_ok(), "file.txt");
5727 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
5728 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
5729 }
5730
5731 #[test]
5732 fn argv_spaced_words_are_not_splat() {
5733 assert!(parse("echo a b c").is_ok());
5734 assert!(parse("echo /tmp/x $(echo y)").is_ok());
5735 }
5736
5737 #[test]
5738 fn parse_cmd_subst_in_command_arg() {
5739 let result = parse("echo $(whoami)").unwrap();
5740 match &result.statements[0] {
5741 Stmt::Command(cmd) => {
5742 assert_eq!(cmd.name, "echo");
5743 match &cmd.args[0] {
5744 Arg::Positional(expr) => {
5745 assert_eq!(subst_cmd(expr).name, "whoami");
5746 }
5747 other => panic!("expected command subst, got {:?}", other),
5748 }
5749 }
5750 other => panic!("expected command, got {:?}", other),
5751 }
5752 }
5753
5754 #[test]
5759 fn parse_condition_and() {
5760 let result = parse("if check-a && check-b; then echo; fi").unwrap();
5762 match &result.statements[0] {
5763 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5764 Expr::BinaryOp { left, op, right } => {
5765 assert_eq!(*op, BinaryOp::And);
5766 assert!(matches!(left.as_ref(), Expr::Command(_)));
5767 assert!(matches!(right.as_ref(), Expr::Command(_)));
5768 }
5769 other => panic!("expected binary op, got {:?}", other),
5770 },
5771 other => panic!("expected if, got {:?}", other),
5772 }
5773 }
5774
5775 #[test]
5776 fn parse_condition_or() {
5777 let result = parse("if try-a || try-b; then echo; fi").unwrap();
5778 match &result.statements[0] {
5779 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5780 Expr::BinaryOp { left, op, right } => {
5781 assert_eq!(*op, BinaryOp::Or);
5782 assert!(matches!(left.as_ref(), Expr::Command(_)));
5783 assert!(matches!(right.as_ref(), Expr::Command(_)));
5784 }
5785 other => panic!("expected binary op, got {:?}", other),
5786 },
5787 other => panic!("expected if, got {:?}", other),
5788 }
5789 }
5790
5791 #[test]
5792 fn parse_condition_and_or_precedence() {
5793 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
5795 match &result.statements[0] {
5796 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5797 Expr::BinaryOp { left, op, right } => {
5798 assert_eq!(*op, BinaryOp::Or);
5800 match left.as_ref() {
5802 Expr::BinaryOp { op: inner_op, .. } => {
5803 assert_eq!(*inner_op, BinaryOp::And);
5804 }
5805 other => panic!("expected binary op (&&), got {:?}", other),
5806 }
5807 assert!(matches!(right.as_ref(), Expr::Command(_)));
5809 }
5810 other => panic!("expected binary op, got {:?}", other),
5811 },
5812 other => panic!("expected if, got {:?}", other),
5813 }
5814 }
5815
5816 #[test]
5817 fn parse_condition_multiple_and() {
5818 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
5819 match &result.statements[0] {
5820 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5821 Expr::BinaryOp { left, op, .. } => {
5822 assert_eq!(*op, BinaryOp::And);
5823 match left.as_ref() {
5825 Expr::BinaryOp { op: inner_op, .. } => {
5826 assert_eq!(*inner_op, BinaryOp::And);
5827 }
5828 other => panic!("expected binary op, got {:?}", other),
5829 }
5830 }
5831 other => panic!("expected binary op, got {:?}", other),
5832 },
5833 other => panic!("expected if, got {:?}", other),
5834 }
5835 }
5836
5837 #[test]
5838 fn parse_condition_mixed_comparison_and_logical() {
5839 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
5841 match &result.statements[0] {
5842 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
5843 Expr::BinaryOp { left, op, right } => {
5844 assert_eq!(*op, BinaryOp::And);
5845 match left.as_ref() {
5847 Expr::Test(test) => match test.as_ref() {
5848 TestExpr::Comparison { op: left_op, .. } => {
5849 assert_eq!(*left_op, TestCmpOp::Eq);
5850 }
5851 other => panic!("expected comparison, got {:?}", other),
5852 },
5853 other => panic!("expected test, got {:?}", other),
5854 }
5855 match right.as_ref() {
5857 Expr::Test(test) => match test.as_ref() {
5858 TestExpr::Comparison { op: right_op, .. } => {
5859 assert_eq!(*right_op, TestCmpOp::NumGt);
5860 }
5861 other => panic!("expected comparison, got {:?}", other),
5862 },
5863 other => panic!("expected test, got {:?}", other),
5864 }
5865 }
5866 other => panic!("expected binary op, got {:?}", other),
5867 },
5868 other => panic!("expected if, got {:?}", other),
5869 }
5870 }
5871
5872 #[test]
5878 fn script_level1_linear() {
5879 let script = r#"
5880NAME="kaish"
5881VERSION=1
5882TIMEOUT=30
5883ITEMS="alpha beta gamma"
5884
5885echo "Starting ${NAME} v${VERSION}"
5886cat "README.md" | grep pattern="install" | head count=5
5887fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
5888echo "Items: ${ITEMS}"
5889"#;
5890 let result = parse(script).unwrap();
5891 let stmts: Vec<_> = result.statements.iter()
5892 .filter(|s| !matches!(s, Stmt::Empty))
5893 .collect();
5894
5895 assert_eq!(stmts.len(), 8);
5896 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(_))); }
5905
5906 #[test]
5908 fn script_level2_branching() {
5909 let script = r#"
5910RESULT=$(kaish-validate "input.json")
5911
5912if [[ ${RESULT.ok} == true ]]; then
5913 echo "Validation passed"
5914 process "input.json" > "output.json"
5915else
5916 echo "Validation failed: ${RESULT.err}"
5917fi
5918
5919if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
5920 echo "Count in valid range"
5921fi
5922
5923if check-network || check-cache; then
5924 fetch url=${URL}
5925fi
5926"#;
5927 let result = parse(script).unwrap();
5928 let stmts: Vec<_> = result.statements.iter()
5929 .filter(|s| !matches!(s, Stmt::Empty))
5930 .collect();
5931
5932 assert_eq!(stmts.len(), 4);
5933
5934 match stmts[0] {
5936 Stmt::Assignment(a) => {
5937 assert_eq!(a.name(), "RESULT");
5938 assert!(matches!(&a.value, Expr::CommandSubst(_)));
5939 }
5940 other => panic!("expected assignment, got {:?}", other),
5941 }
5942
5943 match stmts[1] {
5945 Stmt::If(if_stmt) => {
5946 assert_eq!(if_stmt.then_branch.len(), 2);
5947 assert!(if_stmt.else_branch.is_some());
5948 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
5949 }
5950 other => panic!("expected if, got {:?}", other),
5951 }
5952
5953 match stmts[2] {
5955 Stmt::If(if_stmt) => {
5956 match if_stmt.condition.as_ref() {
5957 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
5958 other => panic!("expected && condition, got {:?}", other),
5959 }
5960 }
5961 other => panic!("expected if, got {:?}", other),
5962 }
5963
5964 match stmts[3] {
5966 Stmt::If(if_stmt) => {
5967 match if_stmt.condition.as_ref() {
5968 Expr::BinaryOp { op, left, right } => {
5969 assert_eq!(*op, BinaryOp::Or);
5970 assert!(matches!(left.as_ref(), Expr::Command(_)));
5971 assert!(matches!(right.as_ref(), Expr::Command(_)));
5972 }
5973 other => panic!("expected || condition, got {:?}", other),
5974 }
5975 }
5976 other => panic!("expected if, got {:?}", other),
5977 }
5978 }
5979
5980 #[test]
5982 fn script_level3_loops_and_functions() {
5983 let script = r#"
5984greet() {
5985 echo "Hello, $1!"
5986}
5987
5988fetch_all() {
5989 for URL in $@; do
5990 fetch url=${URL}
5991 done
5992}
5993
5994USERS="alice bob charlie"
5995
5996for USER in ${USERS}; do
5997 greet ${USER}
5998 if [[ ${USER} == "bob" ]]; then
5999 echo "Found Bob!"
6000 fi
6001done
6002
6003long-running-task &
6004"#;
6005 let result = parse(script).unwrap();
6006 let stmts: Vec<_> = result.statements.iter()
6007 .filter(|s| !matches!(s, Stmt::Empty))
6008 .collect();
6009
6010 assert_eq!(stmts.len(), 5);
6011
6012 match stmts[0] {
6014 Stmt::ToolDef(t) => {
6015 assert_eq!(t.name, "greet");
6016 assert!(t.params.is_empty());
6017 }
6018 other => panic!("expected function def, got {:?}", other),
6019 }
6020
6021 match stmts[1] {
6023 Stmt::ToolDef(t) => {
6024 assert_eq!(t.name, "fetch_all");
6025 assert_eq!(t.body.len(), 1);
6026 assert!(matches!(&t.body[0], Stmt::For(_)));
6027 }
6028 other => panic!("expected function def, got {:?}", other),
6029 }
6030
6031 assert!(matches!(stmts[2], Stmt::Assignment(_)));
6033
6034 match stmts[3] {
6036 Stmt::For(f) => {
6037 assert_eq!(f.variable, "USER");
6038 assert_eq!(f.body.len(), 2);
6039 assert!(matches!(&f.body[0], Stmt::Command(_)));
6040 assert!(matches!(&f.body[1], Stmt::If(_)));
6041 }
6042 other => panic!("expected for loop, got {:?}", other),
6043 }
6044
6045 match stmts[4] {
6047 Stmt::Pipeline(p) => {
6048 assert!(p.background);
6049 assert_eq!(pipeline_commands(p)[0].name, "long-running-task");
6050 }
6051 other => panic!("expected pipeline (background), got {:?}", other),
6052 }
6053 }
6054
6055 #[test]
6057 fn script_level4_complex_nesting() {
6058 let script = r#"
6059RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
6060
6061if ping host=${HOST} && [[ ${RESULT} == true ]]; then
6062 for SERVER in "prod-1 prod-2"; do
6063 deploy target=${SERVER} port=8080
6064 if [[ $? -ne 0 ]]; then
6065 notify channel="ops" message="Deploy failed"
6066 fi
6067 done
6068fi
6069"#;
6070 let result = parse(script).unwrap();
6071 let stmts: Vec<_> = result.statements.iter()
6072 .filter(|s| !matches!(s, Stmt::Empty))
6073 .collect();
6074
6075 assert_eq!(stmts.len(), 2);
6076
6077 match stmts[0] {
6079 Stmt::Assignment(a) => {
6080 assert_eq!(a.name(), "RESULT");
6081 assert_eq!(pipeline_commands(subst_pipeline(&a.value)).len(), 3);
6082 }
6083 other => panic!("expected assignment, got {:?}", other),
6084 }
6085
6086 match stmts[1] {
6088 Stmt::If(if_stmt) => {
6089 match if_stmt.condition.as_ref() {
6090 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
6091 other => panic!("expected && condition, got {:?}", other),
6092 }
6093 assert_eq!(if_stmt.then_branch.len(), 1);
6094 match &if_stmt.then_branch[0] {
6095 Stmt::For(f) => {
6096 assert_eq!(f.body.len(), 2);
6097 assert!(matches!(&f.body[1], Stmt::If(_)));
6098 }
6099 other => panic!("expected for in if body, got {:?}", other),
6100 }
6101 }
6102 other => panic!("expected if, got {:?}", other),
6103 }
6104 }
6105
6106 #[test]
6108 fn script_level5_edge_cases() {
6109 let script = r#"
6110echo ""
6111echo "quotes: \"nested\" here"
6112echo "escapes: \n\t\r\\"
6113echo "unicode: \u2764"
6114
6115X=-99999
6116Y=3.14159265358979
6117Z=-0.001
6118
6119cmd a=1 b="two" c=true d=false e=null
6120
6121if true; then
6122 if false; then
6123 echo "inner"
6124 else
6125 echo "else"
6126 fi
6127fi
6128
6129for I in "a b c"; do
6130 echo ${I}
6131done
6132
6133no_params() {
6134 echo "no params"
6135}
6136
6137function all_args {
6138 echo "args: $@"
6139}
6140
6141a | b | c | d | e &
6142cmd 2> "errors.log"
6143cmd &> "all.log"
6144cmd >> "append.log"
6145cmd < "input.txt"
6146"#;
6147 let result = parse(script).unwrap();
6148 let stmts: Vec<_> = result.statements.iter()
6149 .filter(|s| !matches!(s, Stmt::Empty))
6150 .collect();
6151
6152 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
6154
6155 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
6157 assert!(bg_stmt.is_some(), "expected background pipeline");
6158
6159 match bg_stmt.unwrap() {
6160 Stmt::Pipeline(p) => {
6161 assert_eq!(pipeline_commands(p).len(), 5);
6162 assert!(p.background);
6163 }
6164 _ => unreachable!(),
6165 }
6166 }
6167
6168 #[test]
6173 fn parse_keyword_as_variable_rejected() {
6174 let result = parse(r#"if="value""#);
6177 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
6178
6179 let result = parse("while=true");
6180 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
6181
6182 let result = parse(r#"then="next""#);
6183 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
6184 }
6185
6186 #[test]
6187 fn parse_set_command_with_flag() {
6188 let result = parse("set -e");
6189 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
6190 let program = result.unwrap();
6191 match &program.statements[0] {
6192 Stmt::Command(cmd) => {
6193 assert_eq!(cmd.name, "set");
6194 assert_eq!(cmd.args.len(), 1);
6195 match &cmd.args[0] {
6196 Arg::ShortFlag(f) => assert_eq!(f, "e"),
6197 other => panic!("expected ShortFlag, got {:?}", other),
6198 }
6199 }
6200 other => panic!("expected Command, got {:?}", other),
6201 }
6202 }
6203
6204 #[test]
6205 fn parse_set_command_no_args() {
6206 let result = parse("set");
6207 assert!(result.is_ok(), "failed to parse set: {:?}", result);
6208 let program = result.unwrap();
6209 match &program.statements[0] {
6210 Stmt::Command(cmd) => {
6211 assert_eq!(cmd.name, "set");
6212 assert_eq!(cmd.args.len(), 0);
6213 }
6214 other => panic!("expected Command, got {:?}", other),
6215 }
6216 }
6217
6218 #[test]
6219 fn parse_set_assignment_vs_command() {
6220 let result = parse("X=5");
6222 assert!(result.is_ok());
6223 let program = result.unwrap();
6224 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
6225
6226 let result = parse("set -e");
6228 assert!(result.is_ok());
6229 let program = result.unwrap();
6230 assert!(matches!(&program.statements[0], Stmt::Command(_)));
6231 }
6232
6233 #[test]
6234 fn parse_true_as_command() {
6235 let result = parse("true");
6236 assert!(result.is_ok());
6237 let program = result.unwrap();
6238 match &program.statements[0] {
6239 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
6240 other => panic!("expected Command(true), got {:?}", other),
6241 }
6242 }
6243
6244 #[test]
6245 fn parse_false_as_command() {
6246 let result = parse("false");
6247 assert!(result.is_ok());
6248 let program = result.unwrap();
6249 match &program.statements[0] {
6250 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
6251 other => panic!("expected Command(false), got {:?}", other),
6252 }
6253 }
6254
6255 #[test]
6256 fn parse_dot_as_source_alias() {
6257 let result = parse(". script.kai");
6258 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
6259 let program = result.unwrap();
6260 match &program.statements[0] {
6261 Stmt::Command(cmd) => {
6262 assert_eq!(cmd.name, ".");
6263 assert_eq!(cmd.args.len(), 1);
6264 }
6265 other => panic!("expected Command(.), got {:?}", other),
6266 }
6267 }
6268
6269 #[test]
6270 fn parse_source_command() {
6271 let result = parse("source utils.kai");
6272 assert!(result.is_ok(), "failed to parse source: {:?}", result);
6273 let program = result.unwrap();
6274 match &program.statements[0] {
6275 Stmt::Command(cmd) => {
6276 assert_eq!(cmd.name, "source");
6277 assert_eq!(cmd.args.len(), 1);
6278 }
6279 other => panic!("expected Command(source), got {:?}", other),
6280 }
6281 }
6282
6283 #[test]
6284 fn parse_test_expr_file_test() {
6285 let result = parse(r#"[[ -f "/path/file" ]]"#);
6287 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
6288 }
6289
6290 #[test]
6291 fn parse_test_expr_comparison() {
6292 let result = parse(r#"[[ $X == "value" ]]"#);
6293 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
6294 }
6295
6296 #[test]
6297 fn parse_test_expr_single_eq() {
6298 let result = parse(r#"[[ $X = "value" ]]"#);
6300 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
6301 let program = result.unwrap();
6302 match &program.statements[0] {
6303 Stmt::Test(TestExpr::Comparison { op, .. }) => {
6304 assert_eq!(op, &TestCmpOp::Eq);
6305 }
6306 other => panic!("expected Test(Comparison), got {:?}", other),
6307 }
6308 }
6309
6310 #[test]
6311 fn parse_while_loop() {
6312 let result = parse("while true; do echo; done");
6313 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
6314 let program = result.unwrap();
6315 assert!(matches!(&program.statements[0], Stmt::While(_)));
6316 }
6317
6318 #[test]
6319 fn parse_break_with_level() {
6320 let result = parse("break 2");
6321 assert!(result.is_ok());
6322 let program = result.unwrap();
6323 match &program.statements[0] {
6324 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
6325 other => panic!("expected Break(2), got {:?}", other),
6326 }
6327 }
6328
6329 #[test]
6330 fn parse_continue_with_level() {
6331 let result = parse("continue 3");
6332 assert!(result.is_ok());
6333 let program = result.unwrap();
6334 match &program.statements[0] {
6335 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
6336 other => panic!("expected Continue(3), got {:?}", other),
6337 }
6338 }
6339
6340 #[test]
6341 fn parse_exit_with_code() {
6342 let result = parse("exit 1");
6343 assert!(result.is_ok());
6344 let program = result.unwrap();
6345 match &program.statements[0] {
6346 Stmt::Exit(Some(expr)) => {
6347 match expr.as_ref() {
6348 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
6349 other => panic!("expected Int(1), got {:?}", other),
6350 }
6351 }
6352 other => panic!("expected Exit(1), got {:?}", other),
6353 }
6354 }
6355
6356 #[test]
6363 fn spanned_literal_only_records_byte_range() {
6364 let parts = parse_interpolated_string_spanned("hello world", 100).unwrap();
6365 assert_eq!(parts.len(), 1);
6366 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
6367 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
6368 assert_eq!(parts[0].len, 11);
6369 }
6370
6371 #[test]
6372 fn spanned_braced_var_at_zero() {
6373 let parts = parse_interpolated_string_spanned("${X}", 50).unwrap();
6374 assert_eq!(parts.len(), 1);
6375 assert!(matches!(&parts[0].part, StringPart::Var(_)));
6376 assert_eq!(parts[0].offset, 50);
6377 assert_eq!(parts[0].len, 4); }
6379
6380 #[test]
6381 fn spanned_simple_var_then_literal() {
6382 let parts = parse_interpolated_string_spanned("$X end", 10).unwrap();
6383 assert_eq!(parts.len(), 2);
6384 assert!(matches!(&parts[0].part, StringPart::Var(_)));
6385 assert_eq!(parts[0].offset, 10);
6386 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
6388 assert_eq!(parts[1].offset, 12);
6389 assert_eq!(parts[1].len, 4);
6390 }
6391
6392 #[test]
6393 fn spanned_mixed_literal_var_literal() {
6394 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0).unwrap();
6395 assert_eq!(parts.len(), 3);
6396 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
6398 assert_eq!(parts[0].offset, 0);
6399 assert_eq!(parts[0].len, 3);
6400 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6402 assert_eq!(parts[1].offset, 3);
6403 assert_eq!(parts[1].len, 4);
6404 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
6406 assert_eq!(parts[2].offset, 7);
6407 assert_eq!(parts[2].len, 4);
6408 }
6409
6410 #[test]
6411 fn spanned_positional_param() {
6412 let parts = parse_interpolated_string_spanned("$1 done", 0).unwrap();
6413 assert_eq!(parts.len(), 2);
6414 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
6415 assert_eq!(parts[0].offset, 0);
6416 assert_eq!(parts[0].len, 2); }
6418
6419 #[test]
6420 fn spanned_special_dollar_dollar() {
6421 let parts = parse_interpolated_string_spanned("$$", 5).unwrap();
6422 assert_eq!(parts.len(), 1);
6423 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
6424 assert_eq!(parts[0].offset, 5);
6425 assert_eq!(parts[0].len, 2);
6426 }
6427
6428 #[test]
6429 fn spanned_arithmetic_marker_recognised() {
6430 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0).unwrap();
6434 assert_eq!(parts.len(), 1);
6435 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
6436 }
6437
6438 #[test]
6439 fn spanned_default_separator_yields_var_with_default() {
6440 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0).unwrap();
6441 assert_eq!(parts.len(), 1);
6442 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
6443 assert_eq!(parts[0].offset, 0);
6444 assert_eq!(parts[0].len, 14); }
6446
6447 #[test]
6448 fn spanned_no_dollar_runs_one_literal() {
6449 let parts = parse_interpolated_string_spanned("plain text only", 7).unwrap();
6450 assert_eq!(parts.len(), 1);
6451 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
6452 assert_eq!(parts[0].offset, 7);
6453 assert_eq!(parts[0].len, 15);
6454 }
6455
6456 #[test]
6457 fn spanned_matches_unspanned_part_count() {
6458 let cases = [
6461 "hello",
6462 "$X",
6463 "${X}",
6464 "${X:-d}",
6465 "hi $A and $B",
6466 "$0 $1 $2",
6467 "$$ $? $#",
6468 ];
6469 for s in &cases {
6470 let unspanned = parse_interpolated_string(s).expect("test input parses");
6471 let spanned = parse_interpolated_string_spanned(s, 0).unwrap();
6472 assert_eq!(
6473 unspanned.len(),
6474 spanned.len(),
6475 "part count differs for {:?}",
6476 s
6477 );
6478 }
6479 }
6480
6481 #[test]
6482 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
6483 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0).unwrap();
6488 assert_eq!(parts.len(), 2);
6489
6490 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
6491 assert_eq!(parts[0].offset, 0);
6492 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
6493
6494 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6495 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
6496 assert_eq!(parts[1].len, 4);
6497 }
6498
6499 #[test]
6500 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
6501 let parts = parse_interpolated_string_spanned("hello 世界 world", 0).unwrap();
6504 assert_eq!(parts.len(), 1);
6505 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
6506 assert_eq!(parts[0].offset, 0);
6507 assert_eq!(parts[0].len, 18);
6508 }
6509
6510 #[test]
6511 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
6512 let parts = parse_interpolated_string_spanned("\\$", 0).unwrap();
6515 assert_eq!(parts.len(), 1);
6516 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
6517 assert_eq!(parts[0].offset, 0);
6518 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
6519 }
6520
6521 #[test]
6522 fn spanned_escape_backslash_collapses_pair_to_one() {
6523 let parts = parse_interpolated_string_spanned("\\\\", 0).unwrap();
6524 assert_eq!(parts.len(), 1);
6525 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
6526 assert_eq!(parts[0].len, 2);
6527 }
6528
6529 #[test]
6530 fn spanned_standalone_cr_continuation_realigns_span_start() {
6531 let parts = parse_interpolated_string_spanned("\\\rCD${x}", 0).unwrap();
6539 assert_eq!(parts.len(), 2);
6540 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "CD"));
6541 assert_eq!(parts[0].offset, 2, "literal run must start after the consumed \\+CR");
6542 assert_eq!(parts[0].len, 2);
6543 assert!(matches!(&parts[1].part, StringPart::Var(_)));
6544 assert_eq!(parts[1].offset, 4);
6545 assert_eq!(parts[1].len, 4); }
6547
6548 #[test]
6549 fn spanned_standalone_cr_continuation_mid_run_keeps_span_start() {
6550 let parts = parse_interpolated_string_spanned("AB\\\rCD${x}", 0).unwrap();
6555 assert_eq!(parts.len(), 2);
6556 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "ABCD"));
6557 assert_eq!(parts[0].offset, 0);
6558 assert_eq!(parts[0].len, 6); assert!(matches!(&parts[1].part, StringPart::Var(_)));
6560 assert_eq!(parts[1].offset, 6);
6561 assert_eq!(parts[1].len, 4); }
6563
6564 fn assignment_value(source: &str) -> Expr {
6568 let program = parse(source).unwrap_or_else(|e| panic!("parse {source:?}: {e:?}"));
6569 match program.statements.as_slice() {
6570 [Stmt::Assignment(a)] => a.value.clone(),
6571 other => panic!("expected a single assignment, got {other:?}"),
6572 }
6573 }
6574
6575 #[test]
6576 fn list_literal_three_elements() {
6577 let expr = assignment_value("xs=[a b c]");
6578 match expr {
6579 Expr::ListLiteral(elems) => {
6580 assert_eq!(elems.len(), 3);
6581 assert!(elems.iter().all(|e| matches!(e, ListElem::Item(_))));
6582 }
6583 other => panic!("expected ListLiteral, got {other:?}"),
6584 }
6585 }
6586
6587 #[test]
6588 fn list_literal_empty() {
6589 let expr = assignment_value("xs=[]");
6590 assert!(matches!(expr, Expr::ListLiteral(elems) if elems.is_empty()));
6591 }
6592
6593 #[test]
6594 fn list_literal_single_glued_dog() {
6595 let expr = assignment_value("xs=[dog]");
6599 match expr {
6600 Expr::ListLiteral(elems) => assert_eq!(elems.len(), 1),
6601 other => panic!("expected ListLiteral, got {other:?}"),
6602 }
6603 }
6604
6605 #[test]
6606 fn list_literal_single_int() {
6607 let expr = assignment_value("xs=[1]");
6608 match expr {
6609 Expr::ListLiteral(elems) => match elems.as_slice() {
6610 [ListElem::Item(Expr::Literal(Value::Int(1)))] => {}
6611 other => panic!("expected one Int(1) item, got {other:?}"),
6612 },
6613 other => panic!("expected ListLiteral, got {other:?}"),
6614 }
6615 }
6616
6617 #[test]
6618 fn record_literal_unspaced_colon_equals_spaced() {
6619 let spaced = assignment_value("x={port: 8080}");
6620 let unspaced = assignment_value("x={port:8080}");
6621 assert_eq!(spaced, unspaced, "{{port:8080}} must parse identically to {{port: 8080}}");
6622 match spaced {
6623 Expr::RecordLiteral(entries) => match entries.as_slice() {
6624 [RecordEntry { key: RecordKey::Bare(k), value: Expr::Literal(Value::Int(8080)) }] => {
6625 assert_eq!(k, "port");
6626 }
6627 other => panic!("expected one port:8080 entry, got {other:?}"),
6628 },
6629 other => panic!("expected RecordLiteral, got {other:?}"),
6630 }
6631 }
6632
6633 #[test]
6634 fn record_literal_name_role() {
6635 let expr = assignment_value("u={name: amy, role: maintainer}");
6636 match expr {
6637 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2),
6638 other => panic!("expected RecordLiteral, got {other:?}"),
6639 }
6640 }
6641
6642 #[test]
6643 fn record_literal_multiline_trailing_comma() {
6644 let source = "services={\n web: {port: 8080, replicas: 3, healthy: true},\n api: {port: 9000, replicas: 2, healthy: false},\n}";
6645 let expr = assignment_value(source);
6646 match expr {
6647 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2, "web + api entries"),
6648 other => panic!("expected RecordLiteral, got {other:?}"),
6649 }
6650 }
6651
6652 #[test]
6653 fn record_literal_quoted_key() {
6654 let expr = assignment_value(r#"r={"content-type": x}"#);
6655 match expr {
6656 Expr::RecordLiteral(entries) => match entries.as_slice() {
6657 [RecordEntry { key: RecordKey::Quoted(k), .. }] => assert_eq!(k, "content-type"),
6658 other => panic!("expected one quoted-key entry, got {other:?}"),
6659 },
6660 other => panic!("expected RecordLiteral, got {other:?}"),
6661 }
6662 }
6663
6664 #[test]
6665 fn nested_list_and_record_in_record() {
6666 let expr = assignment_value("x={tags: [a b], meta: {active: true}}");
6667 match expr {
6668 Expr::RecordLiteral(entries) => {
6669 assert_eq!(entries.len(), 2);
6670 assert!(matches!(entries[0].value, Expr::ListLiteral(_)));
6671 assert!(matches!(entries[1].value, Expr::RecordLiteral(_)));
6672 }
6673 other => panic!("expected RecordLiteral, got {other:?}"),
6674 }
6675 }
6676
6677 #[test]
6678 fn spread_and_item_elements() {
6679 let expr = assignment_value("new=[...$xs date]");
6680 match expr {
6681 Expr::ListLiteral(elems) => match elems.as_slice() {
6682 [ListElem::Spread(Expr::VarRef(_)), ListElem::Item(Expr::Literal(Value::String(s)))] => {
6683 assert_eq!(s, "date");
6684 }
6685 other => panic!("expected [Spread($xs), Item(date)], got {other:?}"),
6686 },
6687 other => panic!("expected ListLiteral, got {other:?}"),
6688 }
6689 }
6690
6691 #[test]
6692 fn spread_of_two_variables() {
6693 let expr = assignment_value("c=[...$a ...$b]");
6694 match expr {
6695 Expr::ListLiteral(elems) => {
6696 assert_eq!(elems.len(), 2);
6697 assert!(elems.iter().all(|e| matches!(e, ListElem::Spread(_))));
6698 }
6699 other => panic!("expected ListLiteral, got {other:?}"),
6700 }
6701 }
6702
6703 #[test]
6704 fn in_rhs_accepts_a_list_literal() {
6705 let program = parse("if [[ $a not in [dog] ]]; then echo hit; fi")
6706 .unwrap_or_else(|e| panic!("parse: {e:?}"));
6707 assert_eq!(program.statements.len(), 1);
6708 }
6709
6710 #[test]
6711 fn multiword_bareword_record_value_is_a_parse_error() {
6712 assert!(parse("x={msg: hello world}").is_err());
6715 }
6716
6717 #[test]
6720 fn argv_bracket_glob_stays_a_glob_pattern() {
6721 let program = parse("ls [dog]").unwrap_or_else(|e| panic!("parse: {e:?}"));
6725 assert_eq!(program.statements.len(), 1);
6726 }
6727
6728 #[test]
6729 fn brace_expansion_at_argv_position_is_unaffected() {
6730 let program = parse("cmd *.{rs,go}").unwrap_or_else(|e| panic!("parse: {e:?}"));
6735 assert_eq!(program.statements.len(), 1);
6736 }
6737
6738 #[test]
6739 fn for_head_item_is_not_a_literal() {
6740 let program = parse("for x in [a]; do echo $x; done")
6743 .unwrap_or_else(|e| panic!("parse: {e:?}"));
6744 match program.statements.as_slice() {
6745 [Stmt::For(for_loop)] => {
6746 assert_eq!(for_loop.items.len(), 1);
6747 assert!(
6748 !matches!(for_loop.items[0], Expr::ListLiteral(_)),
6749 "for-head item must not be a ListLiteral: {:?}",
6750 for_loop.items[0]
6751 );
6752 }
6753 other => panic!("expected a single For statement, got {other:?}"),
6754 }
6755 }
6756
6757 #[derive(Debug, Clone, Copy)]
6763 enum NestingLayer {
6764 CmdSubst,
6766 QuotedCmdSubst,
6768 Case,
6770 If,
6771 For,
6772 }
6773
6774 fn wrap_in_layer(inner: &str, layer: NestingLayer) -> String {
6775 match layer {
6776 NestingLayer::CmdSubst => format!("x=$({inner})"),
6777 NestingLayer::QuotedCmdSubst => format!("x=\"pre $({inner}) post\""),
6778 NestingLayer::Case => format!("case v in v) {inner};; esac"),
6779 NestingLayer::If => format!("if true; then {inner}; fi"),
6780 NestingLayer::For => format!("for f in a; do {inner}; done"),
6781 }
6782 }
6783
6784 proptest::proptest! {
6785 #[test]
6803 fn nested_compound_constructs_always_parse(
6804 layers in proptest::collection::vec(
6805 proptest::prop_oneof![
6806 proptest::strategy::Just(NestingLayer::CmdSubst),
6807 proptest::strategy::Just(NestingLayer::QuotedCmdSubst),
6808 proptest::strategy::Just(NestingLayer::Case),
6809 proptest::strategy::Just(NestingLayer::If),
6810 proptest::strategy::Just(NestingLayer::For),
6811 ],
6812 1..=4,
6813 ).prop_filter("at most one QuotedCmdSubst layer", |layers| {
6814 layers.iter().filter(|l| matches!(l, NestingLayer::QuotedCmdSubst)).count() <= 1
6815 })
6816 ) {
6817 let source = layers
6818 .iter()
6819 .fold("echo x".to_string(), |inner, &layer| wrap_in_layer(&inner, layer));
6820 proptest::prop_assert!(
6821 parse(&source).is_ok(),
6822 "grammar-nested construct failed to parse: {source:?}"
6823 );
6824 }
6825 }
6826}