1use crate::ast::{
7 Arg, Assignment, BinaryOp, CaseBranch, CaseStmt, Command, Expr, FileTestOp, ForLoop, IfStmt,
8 ListElem, Pipeline, Program, RecordEntry, RecordKey, Redirect, RedirectKind, SpannedPart, Stmt,
9 StringPart, StringTestOp, TestCmpOp, TestExpr, ToolDef, Value, VarPath, VarSegment, WhileLoop,
10};
11use crate::lexer::{self, HereDocData, Token};
12use chumsky::{input::ValueInput, prelude::*};
13
14pub type Span = SimpleSpan;
16
17fn parse_var_expr(raw: &str) -> Expr {
24 if raw == "${?}" {
26 return Expr::LastExitCode;
27 }
28
29 if raw == "${$}" {
31 return Expr::CurrentPid;
32 }
33
34 if let Some(colon_idx) = find_default_separator(raw) {
37 let path = parse_varpath(&format!("${{{}}}", &raw[2..colon_idx]));
39 let default_str = &raw[colon_idx + 2..raw.len() - 1];
42 let default_word = unquote_default_word(default_str);
46 let default = parse_interpolated_string(&default_word)
47 .unwrap_or_else(|_| vec![StringPart::Literal(default_word.clone())]);
48 return Expr::VarWithDefault { path, default };
49 }
50
51 Expr::VarRef(parse_varpath(raw))
53}
54
55pub(crate) fn bash_substring_hint(var_content: &str) -> Option<String> {
67 let mut depth = 0usize;
68 let colon = var_content.char_indices().find_map(|(i, c)| match c {
69 '[' => {
70 depth += 1;
71 None
72 }
73 ']' => {
74 depth = depth.saturating_sub(1);
75 None
76 }
77 ':' if depth == 0 => Some(i),
78 _ => None,
79 })?;
80 let (name, rest) = var_content.split_at(colon);
81 let after_offset = &rest[1..];
83 let suggestion = if let Some(length) = after_offset.strip_prefix(':') {
86 format!("${{{name}[0:{length}]}}")
87 } else if after_offset.contains(':') {
88 format!("${{{name}[{after_offset}]}}")
89 } else {
90 format!("${{{name}[{after_offset}:]}}")
91 };
92 Some(format!(
93 "${{{var_content}}}: kaish slices with brackets, not `:offset:length` — \
94 write {suggestion}. Brackets are start:end and end-exclusive, so \
95 ${{{name}[0:5]}} is the first five characters and ${{{name}[-3:]}} the last three."
96 ))
97}
98
99fn unquote_default_word(word: &str) -> String {
129 let mut out = String::with_capacity(word.len());
130 let mut in_single = false;
131 let mut in_double = false;
132 let chars: Vec<char> = word.chars().collect();
133 let mut i = 0;
134 while i < chars.len() {
135 let ch = chars[i];
136 if ch == '\\' && !in_single {
140 let run_start = i;
141 while i < chars.len() && chars[i] == '\\' {
142 i += 1;
143 }
144 let run_len = i - run_start;
145 let next_is_quote =
148 chars.get(i).is_some_and(|c| *c == '"' || (*c == '\'' && !in_double));
149 if next_is_quote {
150 if run_len / 2 > 0 {
151 out.push_str(&"\\".repeat(run_len / 2));
152 }
153 if run_len % 2 == 1 {
154 out.push(chars[i]);
158 i += 1;
159 }
160 } else {
163 out.push_str(&"\\".repeat(run_len));
164 }
165 continue;
166 }
167 i += 1;
168 match ch {
169 '\'' if !in_double => in_single = !in_single,
172 '"' if !in_single => in_double = !in_double,
173 '$' if in_single => out.push_str("__KAISH_ESCAPED_DOLLAR__"),
175 _ => out.push(ch),
176 }
177 }
178 out
179}
180
181fn find_default_separator(raw: &str) -> Option<usize> {
183 let bytes = raw.as_bytes();
184 let mut depth = 0;
185 let mut bracket_depth = 0;
186 let mut i = 0;
187
188 while i < bytes.len() {
189 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
190 depth += 1;
191 i += 2;
192 continue;
193 }
194 if bytes[i] == b'}' && depth > 0 {
195 depth -= 1;
196 i += 1;
197 continue;
198 }
199 if bytes[i] == b'[' {
202 bracket_depth += 1;
203 } else if bytes[i] == b']' && bracket_depth > 0 {
204 bracket_depth -= 1;
205 }
206 if depth == 1
209 && bracket_depth == 0
210 && i + 1 < bytes.len()
211 && bytes[i] == b':'
212 && bytes[i + 1] == b'-'
213 {
214 return Some(i);
215 }
216 i += 1;
217 }
218 None
219}
220
221fn find_default_separator_in_content(content: &str) -> Option<usize> {
223 let bytes = content.as_bytes();
224 let mut depth = 0;
225 let mut bracket_depth = 0;
226 let mut i = 0;
227
228 while i < bytes.len() {
229 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
230 depth += 1;
231 i += 2;
232 continue;
233 }
234 if bytes[i] == b'}' && depth > 0 {
235 depth -= 1;
236 i += 1;
237 continue;
238 }
239 if bytes[i] == b'[' {
242 bracket_depth += 1;
243 } else if bytes[i] == b']' && bracket_depth > 0 {
244 bracket_depth -= 1;
245 }
246 if depth == 0
248 && bracket_depth == 0
249 && i + 1 < bytes.len()
250 && bytes[i] == b':'
251 && bytes[i + 1] == b'-'
252 {
253 return Some(i);
254 }
255 i += 1;
256 }
257 None
258}
259
260pub(crate) fn parse_varpath(raw: &str) -> VarPath {
267 let segment_strs = lexer::parse_var_ref(raw).unwrap_or_default();
268 let segments = segment_strs
269 .into_iter()
270 .enumerate()
271 .map(|(i, s)| {
272 if i == 0 {
273 VarSegment::Field(s)
275 } else if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
276 parse_subscript(inner)
277 } else {
278 VarSegment::Field(s)
281 }
282 })
283 .collect();
284 VarPath { segments }
285}
286
287fn parse_subscript(inner: &str) -> VarSegment {
294 if let Some(var) = inner.strip_prefix('$') {
296 return VarSegment::Dynamic(var.to_string());
297 }
298 if inner.len() >= 2
300 && ((inner.starts_with('"') && inner.ends_with('"'))
301 || (inner.starts_with('\'') && inner.ends_with('\'')))
302 {
303 return VarSegment::Key(inner[1..inner.len() - 1].to_string());
304 }
305 if let Some((lhs, rhs)) = inner.split_once(':') {
309 let bound = |s: &str| -> Option<Option<i64>> {
310 if s.is_empty() {
311 Some(None)
312 } else {
313 s.parse::<i64>().ok().map(Some)
314 }
315 };
316 if let (Some(start), Some(end)) = (bound(lhs), bound(rhs)) {
317 return VarSegment::Slice(start, end);
318 }
319 }
320 if let Ok(i) = inner.parse::<i64>() {
322 return VarSegment::Index(i);
323 }
324 VarSegment::Key(inner.to_string())
326}
327
328fn strip_empty_stmts(statements: Vec<Stmt>) -> Vec<Stmt> {
331 statements
332 .into_iter()
333 .filter(|s| !matches!(s, Stmt::Empty))
334 .collect()
335}
336
337fn parse_interpolated_string_spanned(
356 s: &str,
357 base_offset: usize,
358) -> Result<Vec<SpannedPart>, String> {
359 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
360
361 let chars_vec: Vec<char> = s.chars().collect();
362 let mut i = 0;
363 let mut pos: usize = 0;
364
365 let mut parts: Vec<SpannedPart> = Vec::new();
366 let mut current_text = String::new();
367 let mut current_text_start: usize = pos;
368
369 let push_literal =
370 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
371 if !current_text.is_empty() {
372 parts.push(SpannedPart {
373 part: StringPart::Literal(std::mem::take(current_text)),
374 offset: base_offset + *start,
375 len: end - *start,
376 });
377 *start = end;
378 }
379 };
380
381 while i < chars_vec.len() {
382 let ch = chars_vec[i];
383
384 if ch == '\x00' {
385 let start = pos;
387 i += 1;
388 pos += 1;
389 let mut marker = String::new();
390 while let Some(&c) = chars_vec.get(i) {
391 if c == '\x00' {
392 i += 1;
393 pos += 1;
394 break;
395 }
396 marker.push(c);
397 i += 1;
398 pos += c.len_utf8();
399 }
400 if marker == "DOLLAR" {
401 if current_text.is_empty() {
402 current_text_start = start;
403 }
404 current_text.push('$');
405 }
406 } else if ch == '\\' {
407 let next = chars_vec.get(i + 1).copied();
413 match next {
414 Some('$') => {
415 if current_text.is_empty() {
416 current_text_start = pos;
417 }
418 current_text.push('$');
419 i += 2;
420 pos += 2;
421 }
422 Some('\\') => {
423 if current_text.is_empty() {
424 current_text_start = pos;
425 }
426 current_text.push('\\');
427 i += 2;
428 pos += 2;
429 }
430 Some('\n') => {
431 i += 2;
434 pos += 2;
435 if current_text.is_empty() {
436 current_text_start = pos;
437 }
438 }
439 Some('\r') => {
440 i += 2;
442 pos += 2;
443 if chars_vec.get(i) == Some(&'\n') {
444 i += 1;
445 pos += 1;
446 }
447 if current_text.is_empty() {
448 current_text_start = pos;
449 }
450 }
451 _ => {
452 if current_text.is_empty() {
456 current_text_start = pos;
457 }
458 current_text.push('\\');
459 i += 1;
460 pos += 1;
461 }
462 }
463 } else if ch == '$' {
464 let part_start = pos;
466 let next = chars_vec.get(i + 1).copied();
467
468 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
469 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
471 i += 2; pos += 2;
473 let mut cmd_content = String::new();
474 let mut depth = 1;
475 while let Some(&c) = chars_vec.get(i) {
476 i += 1;
477 pos += c.len_utf8();
478 if c == '(' {
479 depth += 1;
480 cmd_content.push(c);
481 } else if c == ')' {
482 depth -= 1;
483 if depth == 0 {
484 break;
485 }
486 cmd_content.push(c);
487 } else {
488 cmd_content.push(c);
489 }
490 }
491 let inserted = if let Ok(program) = parse(&cmd_content) {
492 let stmts = strip_empty_stmts(program.statements);
495 if stmts.is_empty() {
496 false
497 } else {
498 parts.push(SpannedPart {
499 part: StringPart::CommandSubst(stmts),
500 offset: base_offset + part_start,
501 len: pos - part_start,
502 });
503 true
504 }
505 } else {
506 false
507 };
508 if inserted {
509 current_text_start = pos;
512 } else {
513 if current_text.is_empty() {
518 current_text_start = part_start;
519 }
520 current_text.push_str("$(");
521 current_text.push_str(&cmd_content);
522 current_text.push(')');
523 }
524 } else if next == Some('{') {
525 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
526 i += 2; pos += 2;
528 let mut var_content = String::new();
529 let mut depth = 1;
530 while let Some(&c) = chars_vec.get(i) {
531 i += 1;
532 pos += c.len_utf8();
533 if c == '{' && var_content.ends_with('$') {
534 depth += 1;
535 var_content.push(c);
536 } else if c == '}' {
537 depth -= 1;
538 if depth == 0 {
539 break;
540 }
541 var_content.push(c);
542 } else {
543 var_content.push(c);
544 }
545 }
546 let part = if let Some(name) = var_content.strip_prefix('#') {
547 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
548 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
549 let expr = var_content
550 .strip_prefix("__ARITH:")
551 .and_then(|s| s.strip_suffix("__"))
552 .unwrap_or("");
553 StringPart::Arithmetic(expr.to_string())
554 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
555 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
556 let default_str = &var_content[colon_idx + 2..];
557 let default_word = unquote_default_word(default_str);
562 let default = parse_interpolated_string(&default_word)
563 .unwrap_or_else(|_| vec![StringPart::Literal(default_word.clone())]);
564 StringPart::VarWithDefault { path, default }
565 } else if let Some(msg) = bash_substring_hint(&var_content) {
566 return Err(msg);
567 } else {
568 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
569 };
570 parts.push(SpannedPart {
571 part,
572 offset: base_offset + part_start,
573 len: pos - part_start,
574 });
575 current_text_start = pos;
576 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
577 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
578 i += 1; pos += 1;
580 if let Some(&digit) = chars_vec.get(i) {
581 let n = digit.to_digit(10).unwrap_or(0) as usize;
582 i += 1;
583 pos += digit.len_utf8();
584 parts.push(SpannedPart {
585 part: StringPart::Positional(n),
586 offset: base_offset + part_start,
587 len: pos - part_start,
588 });
589 }
590 current_text_start = pos;
591 } else if next == Some('@') {
592 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
593 i += 2; pos += 2;
595 parts.push(SpannedPart {
596 part: StringPart::AllArgs,
597 offset: base_offset + part_start,
598 len: pos - part_start,
599 });
600 current_text_start = pos;
601 } else if next == Some('#') {
602 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
603 i += 2; pos += 2;
605 parts.push(SpannedPart {
606 part: StringPart::ArgCount,
607 offset: base_offset + part_start,
608 len: pos - part_start,
609 });
610 current_text_start = pos;
611 } else if next == Some('?') {
612 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
613 i += 2; pos += 2;
615 parts.push(SpannedPart {
616 part: StringPart::LastExitCode,
617 offset: base_offset + part_start,
618 len: pos - part_start,
619 });
620 current_text_start = pos;
621 } else if next == Some('$') {
622 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
623 i += 2; pos += 2;
625 parts.push(SpannedPart {
626 part: StringPart::CurrentPid,
627 offset: base_offset + part_start,
628 len: pos - part_start,
629 });
630 current_text_start = pos;
631 } else if next.map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
632 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
633 i += 1; pos += 1;
635 let mut var_name = String::new();
636 while let Some(&c) = chars_vec.get(i) {
637 if c.is_ascii_alphanumeric() || c == '_' {
638 var_name.push(c);
639 i += 1;
640 pos += c.len_utf8();
641 } else {
642 break;
643 }
644 }
645 parts.push(SpannedPart {
646 part: StringPart::Var(VarPath::simple(var_name)),
647 offset: base_offset + part_start,
648 len: pos - part_start,
649 });
650 current_text_start = pos;
651 } else {
652 if current_text.is_empty() {
654 current_text_start = pos;
655 }
656 current_text.push(ch);
657 i += 1;
658 pos += 1;
659 }
660 } else {
661 if current_text.is_empty() {
662 current_text_start = pos;
663 }
664 current_text.push(ch);
665 i += 1;
666 pos += ch.len_utf8();
667 }
668 }
669
670 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
671
672 Ok(parts)
673}
674
675fn parse_interpolated_string(s: &str) -> Result<Vec<StringPart>, String> {
676 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
679
680 let mut parts = Vec::new();
681 let mut current_text = String::new();
682 let mut chars = s.chars().peekable();
683
684 while let Some(ch) = chars.next() {
685 if ch == '\x00' {
686 let mut marker = String::new();
688 while let Some(&c) = chars.peek() {
689 if c == '\x00' {
690 chars.next(); break;
692 }
693 if let Some(c) = chars.next() {
694 marker.push(c);
695 }
696 }
697 if marker == "DOLLAR" {
698 current_text.push('$');
699 }
700 } else if ch == '$' {
701 if chars.peek() == Some(&'(') {
703 if !current_text.is_empty() {
705 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
706 }
707
708 chars.next();
710
711 let mut cmd_content = String::new();
713 let mut paren_depth = 1;
714 for c in chars.by_ref() {
715 if c == '(' {
716 paren_depth += 1;
717 cmd_content.push(c);
718 } else if c == ')' {
719 paren_depth -= 1;
720 if paren_depth == 0 {
721 break;
722 }
723 cmd_content.push(c);
724 } else {
725 cmd_content.push(c);
726 }
727 }
728
729 match parse(&cmd_content) {
732 Ok(program) => {
733 let stmts = strip_empty_stmts(program.statements);
734 if stmts.is_empty() {
735 current_text.push_str("$(");
738 current_text.push_str(&cmd_content);
739 current_text.push(')');
740 } else {
741 parts.push(StringPart::CommandSubst(stmts));
742 }
743 }
744 Err(_) => {
745 return Err(format!(
749 "syntax error in command substitution: $({cmd_content})"
750 ));
751 }
752 }
753 } else if chars.peek() == Some(&'{') {
754 if !current_text.is_empty() {
756 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
757 }
758
759 chars.next();
761
762 let mut var_content = String::new();
764 let mut depth = 1;
765 for c in chars.by_ref() {
766 if c == '{' && var_content.ends_with('$') {
767 depth += 1;
768 var_content.push(c);
769 } else if c == '}' {
770 depth -= 1;
771 if depth == 0 {
772 break;
773 }
774 var_content.push(c);
775 } else {
776 var_content.push(c);
777 }
778 }
779
780 let part = if let Some(name) = var_content.strip_prefix('#') {
782 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
784 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
785 let expr = var_content
787 .strip_prefix("__ARITH:")
788 .and_then(|s| s.strip_suffix("__"))
789 .unwrap_or("");
790 StringPart::Arithmetic(expr.to_string())
791 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
792 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
794 let default_str = &var_content[colon_idx + 2..];
795 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
796 StringPart::VarWithDefault { path, default }
797 } else if let Some(msg) = bash_substring_hint(&var_content) {
798 return Err(msg);
799 } else {
800 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
802 };
803 parts.push(part);
804 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
805 if !current_text.is_empty() {
807 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
808 }
809 if let Some(digit) = chars.next() {
810 let n = digit.to_digit(10).unwrap_or(0) as usize;
811 parts.push(StringPart::Positional(n));
812 }
813 } else if chars.peek() == Some(&'@') {
814 if !current_text.is_empty() {
816 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
817 }
818 chars.next(); parts.push(StringPart::AllArgs);
820 } else if chars.peek() == Some(&'#') {
821 if !current_text.is_empty() {
823 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
824 }
825 chars.next(); parts.push(StringPart::ArgCount);
827 } else if chars.peek() == Some(&'?') {
828 if !current_text.is_empty() {
830 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
831 }
832 chars.next(); parts.push(StringPart::LastExitCode);
834 } else if chars.peek() == Some(&'$') {
835 if !current_text.is_empty() {
837 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
838 }
839 chars.next(); parts.push(StringPart::CurrentPid);
841 } else if chars.peek().map(|c| c.is_ascii_alphabetic() || *c == '_').unwrap_or(false) {
842 if !current_text.is_empty() {
844 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
845 }
846
847 let mut var_name = String::new();
849 while let Some(&c) = chars.peek() {
850 if c.is_ascii_alphanumeric() || c == '_' {
851 if let Some(c) = chars.next() {
852 var_name.push(c);
853 }
854 } else {
855 break;
856 }
857 }
858
859 parts.push(StringPart::Var(VarPath::simple(var_name)));
860 } else {
861 current_text.push(ch);
863 }
864 } else {
865 current_text.push(ch);
866 }
867 }
868
869 if !current_text.is_empty() {
870 parts.push(StringPart::Literal(current_text));
871 }
872
873 Ok(parts)
874}
875
876#[derive(Debug, Clone)]
878pub struct ParseError {
879 pub span: Span,
880 pub message: String,
881}
882
883impl ParseError {
884 pub fn format(&self, source: &str) -> String {
889 let start = self.span.start;
890 let mut line = 1usize;
891 let mut col = 1usize;
892 for (i, ch) in source.char_indices() {
893 if i >= start {
894 break;
895 }
896 if ch == '\n' {
897 line += 1;
898 col = 1;
899 } else {
900 col += 1;
901 }
902 }
903 let line_content = {
904 let line_start = source[..start.min(source.len())]
905 .rfind('\n')
906 .map_or(0, |i| i + 1);
907 let line_end = source[start.min(source.len())..]
908 .find('\n')
909 .map_or(source.len(), |i| start + i);
910 source.get(line_start..line_end).unwrap_or("")
911 };
912 if line_content.is_empty() {
913 format!("{}:{} [parse]: {}", line, col, self.message)
914 } else {
915 format!(
916 "{}:{} [parse]: {}\n | {}",
917 line, col, self.message, line_content
918 )
919 }
920 }
921}
922
923impl std::fmt::Display for ParseError {
924 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925 write!(f, "{} at {:?}", self.message, self.span)
926 }
927}
928
929impl std::error::Error for ParseError {}
930
931pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
933 let tokens = lexer::tokenize(source).map_err(|errs| {
935 errs.into_iter()
936 .map(|e| ParseError {
937 span: (e.span.start..e.span.end).into(),
938 message: format!("lexer error: {}", e.token),
939 })
940 .collect::<Vec<_>>()
941 })?;
942
943 let tokens: Vec<(Token, Span)> = tokens
945 .into_iter()
946 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
947 .collect();
948
949 for (tok, span) in &tokens {
955 let message = match tok {
956 Token::VarRef(raw) => raw
957 .strip_prefix("${")
958 .and_then(|s| s.strip_suffix('}'))
959 .filter(|_| find_default_separator(raw).is_none())
960 .and_then(bash_substring_hint),
961 Token::String(s) if s.contains("${") && s.contains(':') => {
965 parse_interpolated_string(s).err()
966 }
967 _ => None,
968 };
969 if let Some(message) = message {
970 return Err(vec![ParseError {
971 span: *span,
972 message,
973 }]);
974 }
975 }
976
977 let end_span: Span = (source.len()..source.len()).into();
979
980 let parser = program_parser();
982 let result = parser.parse(tokens.as_slice().map(end_span, |(t, s)| (t, s)));
983
984 let program = result.into_result().map_err(|errs| {
985 errs.into_iter()
986 .map(|e| ParseError {
987 span: *e.span(),
988 message: e.to_string(),
989 })
990 .collect::<Vec<_>>()
991 })?;
992
993 if first_ambiguous_stdin(&program.statements) {
998 return Err(vec![ParseError {
999 span: (0..0).into(),
1003 message: "multiple stdin redirects on one command are ambiguous; \
1004 use exactly one of `<`, `<<`, or `<<<`"
1005 .to_string(),
1006 }]);
1007 }
1008
1009 Ok(program)
1010}
1011
1012pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
1014 let program = parse(source)?;
1015 program
1016 .statements
1017 .into_iter()
1018 .find(|s| !matches!(s, Stmt::Empty))
1019 .ok_or_else(|| {
1020 vec![ParseError {
1021 span: (0..source.len()).into(),
1022 message: "empty input".to_string(),
1023 }]
1024 })
1025}
1026
1027fn program_parser<'tokens, 'src: 'tokens, I>(
1033) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
1034where
1035 I: ValueInput<'tokens, Token = Token, Span = Span>,
1036{
1037 statement_parser()
1038 .repeated()
1039 .collect::<Vec<_>>()
1040 .map(|statements| Program { statements })
1041}
1042
1043fn statement_parser<'tokens, I>(
1046) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1047where
1048 I: ValueInput<'tokens, Token = Token, Span = Span>,
1049{
1050 recursive(|stmt| {
1051 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
1052
1053 let break_stmt = just(Token::Break)
1055 .ignore_then(
1056 select! { Token::Int(n) => n as usize }.or_not()
1057 )
1058 .map(Stmt::Break);
1059
1060 let continue_stmt = just(Token::Continue)
1062 .ignore_then(
1063 select! { Token::Int(n) => n as usize }.or_not()
1064 )
1065 .map(Stmt::Continue);
1066
1067 let return_stmt = just(Token::Return)
1069 .ignore_then(primary_expr_parser().or_not())
1070 .map(|e| Stmt::Return(e.map(Box::new)));
1071
1072 let exit_stmt = just(Token::Exit)
1074 .ignore_then(primary_expr_parser().or_not())
1075 .map(|e| Stmt::Exit(e.map(Box::new)));
1076
1077 let set_flag_arg = choice((
1086 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
1087 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
1088 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
1090 ));
1091
1092 let option_value_str = select! {
1096 Token::NumberIdent(s) => s,
1097 Token::Int(n) => n.to_string(),
1098 Token::Ident(s) => s,
1099 };
1100
1101 let set_option_assign = ident_parser()
1105 .then_ignore(just(Token::Eq))
1106 .then(option_value_str)
1107 .map(|(name, value)| {
1108 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
1109 });
1110
1111 let set_quoted_arg = select! {
1115 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1116 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1117 };
1118
1119 let set_with_flags = just(Token::Set)
1121 .then(set_flag_arg)
1122 .then(
1123 choice((
1124 set_flag_arg,
1125 set_option_assign,
1127 set_quoted_arg,
1128 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
1130 ))
1131 .repeated()
1132 .collect::<Vec<_>>(),
1133 )
1134 .map(|((_, first_arg), mut rest_args)| {
1135 let mut args = vec![first_arg];
1136 args.append(&mut rest_args);
1137 Stmt::Command(Command {
1138 name: "set".to_string(),
1139 args,
1140 redirects: vec![],
1141 })
1142 });
1143
1144 let set_no_args = just(Token::Set)
1147 .then(
1148 choice((
1149 just(Token::Newline).to(()),
1150 just(Token::Semi).to(()),
1151 just(Token::And).to(()),
1152 just(Token::Or).to(()),
1153 end(),
1154 ))
1155 .rewind(),
1156 )
1157 .map(|_| Stmt::Command(Command {
1158 name: "set".to_string(),
1159 args: vec![],
1160 redirects: vec![],
1161 }));
1162
1163 let set_command = set_with_flags.or(set_no_args);
1167
1168 let env_prefix_assign = ident_parser()
1185 .then_ignore(just(Token::Eq))
1186 .then(value_expr_parser())
1187 .map(|(name, value)| Assignment { path: VarPath::simple(name), value, local: false });
1188 let env_scoped = env_prefix_assign
1189 .repeated()
1190 .at_least(1)
1191 .collect::<Vec<_>>()
1192 .then(pipeline_parser().map(pipeline_into_stmt))
1193 .map(|(assignments, body)| Stmt::EnvScoped {
1194 assignments,
1195 body: Box::new(body),
1196 });
1197
1198 let base_statement = choice((
1200 just(Token::Newline).to(Stmt::Empty),
1201 set_command,
1202 env_scoped,
1203 assignment_parser().map(Stmt::Assignment),
1204 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), if_parser(stmt.clone()).map(Stmt::If),
1208 for_parser(stmt.clone()).map(Stmt::For),
1209 while_parser(stmt.clone()).map(Stmt::While),
1210 case_parser(stmt.clone()).map(Stmt::Case),
1211 break_stmt,
1212 continue_stmt,
1213 return_stmt,
1214 exit_stmt,
1215 test_expr_stmt_parser().map(Stmt::Test),
1216 pipeline_parser().map(pipeline_into_stmt),
1218 ))
1219 .boxed();
1220
1221 base_statement
1227 .clone()
1228 .foldl(
1229 choice((
1230 just(Token::And).to(true), just(Token::Or).to(false), ))
1233 .then(base_statement)
1234 .repeated(),
1235 |left, (is_and, right): (bool, Stmt)| {
1236 if is_and {
1237 Stmt::AndChain {
1238 left: Box::new(left),
1239 right: Box::new(right),
1240 }
1241 } else {
1242 Stmt::OrChain {
1243 left: Box::new(left),
1244 right: Box::new(right),
1245 }
1246 }
1247 },
1248 )
1249 .then_ignore(terminator)
1250 })
1251}
1252
1253fn lvalue_subscript_parser<'tokens, I>(
1263) -> impl Parser<'tokens, I, VarSegment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1264where
1265 I: ValueInput<'tokens, Token = Token, Span = Span>,
1266{
1267 let interior = choice((
1268 select! { Token::SimpleVarRef(name) => VarSegment::Dynamic(name) },
1269 select! { Token::String(s) => VarSegment::Key(s) },
1270 select! { Token::SingleString(s) => VarSegment::Key(s) },
1271 select! { Token::Int(n) => VarSegment::Index(n) },
1272 select! { Token::Ident(s) => parse_subscript(&s) },
1273 ));
1274
1275 just(Token::LBracket)
1276 .ignore_then(interior)
1277 .then_ignore(just(Token::RBracket))
1278 .labelled("subscript")
1279}
1280
1281fn lvalue_path_parser<'tokens, I>(
1287) -> impl Parser<'tokens, I, VarPath, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1288where
1289 I: ValueInput<'tokens, Token = Token, Span = Span>,
1290{
1291 ident_parser()
1292 .then(lvalue_subscript_parser().repeated().collect::<Vec<_>>())
1293 .map(|(name, subscripts)| {
1294 let mut segments = vec![VarSegment::Field(name)];
1295 segments.extend(subscripts);
1296 VarPath { segments }
1297 })
1298 .labelled("lvalue path")
1299}
1300
1301fn assignment_parser<'tokens, I>(
1307) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1308where
1309 I: ValueInput<'tokens, Token = Token, Span = Span>,
1310{
1311 let local_assignment = just(Token::Local)
1313 .ignore_then(lvalue_path_parser())
1314 .then_ignore(just(Token::Eq))
1315 .then(value_expr_parser())
1316 .map(|(path, value)| Assignment {
1317 path,
1318 value,
1319 local: true,
1320 });
1321
1322 let bash_assignment = lvalue_path_parser()
1325 .then_ignore(just(Token::Eq))
1326 .then(value_expr_parser())
1327 .map(|(path, value)| Assignment {
1328 path,
1329 value,
1330 local: false,
1331 });
1332
1333 choice((local_assignment, bash_assignment))
1334 .labelled("assignment")
1335 .boxed()
1336}
1337
1338fn posix_function_parser<'tokens, I, S>(
1342 stmt: S,
1343) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1344where
1345 I: ValueInput<'tokens, Token = Token, Span = Span>,
1346 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1347{
1348 ident_parser()
1349 .then_ignore(just(Token::LParen))
1350 .then_ignore(just(Token::RParen))
1351 .then_ignore(just(Token::LBrace))
1352 .then_ignore(just(Token::Newline).repeated())
1353 .then(
1354 stmt.repeated()
1355 .collect::<Vec<_>>()
1356 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1357 )
1358 .then_ignore(just(Token::Newline).repeated())
1359 .then_ignore(just(Token::RBrace))
1360 .map(|(name, body)| ToolDef { name, params: vec![], body })
1361 .labelled("POSIX function")
1362 .boxed()
1363}
1364
1365fn bash_function_parser<'tokens, I, S>(
1369 stmt: S,
1370) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1371where
1372 I: ValueInput<'tokens, Token = Token, Span = Span>,
1373 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1374{
1375 just(Token::Function)
1376 .ignore_then(ident_parser())
1377 .then_ignore(just(Token::LBrace))
1378 .then_ignore(just(Token::Newline).repeated())
1379 .then(
1380 stmt.repeated()
1381 .collect::<Vec<_>>()
1382 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1383 )
1384 .then_ignore(just(Token::Newline).repeated())
1385 .then_ignore(just(Token::RBrace))
1386 .map(|(name, body)| ToolDef { name, params: vec![], body })
1387 .labelled("bash function")
1388 .boxed()
1389}
1390
1391fn if_parser<'tokens, I, S>(
1398 stmt: S,
1399) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1400where
1401 I: ValueInput<'tokens, Token = Token, Span = Span>,
1402 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1403{
1404 let branch = condition_parser()
1406 .then_ignore(just(Token::Semi).or_not())
1407 .then_ignore(just(Token::Newline).repeated())
1408 .then_ignore(just(Token::Then))
1409 .then_ignore(just(Token::Newline).repeated())
1410 .then(
1411 stmt.clone()
1412 .repeated()
1413 .collect::<Vec<_>>()
1414 .map(|stmts: Vec<Stmt>| {
1415 stmts
1416 .into_iter()
1417 .filter(|s| !matches!(s, Stmt::Empty))
1418 .collect::<Vec<_>>()
1419 }),
1420 );
1421
1422 let elif_branch = just(Token::Elif)
1424 .ignore_then(condition_parser())
1425 .then_ignore(just(Token::Semi).or_not())
1426 .then_ignore(just(Token::Newline).repeated())
1427 .then_ignore(just(Token::Then))
1428 .then_ignore(just(Token::Newline).repeated())
1429 .then(
1430 stmt.clone()
1431 .repeated()
1432 .collect::<Vec<_>>()
1433 .map(|stmts: Vec<Stmt>| {
1434 stmts
1435 .into_iter()
1436 .filter(|s| !matches!(s, Stmt::Empty))
1437 .collect::<Vec<_>>()
1438 }),
1439 );
1440
1441 let else_branch = just(Token::Else)
1443 .ignore_then(just(Token::Newline).repeated())
1444 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1445 .map(|stmts: Vec<Stmt>| {
1446 stmts
1447 .into_iter()
1448 .filter(|s| !matches!(s, Stmt::Empty))
1449 .collect::<Vec<_>>()
1450 });
1451
1452 just(Token::If)
1453 .ignore_then(branch)
1454 .then(elif_branch.repeated().collect::<Vec<_>>())
1455 .then(else_branch.or_not())
1456 .then_ignore(just(Token::Fi))
1457 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1458 build_if_chain(condition, then_branch, elif_branches, else_branch)
1460 })
1461 .labelled("if statement")
1462 .boxed()
1463}
1464
1465fn build_if_chain(
1472 condition: Expr,
1473 then_branch: Vec<Stmt>,
1474 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1475 else_branch: Option<Vec<Stmt>>,
1476) -> IfStmt {
1477 if elif_branches.is_empty() {
1478 IfStmt {
1480 condition: Box::new(condition),
1481 then_branch,
1482 else_branch,
1483 }
1484 } else {
1485 let (elif_cond, elif_then) = elif_branches.remove(0);
1487 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1488 IfStmt {
1489 condition: Box::new(condition),
1490 then_branch,
1491 else_branch: Some(vec![Stmt::If(nested_if)]),
1492 }
1493 }
1494}
1495
1496fn for_parser<'tokens, I, S>(
1498 stmt: S,
1499) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1500where
1501 I: ValueInput<'tokens, Token = Token, Span = Span>,
1502 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1503{
1504 just(Token::For)
1505 .ignore_then(ident_parser())
1506 .then_ignore(just(Token::In))
1507 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1508 .then_ignore(just(Token::Semi).or_not())
1509 .then_ignore(just(Token::Newline).repeated())
1510 .then_ignore(just(Token::Do))
1511 .then_ignore(just(Token::Newline).repeated())
1512 .then(
1513 stmt.repeated()
1514 .collect::<Vec<_>>()
1515 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1516 )
1517 .then_ignore(just(Token::Done))
1518 .map(|((variable, items), body)| ForLoop {
1519 variable,
1520 items,
1521 body,
1522 })
1523 .labelled("for loop")
1524 .boxed()
1525}
1526
1527fn while_parser<'tokens, I, S>(
1529 stmt: S,
1530) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1531where
1532 I: ValueInput<'tokens, Token = Token, Span = Span>,
1533 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1534{
1535 just(Token::While)
1536 .ignore_then(condition_parser())
1537 .then_ignore(just(Token::Semi).or_not())
1538 .then_ignore(just(Token::Newline).repeated())
1539 .then_ignore(just(Token::Do))
1540 .then_ignore(just(Token::Newline).repeated())
1541 .then(
1542 stmt.repeated()
1543 .collect::<Vec<_>>()
1544 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1545 )
1546 .then_ignore(just(Token::Done))
1547 .map(|(condition, body)| WhileLoop {
1548 condition: Box::new(condition),
1549 body,
1550 })
1551 .labelled("while loop")
1552 .boxed()
1553}
1554
1555fn case_parser<'tokens, I, S>(
1562 stmt: S,
1563) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1564where
1565 I: ValueInput<'tokens, Token = Token, Span = Span>,
1566 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1567{
1568 let pattern_part = choice((
1571 select! { Token::GlobWord(s) => s },
1572 select! { Token::Ident(s) => s },
1573 select! { Token::NumberIdent(s) => s },
1574 select! { Token::DashNumWord(s) => s },
1575 select! { Token::AtWord(s) => s },
1576 select! { Token::DottedIdent(s) => s },
1577 select! { Token::String(s) => s },
1578 select! { Token::SingleString(s) => s },
1579 select! { Token::Int(n) => n.to_string() },
1580 select! { Token::Star => "*".to_string() },
1581 select! { Token::Question => "?".to_string() },
1582 select! { Token::Dot => ".".to_string() },
1583 select! { Token::DotDot => "..".to_string() },
1584 select! { Token::Tilde => "~".to_string() },
1585 select! { Token::TildePath(s) => s },
1586 select! { Token::RelativePath(s) => s },
1587 select! { Token::DotSlashPath(s) => s },
1588 select! { Token::Path(p) => p },
1589 select! { Token::VarRef(v) => v },
1590 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1591 choice((
1599 select! { Token::DoubleDashBare(s) => s },
1600 select! { Token::PlusBare(s) => s },
1601 select! { Token::MinusBare(s) => s },
1602 select! { Token::MinusAlone => "-".to_string() },
1603 select! { Token::DoubleDash => "--".to_string() },
1604 select! { Token::ShortFlag(s) => format!("-{}", s) },
1605 select! { Token::LongFlag(s) => format!("--{}", s) },
1606 select! { Token::PlusFlag(s) => format!("+{}", s) },
1607 )),
1608 just(Token::LBracket)
1610 .ignore_then(
1611 choice((
1612 select! { Token::Ident(s) => s },
1613 select! { Token::Int(n) => n.to_string() },
1614 just(Token::Colon).to(":".to_string()),
1615 just(Token::Bang).to("!".to_string()),
1617 select! { Token::ShortFlag(s) => format!("-{}", s) },
1619 ))
1620 .repeated()
1621 .at_least(1)
1622 .collect::<Vec<String>>()
1623 )
1624 .then_ignore(just(Token::RBracket))
1625 .map(|parts| format!("[{}]", parts.join(""))),
1626 just(Token::LBrace)
1628 .ignore_then(
1629 choice((
1630 select! { Token::Ident(s) => s },
1631 select! { Token::Int(n) => n.to_string() },
1632 ))
1633 .separated_by(just(Token::Comma))
1634 .at_least(1)
1635 .collect::<Vec<String>>()
1636 )
1637 .then_ignore(just(Token::RBrace))
1638 .map(|parts| format!("{{{}}}", parts.join(","))),
1639 ));
1640
1641 let pattern = pattern_part
1644 .repeated()
1645 .at_least(1)
1646 .collect::<Vec<String>>()
1647 .map(|parts| parts.join(""))
1648 .labelled("case pattern");
1649
1650 let patterns = pattern
1652 .separated_by(just(Token::Pipe))
1653 .at_least(1)
1654 .collect::<Vec<String>>()
1655 .labelled("case patterns");
1656
1657 let branch = just(Token::LParen)
1659 .or_not()
1660 .ignore_then(just(Token::Newline).repeated())
1661 .ignore_then(patterns)
1662 .then_ignore(just(Token::RParen))
1663 .then_ignore(just(Token::Newline).repeated())
1664 .then(
1665 stmt.clone()
1666 .repeated()
1667 .collect::<Vec<_>>()
1668 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1669 )
1670 .then_ignore(just(Token::DoubleSemi))
1671 .then_ignore(just(Token::Newline).repeated())
1672 .map(|(patterns, body)| CaseBranch { patterns, body })
1673 .labelled("case branch");
1674
1675 just(Token::Case)
1676 .ignore_then(expr_parser())
1677 .then_ignore(just(Token::In))
1678 .then_ignore(just(Token::Newline).repeated())
1679 .then(branch.repeated().collect::<Vec<_>>())
1680 .then_ignore(just(Token::Esac))
1681 .map(|(expr, branches)| CaseStmt { expr, branches })
1682 .labelled("case statement")
1683 .boxed()
1684}
1685
1686fn pipeline_parser<'tokens, I>(
1688) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1689where
1690 I: ValueInput<'tokens, Token = Token, Span = Span>,
1691{
1692 command_parser()
1693 .separated_by(just(Token::Pipe))
1694 .at_least(1)
1695 .collect::<Vec<_>>()
1696 .then(just(Token::Amp).or_not())
1697 .map(|(commands, bg)| Pipeline {
1698 commands,
1699 background: bg.is_some(),
1700 })
1701 .labelled("pipeline")
1702 .boxed()
1703}
1704
1705fn command_parser<'tokens, I>(
1708) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1709where
1710 I: ValueInput<'tokens, Token = Token, Span = Span>,
1711{
1712 let command_name = choice((
1714 ident_parser(),
1715 path_parser(),
1716 select! { Token::DotSlashPath(s) => s },
1717 just(Token::True).to("true".to_string()),
1718 just(Token::False).to("false".to_string()),
1719 just(Token::Dot).to(".".to_string()),
1720 ));
1721
1722 command_name
1732 .then(args_list_parser())
1733 .then(redirect_parser(primary_expr_parser()).repeated().collect::<Vec<_>>())
1734 .map(|((name, args), redirects)| Command {
1735 name,
1736 args,
1737 redirects,
1738 })
1739 .labelled("command")
1740 .boxed()
1741}
1742
1743fn pipeline_into_stmt(p: Pipeline) -> Stmt {
1748 if p.commands.len() == 1 && !p.background && p.commands[0].redirects.is_empty() {
1749 match p.commands.into_iter().next() {
1750 Some(cmd) => Stmt::Command(cmd),
1751 None => Stmt::Empty, }
1753 } else {
1754 Stmt::Pipeline(p)
1755 }
1756}
1757
1758fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
1762 cmd.redirects
1763 .iter()
1764 .filter(|r| {
1765 matches!(
1766 r.kind,
1767 RedirectKind::Stdin | RedirectKind::HereDoc | RedirectKind::HereString
1768 )
1769 })
1770 .count()
1771 > 1
1772}
1773
1774fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
1778 stmts.iter().any(stmt_has_ambiguous_stdin)
1779}
1780
1781fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
1782 match stmt {
1783 Stmt::Command(c) => command_has_ambiguous_stdin(c),
1784 Stmt::Pipeline(p) => p.commands.iter().any(command_has_ambiguous_stdin),
1785 Stmt::If(i) => {
1786 first_ambiguous_stdin(&i.then_branch)
1787 || i.else_branch
1788 .as_deref()
1789 .is_some_and(first_ambiguous_stdin)
1790 }
1791 Stmt::For(f) => first_ambiguous_stdin(&f.body),
1792 Stmt::While(w) => first_ambiguous_stdin(&w.body),
1793 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
1794 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
1795 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
1796 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
1797 }
1798 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
1799 Stmt::Assignment(_)
1800 | Stmt::Break(_)
1801 | Stmt::Continue(_)
1802 | Stmt::Return(_)
1803 | Stmt::Exit(_)
1804 | Stmt::Test(_)
1805 | Stmt::Empty => false,
1806 }
1807}
1808
1809fn is_glue_candidate(arg: &Arg) -> bool {
1830 matches!(arg, Arg::Positional(_) | Arg::LongFlag(_))
1831}
1832
1833fn reject_glued_args<'src>(
1851 args: Vec<(Arg, Span)>,
1852) -> Result<Vec<Arg>, Rich<'src, Token, Span>> {
1853 for pair in args.windows(2) {
1854 let (prev, prev_span) = &pair[0];
1855 let (next, next_span) = &pair[1];
1856 if is_glue_candidate(prev) && is_glue_candidate(next) && prev_span.end == next_span.start {
1857 let msg = "adjacent words with no space between them are not joined into one \
1858 argument (kaish does no token pasting); quote the whole word, e.g. \
1859 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\"";
1860 return Err(Rich::custom(*next_span, msg));
1861 }
1862 }
1863 Ok(args.into_iter().map(|(arg, _)| arg).collect())
1864}
1865
1866fn args_list_parser<'tokens, I>(
1870) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1871where
1872 I: ValueInput<'tokens, Token = Token, Span = Span>,
1873{
1874 let pre_dash = arg_before_double_dash_parser()
1882 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1883 .repeated()
1884 .collect::<Vec<(Arg, Span)>>()
1885 .try_map(|args, _span| reject_glued_args(args));
1886
1887 let double_dash = select! {
1889 Token::DoubleDash => Arg::DoubleDash,
1890 };
1891
1892 let post_dash_arg = choice((
1894 select! {
1896 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
1897 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
1898 },
1899 word_assign_arg_parser(),
1906 test_operator_arg_parser(),
1908 primary_expr_parser().map(Arg::Positional),
1910 ));
1911
1912 let post_dash = post_dash_arg
1916 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1917 .repeated()
1918 .collect::<Vec<(Arg, Span)>>()
1919 .try_map(|args, _span| reject_glued_args(args));
1920
1921 pre_dash
1923 .then(double_dash.then(post_dash).or_not())
1924 .map(|(mut args, maybe_dd)| {
1925 if let Some((dd, post)) = maybe_dd {
1926 args.push(dd);
1927 args.extend(post);
1928 }
1929 args
1930 })
1931}
1932
1933fn keyword_word<'tokens, I>(
1942) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1943where
1944 I: ValueInput<'tokens, Token = Token, Span = Span>,
1945{
1946 select! {
1947 Token::Set => "set",
1948 Token::Local => "local",
1949 Token::If => "if",
1950 Token::Then => "then",
1951 Token::Else => "else",
1952 Token::Elif => "elif",
1953 Token::Fi => "fi",
1954 Token::For => "for",
1955 Token::While => "while",
1956 Token::In => "in",
1957 Token::Do => "do",
1958 Token::Done => "done",
1959 Token::Case => "case",
1960 Token::Esac => "esac",
1961 Token::Function => "function",
1962 Token::Break => "break",
1963 Token::Continue => "continue",
1964 Token::Return => "return",
1965 Token::Exit => "exit",
1966 }
1967 .map(|s| s.to_string())
1968}
1969
1970fn word_assign_arg_parser<'tokens, I>(
1980) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1981where
1982 I: ValueInput<'tokens, Token = Token, Span = Span>,
1983{
1984 choice((
1985 select! { Token::Ident(s) => s },
1986 keyword_word(),
1987 ))
1988 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1989 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1990 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1991 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1992 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1994 Err(Rich::custom(
1995 span,
1996 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
1997 ))
1998 } else {
1999 Ok(Arg::WordAssign { key, value })
2000 }
2001 })
2002}
2003
2004fn test_operator_arg_parser<'tokens, I>(
2021) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2022where
2023 I: ValueInput<'tokens, Token = Token, Span = Span>,
2024{
2025 select! {
2026 Token::Eq => "=",
2027 Token::EqEq => "==",
2028 Token::NotEq => "!=",
2029 Token::Bang => "!",
2030 }
2031 .map(|s| Arg::Positional(Expr::Literal(Value::String(s.to_string()))))
2032}
2033
2034fn arg_before_double_dash_parser<'tokens, I>(
2036) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2037where
2038 I: ValueInput<'tokens, Token = Token, Span = Span>,
2039{
2040 let long_flag_with_value = select! {
2042 Token::LongFlag(name) => name,
2043 }
2044 .then_ignore(just(Token::Eq))
2045 .then(primary_expr_parser())
2046 .map(|(key, value)| Arg::Named { key, value });
2047
2048 let long_flag = select! {
2050 Token::LongFlag(name) => Arg::LongFlag(name),
2051 };
2052
2053 let short_flag = select! {
2055 Token::ShortFlag(name) => Arg::ShortFlag(name),
2056 };
2057
2058 let named = word_assign_arg_parser();
2060
2061 let positional = primary_expr_parser().map(Arg::Positional);
2063
2064 let test_operator = test_operator_arg_parser();
2068
2069 choice((
2072 long_flag_with_value,
2073 long_flag,
2074 short_flag,
2075 named,
2076 test_operator,
2077 positional,
2078 ))
2079 .boxed()
2080}
2081
2082fn redirect_parser<'tokens, I, T>(
2092 target: T,
2093) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2094where
2095 I: ValueInput<'tokens, Token = Token, Span = Span>,
2096 T: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2097{
2098 let target = target
2111 .clone()
2112 .map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) })
2113 .then(target.clone().map_with(|_, e| e.span()).rewind().or_not())
2114 .try_map(|((expr, span), glued), _| match glued {
2115 Some(next_span) if next_span.start == span.end => Err(Rich::custom(
2116 next_span,
2117 "adjacent words with no space between them are not joined into the redirect \
2118 target (kaish does no token pasting); quote the whole target, e.g. \
2119 \"/tmp/$(echo x).txt\"",
2120 )),
2121 _ => Ok(expr),
2122 })
2123 .boxed();
2124
2125 let regular_redirect = select! {
2127 Token::GtGt => RedirectKind::StdoutAppend,
2128 Token::Gt => RedirectKind::StdoutOverwrite,
2129 Token::Lt => RedirectKind::Stdin,
2130 Token::Stderr => RedirectKind::Stderr,
2131 Token::Both => RedirectKind::Both,
2132 }
2133 .then(target.clone())
2134 .map(|(kind, target)| Redirect { kind, target });
2135
2136 let heredoc_redirect = just(Token::HereDocStart)
2144 .ignore_then(select! { Token::HereDoc(data) => data })
2145 .try_map(|data: HereDocData, span| {
2146 let target = if data.literal {
2147 let body = if data.strip_tabs {
2148 crate::interpreter::strip_leading_tabs(&data.content)
2149 } else {
2150 data.content
2151 };
2152 Expr::Literal(Value::String(body))
2153 } else {
2154 let parts =
2155 parse_interpolated_string_spanned(&data.content, data.body_start_offset)
2156 .map_err(|msg| Rich::custom(span, msg))?;
2157 if parts.len() == 1 && !data.strip_tabs {
2161 if let StringPart::Literal(text) = &parts[0].part {
2162 return Ok(Redirect {
2163 kind: RedirectKind::HereDoc,
2164 target: Expr::Literal(Value::String(text.clone())),
2165 });
2166 }
2167 }
2168 Expr::HereDocBody {
2169 parts,
2170 strip_tabs: data.strip_tabs,
2171 }
2172 };
2173 Ok(Redirect {
2174 kind: RedirectKind::HereDoc,
2175 target,
2176 })
2177 });
2178
2179 let herestring_redirect = just(Token::HereString)
2183 .ignore_then(target.clone())
2184 .map(|target| Redirect {
2185 kind: RedirectKind::HereString,
2186 target,
2187 });
2188
2189 let merge_stderr_redirect = just(Token::StderrToStdout)
2191 .map(|_| Redirect {
2192 kind: RedirectKind::MergeStderr,
2193 target: Expr::Literal(Value::Null),
2195 });
2196
2197 let merge_stdout_redirect = choice((
2199 just(Token::StdoutToStderr),
2200 just(Token::StdoutToStderr2),
2201 ))
2202 .map(|_| Redirect {
2203 kind: RedirectKind::MergeStdout,
2204 target: Expr::Literal(Value::Null),
2206 });
2207
2208 choice((
2209 heredoc_redirect,
2210 herestring_redirect,
2211 merge_stderr_redirect,
2212 merge_stdout_redirect,
2213 regular_redirect,
2214 ))
2215 .labelled("redirect")
2216 .boxed()
2217}
2218
2219fn test_expr_stmt_parser<'tokens, I>(
2231) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2232where
2233 I: ValueInput<'tokens, Token = Token, Span = Span>,
2234{
2235 let file_test_op = select! {
2237 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
2238 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
2239 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
2240 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
2241 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
2242 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
2243 };
2244
2245 let string_test_op = select! {
2249 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
2250 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
2251 Token::ShortFlag(s) if s == "list" => StringTestOp::IsList,
2252 Token::ShortFlag(s) if s == "record" => StringTestOp::IsRecord,
2253 };
2254
2255 let cmp_op = choice((
2258 just(Token::EqEq).to(TestCmpOp::Eq),
2259 just(Token::Eq).to(TestCmpOp::Eq),
2260 just(Token::NotEq).to(TestCmpOp::NotEq),
2261 just(Token::Match).to(TestCmpOp::Match),
2262 just(Token::NotMatch).to(TestCmpOp::NotMatch),
2263 just(Token::Gt).to(TestCmpOp::Gt),
2264 just(Token::Lt).to(TestCmpOp::Lt),
2265 just(Token::GtEq).to(TestCmpOp::GtEq),
2266 just(Token::LtEq).to(TestCmpOp::LtEq),
2267 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
2268 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
2269 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
2270 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
2271 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
2272 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
2273 ));
2274
2275 let file_test = file_test_op
2277 .then(primary_expr_parser())
2278 .map(|(op, path)| TestExpr::FileTest {
2279 op,
2280 path: Box::new(path),
2281 });
2282
2283 let string_test = string_test_op
2285 .then(primary_expr_parser())
2286 .map(|(op, value)| TestExpr::StringTest {
2287 op,
2288 value: Box::new(value),
2289 });
2290
2291 let comparison = primary_expr_parser()
2293 .then(cmp_op)
2294 .then(primary_expr_parser())
2295 .map(|((left, op), right)| TestExpr::Comparison {
2296 left: Box::new(left),
2297 op,
2298 right: Box::new(right),
2299 });
2300
2301 let not_in = primary_expr_parser()
2307 .then_ignore(select! { Token::Ident(s) if s == "not" => () })
2308 .then_ignore(just(Token::In))
2309 .then(value_primary_parser())
2310 .map(|(left, right)| TestExpr::NotIn {
2311 left: Box::new(left),
2312 right: Box::new(right),
2313 });
2314
2315 let in_ = primary_expr_parser()
2316 .then_ignore(just(Token::In))
2317 .then(value_primary_parser())
2318 .map(|(left, right)| TestExpr::In {
2319 left: Box::new(left),
2320 right: Box::new(right),
2321 });
2322
2323 let primary_test = choice((file_test, string_test, not_in, in_, comparison));
2325
2326 let unary = recursive(|unary| {
2340 let not_expr = just(Token::Bang)
2341 .ignore_then(unary)
2342 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
2343 choice((not_expr, primary_test.clone()))
2344 });
2345
2346 let and_expr = unary.clone().foldl(
2348 just(Token::And).ignore_then(unary).repeated(),
2349 |left, right| TestExpr::And {
2350 left: Box::new(left),
2351 right: Box::new(right),
2352 },
2353 );
2354
2355 let compound_test = and_expr.clone().foldl(
2357 just(Token::Or).ignore_then(and_expr).repeated(),
2358 |left, right| TestExpr::Or {
2359 left: Box::new(left),
2360 right: Box::new(right),
2361 },
2362 );
2363
2364 just(Token::LBracket)
2367 .then(just(Token::LBracket))
2368 .ignore_then(compound_test)
2369 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
2370 .labelled("test expression")
2371 .boxed()
2372}
2373
2374fn condition_parser<'tokens, I>(
2389) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2390where
2391 I: ValueInput<'tokens, Token = Token, Span = Span>,
2392{
2393 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
2395
2396 let command_condition = command_parser().map(Expr::Command);
2399
2400 let base = choice((test_expr_condition, command_condition));
2402
2403 let and_expr = base.clone().foldl(
2406 just(Token::And).ignore_then(base).repeated(),
2407 |left, right| Expr::BinaryOp {
2408 left: Box::new(left),
2409 op: BinaryOp::And,
2410 right: Box::new(right),
2411 },
2412 );
2413
2414 and_expr
2416 .clone()
2417 .foldl(
2418 just(Token::Or).ignore_then(and_expr).repeated(),
2419 |left, right| Expr::BinaryOp {
2420 left: Box::new(left),
2421 op: BinaryOp::Or,
2422 right: Box::new(right),
2423 },
2424 )
2425 .labelled("condition")
2426 .boxed()
2427}
2428
2429fn expr_parser<'tokens, I>(
2436) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2437where
2438 I: ValueInput<'tokens, Token = Token, Span = Span>,
2439{
2440 primary_expr_parser()
2442}
2443
2444fn value_expr_parser<'tokens, I>(
2449) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2450where
2451 I: ValueInput<'tokens, Token = Token, Span = Span>,
2452{
2453 value_literal_parser()
2454}
2455
2456fn value_primary_parser<'tokens, I>(
2462) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2463where
2464 I: ValueInput<'tokens, Token = Token, Span = Span>,
2465{
2466 value_literal_parser()
2467}
2468
2469fn value_literal_parser<'tokens, I>(
2481) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2482where
2483 I: ValueInput<'tokens, Token = Token, Span = Span>,
2484{
2485 recursive(|value| {
2486 choice((
2487 list_literal_parser(value.clone()),
2488 record_literal_parser(value.clone()),
2489 primary_expr_parser(),
2490 ))
2491 })
2492 .boxed()
2493}
2494
2495fn list_literal_parser<'tokens, I, V>(
2503 value: V,
2504) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2505where
2506 I: ValueInput<'tokens, Token = Token, Span = Span>,
2507 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2508{
2509 let spread_elem = just(Token::DotDotDot)
2510 .ignore_then(value.clone())
2511 .map(ListElem::Spread);
2512 let item_elem = value.map(ListElem::Item);
2513 let elem = choice((spread_elem, item_elem));
2514
2515 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2516
2517 just(Token::LBracket)
2518 .ignore_then(just(Token::Newline).repeated())
2519 .ignore_then(elem.then_ignore(sep).repeated().collect::<Vec<_>>())
2520 .then_ignore(just(Token::RBracket))
2521 .map(Expr::ListLiteral)
2522 .labelled("list literal")
2523}
2524
2525fn record_literal_parser<'tokens, I, V>(
2533 value: V,
2534) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2535where
2536 I: ValueInput<'tokens, Token = Token, Span = Span>,
2537 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2538{
2539 let bare_key = select! { Token::Ident(s) => RecordKey::Bare(s) };
2540 let double_key = select! { Token::String(s) => s }.try_map(|s, span| {
2546 let parts = parse_interpolated_string(&s)
2547 .map_err(|e| Rich::custom(span, format!("record key: {e}")))?;
2548 Ok(match parts.as_slice() {
2549 [] => RecordKey::Quoted(String::new()),
2550 [StringPart::Literal(lit)] => RecordKey::Quoted(lit.clone()),
2551 _ => RecordKey::Interpolated(parts),
2552 })
2553 });
2554 let single_key = select! { Token::SingleString(s) => RecordKey::Quoted(s) };
2555 let key = choice((double_key, single_key, bare_key)).labelled("record key");
2556
2557 let stray_bareword_after_value = select! { Token::Ident(s) => s }
2569 .then(just(Token::Colon).or_not())
2570 .rewind()
2571 .or_not()
2572 .try_map(|maybe, span| match maybe {
2573 Some((word, None)) => Err(Rich::custom(
2574 span,
2575 format!(
2576 "record value: unexpected word \"{word}\" after the value — a multi-word \
2577 value must be quoted, e.g. {{key: \"hello world\"}}"
2578 ),
2579 )),
2580 _ => Ok(()),
2581 });
2582
2583 let entry = key
2584 .then_ignore(just(Token::Colon))
2585 .then(value)
2586 .then_ignore(stray_bareword_after_value)
2587 .map(|(key, value)| RecordEntry { key, value });
2588
2589 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2590
2591 just(Token::LBrace)
2592 .ignore_then(just(Token::Newline).repeated())
2593 .ignore_then(entry.then_ignore(sep).repeated().collect::<Vec<_>>())
2594 .then_ignore(just(Token::RBrace))
2595 .map(Expr::RecordLiteral)
2596 .labelled("record literal")
2597}
2598
2599fn primary_expr_parser<'tokens, I>(
2603) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2604where
2605 I: ValueInput<'tokens, Token = Token, Span = Span>,
2606{
2607 let positional = select! {
2609 Token::Positional(n) => Expr::Positional(n),
2610 Token::AllArgs => Expr::AllArgs,
2611 Token::ArgCount => Expr::ArgCount,
2612 Token::VarLength(name) => Expr::VarLength(parse_varpath(&format!("${{{name}}}"))),
2613 Token::LastExitCode => Expr::LastExitCode,
2614 Token::CurrentPid => Expr::CurrentPid,
2615 };
2616
2617 let arithmetic = select! {
2619 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
2620 };
2621
2622 let keyword_as_bareword = select! {
2625 Token::Done => "done",
2626 Token::Fi => "fi",
2627 Token::Then => "then",
2628 Token::Else => "else",
2629 Token::Elif => "elif",
2630 Token::In => "in",
2631 Token::Do => "do",
2632 Token::Esac => "esac",
2633 Token::Set => "set",
2638 }
2639 .map(|s| Expr::Literal(Value::String(s.to_string())));
2640
2641 let plus_minus_bare = select! {
2645 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
2646 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
2647 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
2648 Token::DoubleDashBare(s) => Expr::Literal(Value::String(s)),
2649 };
2650
2651 let glob_pattern = select! {
2653 Token::GlobWord(s) => Expr::GlobPattern(s),
2654 Token::Star => Expr::GlobPattern("*".to_string()),
2655 Token::Question => Expr::GlobPattern("?".to_string()),
2656 };
2657
2658 recursive(|expr| {
2659 choice((
2660 positional,
2661 arithmetic,
2662 cmd_subst_parser(expr.clone()),
2663 var_expr_parser(),
2664 interpolated_string_parser(),
2665 literal_parser().map(Expr::Literal),
2666 glob_pattern,
2668 ident_parser().map(|s| Expr::Literal(Value::String(s))),
2670 path_parser().map(|s| Expr::Literal(Value::String(s))),
2672 select! {
2675 Token::Dot => Expr::Literal(Value::String(".".into())),
2681 Token::DotDot => Expr::Literal(Value::String("..".into())),
2682 Token::Comma => Expr::Literal(Value::String(",".into())),
2694 Token::Colon => Expr::Literal(Value::String(":".into())),
2701 Token::Tilde => Expr::Literal(Value::String("~".into())),
2702 Token::TildePath(s) => Expr::Literal(Value::String(s)),
2703 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
2704 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
2705 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
2707 Token::DashNumWord(s) => Expr::Literal(Value::String(s)),
2710 Token::AtWord(s) => Expr::Literal(Value::String(s)),
2712 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
2717 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
2720 },
2721 plus_minus_bare,
2722 keyword_as_bareword,
2724 ))
2725 .labelled("expression")
2726 })
2727 .boxed()
2728}
2729
2730fn var_expr_parser<'tokens, I>(
2733) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2734where
2735 I: ValueInput<'tokens, Token = Token, Span = Span>,
2736{
2737 choice((
2738 select! { Token::VarRef(raw) => raw }.try_map(|raw, span| {
2739 let inner = raw
2743 .strip_prefix("${")
2744 .and_then(|s| s.strip_suffix('}'))
2745 .unwrap_or(&raw);
2746 if !raw.starts_with("${?}")
2747 && !raw.starts_with("${$}")
2748 && find_default_separator(&raw).is_none()
2749 && let Some(msg) = bash_substring_hint(inner)
2750 {
2751 return Err(Rich::custom(span, msg));
2752 }
2753 Ok(parse_var_expr(&raw))
2754 }),
2755 select! { Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)) },
2756 ))
2757 .labelled("variable reference")
2758}
2759
2760fn cmd_subst_parser<'tokens, I, E>(
2764 expr: E,
2765) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2766where
2767 I: ValueInput<'tokens, Token = Token, Span = Span>,
2768 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2769{
2770 let long_flag_with_value = select! {
2773 Token::LongFlag(name) => name,
2774 }
2775 .then_ignore(just(Token::Eq))
2776 .then(expr.clone())
2777 .map(|(key, value)| Arg::Named { key, value });
2778
2779 let long_flag = select! {
2781 Token::LongFlag(name) => Arg::LongFlag(name),
2782 };
2783
2784 let short_flag = select! {
2786 Token::ShortFlag(name) => Arg::ShortFlag(name),
2787 };
2788
2789 let named = choice((ident_parser(), keyword_word()))
2792 .then_ignore(just(Token::Eq))
2793 .then(expr.clone())
2794 .map(|(key, value)| Arg::WordAssign { key, value });
2795
2796 let positional = expr.clone().map(Arg::Positional);
2798
2799 let arg = choice((
2800 long_flag_with_value,
2801 long_flag,
2802 short_flag,
2803 named,
2804 positional,
2805 ));
2806
2807 let command_name = choice((
2809 ident_parser(),
2810 just(Token::True).to("true".to_string()),
2811 just(Token::False).to("false".to_string()),
2812 ));
2813
2814 let command = command_name
2821 .then(arg.repeated().collect::<Vec<_>>())
2822 .then(
2823 redirect_parser(expr.clone())
2824 .repeated()
2825 .collect::<Vec<_>>(),
2826 )
2827 .map(|((name, args), redirects)| Command {
2828 name,
2829 args,
2830 redirects,
2831 });
2832
2833 let pipeline = command
2835 .separated_by(just(Token::Pipe))
2836 .at_least(1)
2837 .collect::<Vec<_>>()
2838 .map(|commands| Pipeline {
2839 commands,
2840 background: false,
2841 });
2842
2843 let pipeline_stmt = pipeline.map(pipeline_into_stmt);
2846
2847 let chained = pipeline_stmt.clone().foldl(
2855 choice((
2856 just(Token::And).to(true), just(Token::Or).to(false), ))
2859 .then(pipeline_stmt.clone())
2860 .repeated(),
2861 |left, (is_and, right): (bool, Stmt)| {
2862 if is_and {
2863 Stmt::AndChain {
2864 left: Box::new(left),
2865 right: Box::new(right),
2866 }
2867 } else {
2868 Stmt::OrChain {
2869 left: Box::new(left),
2870 right: Box::new(right),
2871 }
2872 }
2873 },
2874 );
2875
2876 let separator = choice((just(Token::Newline), just(Token::Semi)));
2881 let body = separator
2882 .clone()
2883 .repeated()
2884 .ignore_then(
2885 chained
2886 .separated_by(separator.clone().repeated().at_least(1))
2887 .allow_trailing()
2888 .collect::<Vec<_>>(),
2889 )
2890 .then_ignore(separator.repeated());
2891
2892 just(Token::CmdSubstStart)
2893 .ignore_then(body)
2894 .then_ignore(just(Token::RParen))
2895 .map(Expr::CommandSubst)
2896 .labelled("command substitution")
2897}
2898
2899fn interpolated_string_parser<'tokens, I>(
2901) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2902where
2903 I: ValueInput<'tokens, Token = Token, Span = Span>,
2904{
2905 let double_quoted = select! {
2907 Token::String(s) => s,
2908 }
2909 .try_map(|s, span| {
2910 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
2912 let parts = parse_interpolated_string(&s)
2915 .map_err(|msg| Rich::custom(span, msg))?;
2916 if parts.len() == 1
2917 && let StringPart::Literal(text) = &parts[0] {
2918 return Ok(Expr::Literal(Value::String(text.clone())));
2919 }
2920 Ok(Expr::Interpolated(parts))
2921 } else {
2922 Ok(Expr::Literal(Value::String(s)))
2923 }
2924 });
2925
2926 let single_quoted = select! {
2928 Token::SingleString(s) => Expr::Literal(Value::String(s)),
2929 };
2930
2931 choice((single_quoted, double_quoted)).labelled("string")
2932}
2933
2934fn literal_parser<'tokens, I>(
2936) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2937where
2938 I: ValueInput<'tokens, Token = Token, Span = Span>,
2939{
2940 choice((
2941 select! {
2942 Token::True => Value::Bool(true),
2943 Token::False => Value::Bool(false),
2944 },
2945 select! {
2946 Token::Int(n) => Value::Int(n),
2947 Token::Float(f) => Value::Float(f),
2948 },
2949 ))
2950 .labelled("literal")
2951 .boxed()
2952}
2953
2954fn ident_parser<'tokens, I>(
2956) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2957where
2958 I: ValueInput<'tokens, Token = Token, Span = Span>,
2959{
2960 select! {
2961 Token::Ident(s) => s,
2962 }
2963 .labelled("identifier")
2964}
2965
2966fn path_parser<'tokens, I>(
2968) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2969where
2970 I: ValueInput<'tokens, Token = Token, Span = Span>,
2971{
2972 select! {
2973 Token::Path(s) => s,
2974 }
2975 .labelled("path")
2976}
2977
2978#[cfg(test)]
2979#[allow(clippy::approx_constant)]
2980mod tests {
2981 use super::*;
2982
2983 fn subst_cmd(expr: &Expr) -> &Command {
2985 match expr {
2986 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2987 [Stmt::Command(cmd)] => cmd,
2988 other => panic!("expected a single command in $(), got {other:?}"),
2989 },
2990 other => panic!("expected command subst, got {other:?}"),
2991 }
2992 }
2993
2994 fn subst_pipeline(expr: &Expr) -> &Pipeline {
2996 match expr {
2997 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2998 [Stmt::Pipeline(p)] => p,
2999 other => panic!("expected a single pipeline in $(), got {other:?}"),
3000 },
3001 other => panic!("expected command subst, got {other:?}"),
3002 }
3003 }
3004
3005 #[test]
3006 fn parse_empty() {
3007 let result = parse("");
3008 assert!(result.is_ok());
3009 assert_eq!(result.expect("ok").statements.len(), 0);
3010 }
3011
3012 #[test]
3013 fn parse_newlines_only() {
3014 let result = parse("\n\n\n");
3015 assert!(result.is_ok());
3016 }
3017
3018 #[test]
3019 fn parse_simple_command() {
3020 let result = parse("echo");
3021 assert!(result.is_ok());
3022 let program = result.expect("ok");
3023 assert_eq!(program.statements.len(), 1);
3024 assert!(matches!(&program.statements[0], Stmt::Command(_)));
3025 }
3026
3027 #[test]
3028 fn parse_command_with_string_arg() {
3029 let result = parse(r#"echo "hello""#);
3030 assert!(result.is_ok());
3031 let program = result.expect("ok");
3032 match &program.statements[0] {
3033 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
3034 _ => panic!("expected Command"),
3035 }
3036 }
3037
3038 #[test]
3039 fn parse_assignment() {
3040 let result = parse("X=5");
3041 assert!(result.is_ok());
3042 let program = result.expect("ok");
3043 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
3044 }
3045
3046 #[test]
3047 fn parse_pipeline() {
3048 let result = parse("a | b | c");
3049 assert!(result.is_ok());
3050 let program = result.expect("ok");
3051 match &program.statements[0] {
3052 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
3053 _ => panic!("expected Pipeline"),
3054 }
3055 }
3056
3057 #[test]
3058 fn parse_background_job() {
3059 let result = parse("cmd &");
3060 assert!(result.is_ok());
3061 let program = result.expect("ok");
3062 match &program.statements[0] {
3063 Stmt::Pipeline(p) => assert!(p.background),
3064 _ => panic!("expected Pipeline with background"),
3065 }
3066 }
3067
3068 #[test]
3069 fn parse_if_simple() {
3070 let result = parse("if true; then echo; fi");
3071 assert!(result.is_ok());
3072 let program = result.expect("ok");
3073 assert!(matches!(&program.statements[0], Stmt::If(_)));
3074 }
3075
3076 #[test]
3077 fn parse_if_else() {
3078 let result = parse("if true; then echo; else echo; fi");
3079 assert!(result.is_ok());
3080 let program = result.expect("ok");
3081 match &program.statements[0] {
3082 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
3083 _ => panic!("expected If"),
3084 }
3085 }
3086
3087 #[test]
3088 fn parse_elif_simple() {
3089 let result = parse("if true; then echo a; elif false; then echo b; fi");
3090 assert!(result.is_ok(), "parse failed: {:?}", result);
3091 let program = result.expect("ok");
3092 match &program.statements[0] {
3093 Stmt::If(if_stmt) => {
3094 assert!(if_stmt.else_branch.is_some());
3096 let else_branch = if_stmt.else_branch.as_ref().unwrap();
3097 assert_eq!(else_branch.len(), 1);
3098 assert!(matches!(&else_branch[0], Stmt::If(_)));
3099 }
3100 _ => panic!("expected If"),
3101 }
3102 }
3103
3104 #[test]
3105 fn parse_elif_with_else() {
3106 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
3107 assert!(result.is_ok(), "parse failed: {:?}", result);
3108 let program = result.expect("ok");
3109 match &program.statements[0] {
3110 Stmt::If(outer_if) => {
3111 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
3113 assert_eq!(else_branch.len(), 1);
3114 match &else_branch[0] {
3115 Stmt::If(inner_if) => {
3116 assert!(inner_if.else_branch.is_some());
3118 }
3119 _ => panic!("expected nested If from elif"),
3120 }
3121 }
3122 _ => panic!("expected If"),
3123 }
3124 }
3125
3126 #[test]
3127 fn parse_multiple_elif() {
3128 let result = parse(
3130 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
3131 );
3132 assert!(result.is_ok(), "parse failed: {:?}", result);
3133 }
3134
3135 #[test]
3136 fn parse_for_loop() {
3137 let result = parse("for X in items; do echo; done");
3138 assert!(result.is_ok());
3139 let program = result.expect("ok");
3140 assert!(matches!(&program.statements[0], Stmt::For(_)));
3141 }
3142
3143 #[test]
3144 fn parse_brackets_not_array_literal() {
3145 let result = parse("cmd [1");
3147 let _ = result;
3150 }
3151
3152 #[test]
3153 fn parse_named_arg() {
3154 let result = parse("cmd foo=5");
3158 assert!(result.is_ok());
3159 let program = result.expect("ok");
3160 match &program.statements[0] {
3161 Stmt::Command(cmd) => {
3162 assert_eq!(cmd.args.len(), 1);
3163 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
3164 }
3165 _ => panic!("expected Command"),
3166 }
3167 }
3168
3169 #[test]
3170 fn parse_short_flag() {
3171 let result = parse("ls -l");
3172 assert!(result.is_ok());
3173 let program = result.expect("ok");
3174 match &program.statements[0] {
3175 Stmt::Command(cmd) => {
3176 assert_eq!(cmd.name, "ls");
3177 assert_eq!(cmd.args.len(), 1);
3178 match &cmd.args[0] {
3179 Arg::ShortFlag(name) => assert_eq!(name, "l"),
3180 _ => panic!("expected ShortFlag"),
3181 }
3182 }
3183 _ => panic!("expected Command"),
3184 }
3185 }
3186
3187 #[test]
3188 fn parse_long_flag() {
3189 let result = parse("git push --force");
3190 assert!(result.is_ok());
3191 let program = result.expect("ok");
3192 match &program.statements[0] {
3193 Stmt::Command(cmd) => {
3194 assert_eq!(cmd.name, "git");
3195 assert_eq!(cmd.args.len(), 2);
3196 match &cmd.args[0] {
3197 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
3198 _ => panic!("expected Positional push"),
3199 }
3200 match &cmd.args[1] {
3201 Arg::LongFlag(name) => assert_eq!(name, "force"),
3202 _ => panic!("expected LongFlag"),
3203 }
3204 }
3205 _ => panic!("expected Command"),
3206 }
3207 }
3208
3209 #[test]
3210 fn parse_long_flag_with_value() {
3211 let result = parse(r#"git commit --message="hello""#);
3212 assert!(result.is_ok());
3213 let program = result.expect("ok");
3214 match &program.statements[0] {
3215 Stmt::Command(cmd) => {
3216 assert_eq!(cmd.name, "git");
3217 assert_eq!(cmd.args.len(), 2);
3218 match &cmd.args[1] {
3219 Arg::Named { key, value } => {
3220 assert_eq!(key, "message");
3221 match value {
3222 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
3223 _ => panic!("expected String value"),
3224 }
3225 }
3226 _ => panic!("expected Named from --flag=value"),
3227 }
3228 }
3229 _ => panic!("expected Command"),
3230 }
3231 }
3232
3233 #[test]
3234 fn parse_mixed_flags_and_args() {
3235 let result = parse(r#"git commit -m "message" --amend"#);
3236 assert!(result.is_ok());
3237 let program = result.expect("ok");
3238 match &program.statements[0] {
3239 Stmt::Command(cmd) => {
3240 assert_eq!(cmd.name, "git");
3241 assert_eq!(cmd.args.len(), 4);
3242 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
3244 match &cmd.args[1] {
3246 Arg::ShortFlag(name) => assert_eq!(name, "m"),
3247 _ => panic!("expected ShortFlag -m"),
3248 }
3249 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
3251 match &cmd.args[3] {
3253 Arg::LongFlag(name) => assert_eq!(name, "amend"),
3254 _ => panic!("expected LongFlag --amend"),
3255 }
3256 }
3257 _ => panic!("expected Command"),
3258 }
3259 }
3260
3261 #[test]
3262 fn parse_redirect_stdout() {
3263 let result = parse("cmd > file");
3264 assert!(result.is_ok());
3265 let program = result.expect("ok");
3266 match &program.statements[0] {
3268 Stmt::Pipeline(p) => {
3269 assert_eq!(p.commands.len(), 1);
3270 let cmd = &p.commands[0];
3271 assert_eq!(cmd.redirects.len(), 1);
3272 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
3273 }
3274 _ => panic!("expected Pipeline"),
3275 }
3276 }
3277
3278 #[test]
3279 fn parse_var_ref() {
3280 let result = parse("echo ${VAR}");
3281 assert!(result.is_ok());
3282 let program = result.expect("ok");
3283 match &program.statements[0] {
3284 Stmt::Command(cmd) => {
3285 assert_eq!(cmd.args.len(), 1);
3286 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
3287 }
3288 _ => panic!("expected Command"),
3289 }
3290 }
3291
3292 #[test]
3293 fn parse_multiple_statements() {
3294 let result = parse("a\nb\nc");
3295 assert!(result.is_ok());
3296 let program = result.expect("ok");
3297 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3298 assert_eq!(non_empty.len(), 3);
3299 }
3300
3301 #[test]
3302 fn parse_semicolon_separated() {
3303 let result = parse("a; b; c");
3304 assert!(result.is_ok());
3305 let program = result.expect("ok");
3306 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3307 assert_eq!(non_empty.len(), 3);
3308 }
3309
3310 #[test]
3311 fn parse_complex_pipeline() {
3312 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
3313 assert!(result.is_ok());
3314 let program = result.expect("ok");
3315 match &program.statements[0] {
3316 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
3317 _ => panic!("expected Pipeline"),
3318 }
3319 }
3320
3321 #[test]
3322 fn parse_json_as_string_arg() {
3323 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
3325 assert!(result.is_ok());
3326 }
3327
3328 #[test]
3329 fn parse_mixed_args() {
3330 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
3331 assert!(result.is_ok());
3332 let program = result.expect("ok");
3333 match &program.statements[0] {
3334 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
3335 _ => panic!("expected Command"),
3336 }
3337 }
3338
3339 #[test]
3340 fn error_unterminated_string() {
3341 let result = parse(r#"echo "hello"#);
3342 assert!(result.is_err());
3343 }
3344
3345 #[test]
3346 fn error_unterminated_var_ref() {
3347 let result = parse("echo ${VAR");
3348 assert!(result.is_err());
3349 }
3350
3351 #[test]
3352 fn error_missing_fi() {
3353 let result = parse("if true; then echo");
3354 assert!(result.is_err());
3355 }
3356
3357 #[test]
3358 fn error_missing_done() {
3359 let result = parse("for X in items; do echo");
3360 assert!(result.is_err());
3361 }
3362
3363 #[test]
3364 fn parse_lvalue_single_index() {
3365 let result = parse("xs[0]=9").unwrap();
3366 match &result.statements[0] {
3367 Stmt::Assignment(a) => {
3368 assert_eq!(a.name(), "xs");
3369 assert_eq!(
3370 a.path.segments,
3371 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
3372 );
3373 assert!(!a.local);
3374 }
3375 other => panic!("expected assignment, got {:?}", other),
3376 }
3377 }
3378
3379 #[test]
3380 fn parse_lvalue_negative_index() {
3381 let result = parse("xs[-1]=7").unwrap();
3382 match &result.statements[0] {
3383 Stmt::Assignment(a) => assert_eq!(
3384 a.path.segments,
3385 vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)]
3386 ),
3387 other => panic!("expected assignment, got {:?}", other),
3388 }
3389 }
3390
3391 #[test]
3392 fn parse_lvalue_bareword_key() {
3393 let result = parse("user[email]=x").unwrap();
3394 match &result.statements[0] {
3395 Stmt::Assignment(a) => assert_eq!(
3396 a.path.segments,
3397 vec![
3398 VarSegment::Field("user".into()),
3399 VarSegment::Key("email".into())
3400 ]
3401 ),
3402 other => panic!("expected assignment, got {:?}", other),
3403 }
3404 }
3405
3406 #[test]
3407 fn parse_lvalue_chained_keys() {
3408 let result = parse("s[web][port]=9000").unwrap();
3409 match &result.statements[0] {
3410 Stmt::Assignment(a) => assert_eq!(
3411 a.path.segments,
3412 vec![
3413 VarSegment::Field("s".into()),
3414 VarSegment::Key("web".into()),
3415 VarSegment::Key("port".into())
3416 ]
3417 ),
3418 other => panic!("expected assignment, got {:?}", other),
3419 }
3420 }
3421
3422 #[test]
3423 fn parse_lvalue_dynamic_key() {
3424 let result = parse("r[$k]=v").unwrap();
3425 match &result.statements[0] {
3426 Stmt::Assignment(a) => assert_eq!(
3427 a.path.segments,
3428 vec![
3429 VarSegment::Field("r".into()),
3430 VarSegment::Dynamic("k".into())
3431 ]
3432 ),
3433 other => panic!("expected assignment, got {:?}", other),
3434 }
3435 }
3436
3437 #[test]
3438 fn parse_local_lvalue_spaced() {
3439 let result = parse("local xs[0] = 9").unwrap();
3440 match &result.statements[0] {
3441 Stmt::Assignment(a) => {
3442 assert!(a.local);
3443 assert_eq!(
3444 a.path.segments,
3445 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
3446 );
3447 }
3448 other => panic!("expected assignment, got {:?}", other),
3449 }
3450 }
3451
3452 #[test]
3453 fn env_prefix_subscripted_target_is_not_captured_as_env_scoped() {
3454 let result = parse("user={}\nuser[email]=x echo hi").unwrap();
3464 for stmt in &result.statements {
3465 assert!(
3466 !matches!(stmt, Stmt::EnvScoped { .. }),
3467 "a subscripted assignment must never be captured into EnvScoped: {stmt:?}"
3468 );
3469 }
3470 assert!(matches!(&result.statements[1], Stmt::Assignment(a) if a.name() == "user"));
3472 assert!(matches!(&result.statements[2], Stmt::Command(c) if c.name == "echo"));
3473 }
3474
3475 #[test]
3476 fn parse_nested_cmd_subst() {
3477 let result = parse("X=$(echo $(date))").unwrap();
3479 match &result.statements[0] {
3480 Stmt::Assignment(a) => {
3481 assert_eq!(a.name(), "X");
3482 let outer = subst_cmd(&a.value);
3483 assert_eq!(outer.name, "echo");
3484 match &outer.args[0] {
3486 Arg::Positional(inner_expr) => {
3487 assert_eq!(subst_cmd(inner_expr).name, "date");
3488 }
3489 other => panic!("expected nested cmd subst arg, got {:?}", other),
3490 }
3491 }
3492 other => panic!("expected assignment, got {:?}", other),
3493 }
3494 }
3495
3496 #[test]
3497 fn parse_deeply_nested_cmd_subst() {
3498 let result = parse("X=$(a $(b $(c)))").unwrap();
3500 match &result.statements[0] {
3501 Stmt::Assignment(a) => {
3502 let level1 = subst_cmd(&a.value);
3503 assert_eq!(level1.name, "a");
3504 match &level1.args[0] {
3505 Arg::Positional(level2_expr) => {
3506 let level2 = subst_cmd(level2_expr);
3507 assert_eq!(level2.name, "b");
3508 match &level2.args[0] {
3509 Arg::Positional(level3_expr) => {
3510 assert_eq!(subst_cmd(level3_expr).name, "c");
3511 }
3512 other => panic!("expected level3 cmd subst, got {:?}", other),
3513 }
3514 }
3515 other => panic!("expected level2 cmd subst, got {:?}", other),
3516 }
3517 }
3518 other => panic!("expected assignment, got {:?}", other),
3519 }
3520 }
3521
3522 #[test]
3527 fn value_int_preserved() {
3528 let result = parse("X=42").unwrap();
3529 match &result.statements[0] {
3530 Stmt::Assignment(a) => {
3531 assert_eq!(a.name(), "X");
3532 match &a.value {
3533 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
3534 other => panic!("expected int literal, got {:?}", other),
3535 }
3536 }
3537 other => panic!("expected assignment, got {:?}", other),
3538 }
3539 }
3540
3541 #[test]
3542 fn value_negative_int_preserved() {
3543 let result = parse("X=-99").unwrap();
3544 match &result.statements[0] {
3545 Stmt::Assignment(a) => match &a.value {
3546 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
3547 other => panic!("expected int, got {:?}", other),
3548 },
3549 other => panic!("expected assignment, got {:?}", other),
3550 }
3551 }
3552
3553 #[test]
3554 fn value_float_preserved() {
3555 let result = parse("PI=3.14").unwrap();
3556 match &result.statements[0] {
3557 Stmt::Assignment(a) => match &a.value {
3558 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
3559 other => panic!("expected float, got {:?}", other),
3560 },
3561 other => panic!("expected assignment, got {:?}", other),
3562 }
3563 }
3564
3565 #[test]
3566 fn value_string_preserved() {
3567 let result = parse(r#"echo "hello world""#).unwrap();
3568 match &result.statements[0] {
3569 Stmt::Command(cmd) => {
3570 assert_eq!(cmd.name, "echo");
3571 match &cmd.args[0] {
3572 Arg::Positional(Expr::Literal(Value::String(s))) => {
3573 assert_eq!(s, "hello world");
3574 }
3575 other => panic!("expected string arg, got {:?}", other),
3576 }
3577 }
3578 other => panic!("expected command, got {:?}", other),
3579 }
3580 }
3581
3582 #[test]
3583 fn value_string_with_escapes_preserved() {
3584 let result = parse(r#"echo "line1\nline2""#).unwrap();
3585 match &result.statements[0] {
3586 Stmt::Command(cmd) => match &cmd.args[0] {
3587 Arg::Positional(Expr::Literal(Value::String(s))) => {
3588 assert_eq!(s, "line1\nline2");
3589 }
3590 other => panic!("expected string, got {:?}", other),
3591 },
3592 other => panic!("expected command, got {:?}", other),
3593 }
3594 }
3595
3596 #[test]
3597 fn value_command_name_preserved() {
3598 let result = parse("my-command").unwrap();
3599 match &result.statements[0] {
3600 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
3601 other => panic!("expected command, got {:?}", other),
3602 }
3603 }
3604
3605 #[test]
3606 fn value_assignment_name_preserved() {
3607 let result = parse("MY_VAR=1").unwrap();
3608 match &result.statements[0] {
3609 Stmt::Assignment(a) => assert_eq!(a.name(), "MY_VAR"),
3610 other => panic!("expected assignment, got {:?}", other),
3611 }
3612 }
3613
3614 #[test]
3615 fn value_for_variable_preserved() {
3616 let result = parse("for ITEM in items; do echo; done").unwrap();
3617 match &result.statements[0] {
3618 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
3619 other => panic!("expected for, got {:?}", other),
3620 }
3621 }
3622
3623 #[test]
3624 fn value_varref_name_preserved() {
3625 let result = parse("echo ${MESSAGE}").unwrap();
3626 match &result.statements[0] {
3627 Stmt::Command(cmd) => match &cmd.args[0] {
3628 Arg::Positional(Expr::VarRef(path)) => {
3629 assert_eq!(path.segments.len(), 1);
3630 let VarSegment::Field(name) = &path.segments[0] else {
3631 panic!("expected root field, got {:?}", path.segments[0]);
3632 };
3633 assert_eq!(name, "MESSAGE");
3634 }
3635 other => panic!("expected varref, got {:?}", other),
3636 },
3637 other => panic!("expected command, got {:?}", other),
3638 }
3639 }
3640
3641 #[test]
3642 fn value_varref_field_access_preserved() {
3643 let result = parse("echo ${RESULT.data}").unwrap();
3644 match &result.statements[0] {
3645 Stmt::Command(cmd) => match &cmd.args[0] {
3646 Arg::Positional(Expr::VarRef(path)) => {
3647 assert_eq!(path.segments.len(), 2);
3651 let VarSegment::Field(a) = &path.segments[0] else {
3652 panic!("expected field, got {:?}", path.segments[0]);
3653 };
3654 let VarSegment::Field(b) = &path.segments[1] else {
3655 panic!("expected field, got {:?}", path.segments[1]);
3656 };
3657 assert_eq!(a, "RESULT");
3658 assert_eq!(b, "data");
3659 }
3660 other => panic!("expected varref, got {:?}", other),
3661 },
3662 other => panic!("expected command, got {:?}", other),
3663 }
3664 }
3665
3666 #[test]
3667 fn value_varref_index_parsed() {
3668 let result = parse("echo ${ITEMS[0]}").unwrap();
3671 match &result.statements[0] {
3672 Stmt::Command(cmd) => match &cmd.args[0] {
3673 Arg::Positional(Expr::VarRef(path)) => {
3674 assert_eq!(path.segments.len(), 2);
3675 let VarSegment::Field(name) = &path.segments[0] else {
3676 panic!("expected root field, got {:?}", path.segments[0]);
3677 };
3678 assert_eq!(name, "ITEMS");
3679 assert_eq!(path.segments[1], VarSegment::Index(0));
3680 }
3681 other => panic!("expected varref, got {:?}", other),
3682 },
3683 other => panic!("expected command, got {:?}", other),
3684 }
3685 }
3686
3687 #[test]
3688 fn value_named_arg_preserved() {
3689 let result = parse("cmd count=42").unwrap();
3693 match &result.statements[0] {
3694 Stmt::Command(cmd) => {
3695 assert_eq!(cmd.name, "cmd");
3696 match &cmd.args[0] {
3697 Arg::WordAssign { key, value } => {
3698 assert_eq!(key, "count");
3699 match value {
3700 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
3701 other => panic!("expected int, got {:?}", other),
3702 }
3703 }
3704 other => panic!("expected WordAssign arg, got {:?}", other),
3705 }
3706 }
3707 other => panic!("expected command, got {:?}", other),
3708 }
3709 }
3710
3711 #[test]
3712 fn value_function_def_name_preserved() {
3713 let result = parse("greet() { echo }").unwrap();
3714 match &result.statements[0] {
3715 Stmt::ToolDef(t) => {
3716 assert_eq!(t.name, "greet");
3717 assert!(t.params.is_empty());
3718 }
3719 other => panic!("expected function def, got {:?}", other),
3720 }
3721 }
3722
3723 #[test]
3728 fn parse_comparison_equals() {
3729 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
3731 match &result.statements[0] {
3732 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3733 Expr::Test(test) => match test.as_ref() {
3734 TestExpr::Comparison { left, op, right } => {
3735 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3736 assert_eq!(*op, TestCmpOp::Eq);
3737 match right.as_ref() {
3738 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
3739 other => panic!("expected int, got {:?}", other),
3740 }
3741 }
3742 other => panic!("expected comparison, got {:?}", other),
3743 },
3744 other => panic!("expected test expr, got {:?}", other),
3745 },
3746 other => panic!("expected if, got {:?}", other),
3747 }
3748 }
3749
3750 #[test]
3751 fn parse_comparison_not_equals() {
3752 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
3753 match &result.statements[0] {
3754 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3755 Expr::Test(test) => match test.as_ref() {
3756 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
3757 other => panic!("expected comparison, got {:?}", other),
3758 },
3759 other => panic!("expected test expr, got {:?}", other),
3760 },
3761 other => panic!("expected if, got {:?}", other),
3762 }
3763 }
3764
3765 #[test]
3766 fn parse_comparison_less_than() {
3767 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
3768 match &result.statements[0] {
3769 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3770 Expr::Test(test) => match test.as_ref() {
3771 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
3772 other => panic!("expected comparison, got {:?}", other),
3773 },
3774 other => panic!("expected test expr, got {:?}", other),
3775 },
3776 other => panic!("expected if, got {:?}", other),
3777 }
3778 }
3779
3780 #[test]
3781 fn parse_comparison_greater_than() {
3782 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
3783 match &result.statements[0] {
3784 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3785 Expr::Test(test) => match test.as_ref() {
3786 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
3787 other => panic!("expected comparison, got {:?}", other),
3788 },
3789 other => panic!("expected test expr, got {:?}", other),
3790 },
3791 other => panic!("expected if, got {:?}", other),
3792 }
3793 }
3794
3795 #[test]
3796 fn parse_comparison_less_equal() {
3797 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
3798 match &result.statements[0] {
3799 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3800 Expr::Test(test) => match test.as_ref() {
3801 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
3802 other => panic!("expected comparison, got {:?}", other),
3803 },
3804 other => panic!("expected test expr, got {:?}", other),
3805 },
3806 other => panic!("expected if, got {:?}", other),
3807 }
3808 }
3809
3810 #[test]
3811 fn parse_comparison_greater_equal() {
3812 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
3813 match &result.statements[0] {
3814 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3815 Expr::Test(test) => match test.as_ref() {
3816 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
3817 other => panic!("expected comparison, got {:?}", other),
3818 },
3819 other => panic!("expected test expr, got {:?}", other),
3820 },
3821 other => panic!("expected if, got {:?}", other),
3822 }
3823 }
3824
3825 #[test]
3826 fn parse_regex_match() {
3827 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
3828 match &result.statements[0] {
3829 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3830 Expr::Test(test) => match test.as_ref() {
3831 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
3832 other => panic!("expected comparison, got {:?}", other),
3833 },
3834 other => panic!("expected test expr, got {:?}", other),
3835 },
3836 other => panic!("expected if, got {:?}", other),
3837 }
3838 }
3839
3840 #[test]
3841 fn parse_regex_not_match() {
3842 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
3843 match &result.statements[0] {
3844 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3845 Expr::Test(test) => match test.as_ref() {
3846 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
3847 other => panic!("expected comparison, got {:?}", other),
3848 },
3849 other => panic!("expected test expr, got {:?}", other),
3850 },
3851 other => panic!("expected if, got {:?}", other),
3852 }
3853 }
3854
3855 #[test]
3856 fn parse_string_interpolation() {
3857 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
3858 match &result.statements[0] {
3859 Stmt::Command(cmd) => match &cmd.args[0] {
3860 Arg::Positional(Expr::Interpolated(parts)) => {
3861 assert_eq!(parts.len(), 3);
3862 match &parts[0] {
3863 StringPart::Literal(s) => assert_eq!(s, "Hello "),
3864 other => panic!("expected literal, got {:?}", other),
3865 }
3866 match &parts[1] {
3867 StringPart::Var(path) => {
3868 assert_eq!(path.segments.len(), 1);
3869 let VarSegment::Field(name) = &path.segments[0] else {
3870 panic!("expected root field, got {:?}", path.segments[0]);
3871 };
3872 assert_eq!(name, "NAME");
3873 }
3874 other => panic!("expected var, got {:?}", other),
3875 }
3876 match &parts[2] {
3877 StringPart::Literal(s) => assert_eq!(s, "!"),
3878 other => panic!("expected literal, got {:?}", other),
3879 }
3880 }
3881 other => panic!("expected interpolated, got {:?}", other),
3882 },
3883 other => panic!("expected command, got {:?}", other),
3884 }
3885 }
3886
3887 #[test]
3888 fn parse_string_interpolation_multiple_vars() {
3889 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
3890 match &result.statements[0] {
3891 Stmt::Command(cmd) => match &cmd.args[0] {
3892 Arg::Positional(Expr::Interpolated(parts)) => {
3893 assert_eq!(parts.len(), 3);
3895 assert!(matches!(&parts[0], StringPart::Var(_)));
3896 assert!(matches!(&parts[1], StringPart::Literal(_)));
3897 assert!(matches!(&parts[2], StringPart::Var(_)));
3898 }
3899 other => panic!("expected interpolated, got {:?}", other),
3900 },
3901 other => panic!("expected command, got {:?}", other),
3902 }
3903 }
3904
3905 #[test]
3906 fn parse_empty_function_body() {
3907 let result = parse("empty() { }").unwrap();
3908 match &result.statements[0] {
3909 Stmt::ToolDef(t) => {
3910 assert_eq!(t.name, "empty");
3911 assert!(t.params.is_empty());
3912 assert!(t.body.is_empty());
3913 }
3914 other => panic!("expected function def, got {:?}", other),
3915 }
3916 }
3917
3918 #[test]
3919 fn parse_bash_style_function() {
3920 let result = parse("function greet { echo hello }").unwrap();
3921 match &result.statements[0] {
3922 Stmt::ToolDef(t) => {
3923 assert_eq!(t.name, "greet");
3924 assert!(t.params.is_empty());
3925 assert_eq!(t.body.len(), 1);
3926 }
3927 other => panic!("expected function def, got {:?}", other),
3928 }
3929 }
3930
3931 #[test]
3932 fn parse_comparison_string_values() {
3933 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
3934 match &result.statements[0] {
3935 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3936 Expr::Test(test) => match test.as_ref() {
3937 TestExpr::Comparison { left, op, right } => {
3938 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3939 assert_eq!(*op, TestCmpOp::Eq);
3940 match right.as_ref() {
3941 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
3942 other => panic!("expected string, got {:?}", other),
3943 }
3944 }
3945 other => panic!("expected comparison, got {:?}", other),
3946 },
3947 other => panic!("expected test expr, got {:?}", other),
3948 },
3949 other => panic!("expected if, got {:?}", other),
3950 }
3951 }
3952
3953 #[test]
3958 fn parse_cmd_subst_simple() {
3959 let result = parse("X=$(echo)").unwrap();
3960 match &result.statements[0] {
3961 Stmt::Assignment(a) => {
3962 assert_eq!(a.name(), "X");
3963 assert_eq!(subst_cmd(&a.value).name, "echo");
3964 }
3965 other => panic!("expected assignment, got {:?}", other),
3966 }
3967 }
3968
3969 #[test]
3970 fn parse_cmd_subst_with_args() {
3971 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
3972 match &result.statements[0] {
3973 Stmt::Assignment(a) => {
3974 let cmd = subst_cmd(&a.value);
3975 assert_eq!(cmd.name, "fetch");
3976 assert_eq!(cmd.args.len(), 1);
3977 match &cmd.args[0] {
3978 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
3979 other => panic!("expected WordAssign arg, got {:?}", other),
3980 }
3981 }
3982 other => panic!("expected assignment, got {:?}", other),
3983 }
3984 }
3985
3986 #[test]
3987 fn parse_cmd_subst_pipeline() {
3988 let result = parse("X=$(cat file | grep pattern)").unwrap();
3989 match &result.statements[0] {
3990 Stmt::Assignment(a) => {
3991 let pipeline = subst_pipeline(&a.value);
3992 assert_eq!(pipeline.commands.len(), 2);
3993 assert_eq!(pipeline.commands[0].name, "cat");
3994 assert_eq!(pipeline.commands[1].name, "grep");
3995 }
3996 other => panic!("expected assignment, got {:?}", other),
3997 }
3998 }
3999
4000 #[test]
4001 fn parse_cmd_subst_with_redirect() {
4002 let result = parse("X=$(echo hi > out.txt)").unwrap();
4007 match &result.statements[0] {
4008 Stmt::Assignment(a) => {
4009 let pipeline = subst_pipeline(&a.value);
4010 assert_eq!(pipeline.commands.len(), 1);
4011 let cmd = &pipeline.commands[0];
4012 assert_eq!(cmd.name, "echo");
4013 assert_eq!(cmd.redirects.len(), 1);
4014 assert!(matches!(
4015 cmd.redirects[0].kind,
4016 RedirectKind::StdoutOverwrite
4017 ));
4018 }
4019 other => panic!("expected assignment, got {:?}", other),
4020 }
4021 }
4022
4023 #[test]
4024 fn parse_cmd_subst_redirect_target_with_nested_subst() {
4025 let result = parse("X=$(echo hi > $(echo f))").unwrap();
4031 match &result.statements[0] {
4032 Stmt::Assignment(a) => {
4033 let pipeline = subst_pipeline(&a.value);
4034 assert_eq!(pipeline.commands.len(), 1);
4035 let cmd = &pipeline.commands[0];
4036 assert_eq!(cmd.name, "echo");
4037 assert_eq!(cmd.redirects.len(), 1);
4038 assert!(
4039 matches!(cmd.redirects[0].target, Expr::CommandSubst(_)),
4040 "redirect target should be a nested command substitution, got {:?}",
4041 cmd.redirects[0].target
4042 );
4043 }
4044 other => panic!("expected assignment, got {:?}", other),
4045 }
4046 }
4047
4048 #[test]
4049 fn parse_cmd_subst_chain_with_redirect() {
4050 let result = parse("X=$(echo a && echo b > out.txt)").unwrap();
4054 let stmts = match &result.statements[0] {
4055 Stmt::Assignment(a) => match &a.value {
4056 Expr::CommandSubst(s) => s,
4057 other => panic!("expected command subst, got {:?}", other),
4058 },
4059 other => panic!("expected assignment, got {:?}", other),
4060 };
4061 match stmts.as_slice() {
4062 [Stmt::AndChain { left, right }] => {
4063 assert!(
4065 matches!(**left, Stmt::Command(_)),
4066 "left of && should be a bare command, got {:?}",
4067 left
4068 );
4069 match &**right {
4071 Stmt::Pipeline(p) => {
4072 assert_eq!(p.commands.len(), 1);
4073 assert_eq!(p.commands[0].name, "echo");
4074 assert_eq!(p.commands[0].redirects.len(), 1);
4075 }
4076 other => panic!("right should be a redirect-bearing pipeline, got {:?}", other),
4077 }
4078 }
4079 other => panic!("expected a single AndChain, got {:?}", other),
4080 }
4081 }
4082
4083 #[test]
4084 fn parse_cmd_subst_in_condition() {
4085 let result = parse("if kaish-validate; then echo; fi").unwrap();
4087 match &result.statements[0] {
4088 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4089 Expr::Command(cmd) => {
4090 assert_eq!(cmd.name, "kaish-validate");
4091 }
4092 other => panic!("expected command, got {:?}", other),
4093 },
4094 other => panic!("expected if, got {:?}", other),
4095 }
4096 }
4097
4098 #[test]
4103 fn parse_env_prefix_single() {
4104 let result = parse("FOO=bar echo hi").unwrap();
4105 match &result.statements[0] {
4106 Stmt::EnvScoped { assignments, body } => {
4107 assert_eq!(assignments.len(), 1);
4108 assert_eq!(assignments[0].name(), "FOO");
4109 assert!(!assignments[0].local);
4110 match body.as_ref() {
4111 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
4112 other => panic!("expected command body, got {other:?}"),
4113 }
4114 }
4115 other => panic!("expected env-scoped, got {other:?}"),
4116 }
4117 }
4118
4119 #[test]
4120 fn parse_env_prefix_multiple() {
4121 let result = parse("A=1 B=2 run").unwrap();
4122 match &result.statements[0] {
4123 Stmt::EnvScoped { assignments, body } => {
4124 assert_eq!(assignments.len(), 2);
4125 assert_eq!(assignments[0].name(), "A");
4126 assert_eq!(assignments[1].name(), "B");
4127 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
4128 }
4129 other => panic!("expected env-scoped, got {other:?}"),
4130 }
4131 }
4132
4133 #[test]
4134 fn parse_bare_assignment_is_not_env_scoped() {
4135 let result = parse("FOO=bar").unwrap();
4137 assert!(
4138 matches!(&result.statements[0], Stmt::Assignment(a) if a.name() == "FOO"),
4139 "got {:?}",
4140 result.statements[0]
4141 );
4142 }
4143
4144 #[test]
4145 fn parse_assignment_then_and_chain_does_not_over_capture() {
4146 let result = parse("FOO=bar && echo hi").unwrap();
4149 match &result.statements[0] {
4150 Stmt::AndChain { left, right } => {
4151 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name() == "FOO"));
4152 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
4153 }
4154 other => panic!("expected and-chain, got {other:?}"),
4155 }
4156 }
4157
4158 #[test]
4159 fn parse_env_prefix_pipeline_body() {
4160 let result = parse("FOO=bar cat | grep x").unwrap();
4161 match &result.statements[0] {
4162 Stmt::EnvScoped { assignments, body } => {
4163 assert_eq!(assignments[0].name(), "FOO");
4164 match body.as_ref() {
4165 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 2),
4166 other => panic!("expected pipeline body, got {other:?}"),
4167 }
4168 }
4169 other => panic!("expected env-scoped, got {other:?}"),
4170 }
4171 }
4172
4173 fn parse_err_message(source: &str) -> String {
4178 parse(source)
4179 .expect_err("expected a parse error")
4180 .iter()
4181 .map(|e| e.message.clone())
4182 .collect::<Vec<_>>()
4183 .join(" ")
4184 }
4185
4186 #[test]
4187 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
4188 let msg = parse_err_message("echo /tmp/$(echo x).txt");
4191 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
4192 }
4193
4194 #[test]
4195 fn argv_splat_var_glued_to_path_is_rejected() {
4196 assert!(parse("echo $dir/out.txt").is_err());
4197 }
4198
4199 #[test]
4200 fn argv_splat_three_way_glue_is_rejected() {
4201 assert!(parse("echo foo$(echo bar)baz").is_err());
4202 }
4203
4204 #[test]
4205 fn argv_splat_quoted_word_is_accepted() {
4206 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
4208 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
4209 }
4210
4211 #[test]
4212 fn argv_single_token_words_are_not_splat() {
4213 assert!(parse("echo file.txt").is_ok(), "file.txt");
4215 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
4216 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
4217 }
4218
4219 #[test]
4220 fn argv_spaced_words_are_not_splat() {
4221 assert!(parse("echo a b c").is_ok());
4222 assert!(parse("echo /tmp/x $(echo y)").is_ok());
4223 }
4224
4225 #[test]
4226 fn parse_cmd_subst_in_command_arg() {
4227 let result = parse("echo $(whoami)").unwrap();
4228 match &result.statements[0] {
4229 Stmt::Command(cmd) => {
4230 assert_eq!(cmd.name, "echo");
4231 match &cmd.args[0] {
4232 Arg::Positional(expr) => {
4233 assert_eq!(subst_cmd(expr).name, "whoami");
4234 }
4235 other => panic!("expected command subst, got {:?}", other),
4236 }
4237 }
4238 other => panic!("expected command, got {:?}", other),
4239 }
4240 }
4241
4242 #[test]
4247 fn parse_condition_and() {
4248 let result = parse("if check-a && check-b; then echo; fi").unwrap();
4250 match &result.statements[0] {
4251 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4252 Expr::BinaryOp { left, op, right } => {
4253 assert_eq!(*op, BinaryOp::And);
4254 assert!(matches!(left.as_ref(), Expr::Command(_)));
4255 assert!(matches!(right.as_ref(), Expr::Command(_)));
4256 }
4257 other => panic!("expected binary op, got {:?}", other),
4258 },
4259 other => panic!("expected if, got {:?}", other),
4260 }
4261 }
4262
4263 #[test]
4264 fn parse_condition_or() {
4265 let result = parse("if try-a || try-b; then echo; fi").unwrap();
4266 match &result.statements[0] {
4267 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4268 Expr::BinaryOp { left, op, right } => {
4269 assert_eq!(*op, BinaryOp::Or);
4270 assert!(matches!(left.as_ref(), Expr::Command(_)));
4271 assert!(matches!(right.as_ref(), Expr::Command(_)));
4272 }
4273 other => panic!("expected binary op, got {:?}", other),
4274 },
4275 other => panic!("expected if, got {:?}", other),
4276 }
4277 }
4278
4279 #[test]
4280 fn parse_condition_and_or_precedence() {
4281 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
4283 match &result.statements[0] {
4284 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4285 Expr::BinaryOp { left, op, right } => {
4286 assert_eq!(*op, BinaryOp::Or);
4288 match left.as_ref() {
4290 Expr::BinaryOp { op: inner_op, .. } => {
4291 assert_eq!(*inner_op, BinaryOp::And);
4292 }
4293 other => panic!("expected binary op (&&), got {:?}", other),
4294 }
4295 assert!(matches!(right.as_ref(), Expr::Command(_)));
4297 }
4298 other => panic!("expected binary op, got {:?}", other),
4299 },
4300 other => panic!("expected if, got {:?}", other),
4301 }
4302 }
4303
4304 #[test]
4305 fn parse_condition_multiple_and() {
4306 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
4307 match &result.statements[0] {
4308 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4309 Expr::BinaryOp { left, op, .. } => {
4310 assert_eq!(*op, BinaryOp::And);
4311 match left.as_ref() {
4313 Expr::BinaryOp { op: inner_op, .. } => {
4314 assert_eq!(*inner_op, BinaryOp::And);
4315 }
4316 other => panic!("expected binary op, got {:?}", other),
4317 }
4318 }
4319 other => panic!("expected binary op, got {:?}", other),
4320 },
4321 other => panic!("expected if, got {:?}", other),
4322 }
4323 }
4324
4325 #[test]
4326 fn parse_condition_mixed_comparison_and_logical() {
4327 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
4329 match &result.statements[0] {
4330 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4331 Expr::BinaryOp { left, op, right } => {
4332 assert_eq!(*op, BinaryOp::And);
4333 match left.as_ref() {
4335 Expr::Test(test) => match test.as_ref() {
4336 TestExpr::Comparison { op: left_op, .. } => {
4337 assert_eq!(*left_op, TestCmpOp::Eq);
4338 }
4339 other => panic!("expected comparison, got {:?}", other),
4340 },
4341 other => panic!("expected test, got {:?}", other),
4342 }
4343 match right.as_ref() {
4345 Expr::Test(test) => match test.as_ref() {
4346 TestExpr::Comparison { op: right_op, .. } => {
4347 assert_eq!(*right_op, TestCmpOp::NumGt);
4348 }
4349 other => panic!("expected comparison, got {:?}", other),
4350 },
4351 other => panic!("expected test, got {:?}", other),
4352 }
4353 }
4354 other => panic!("expected binary op, got {:?}", other),
4355 },
4356 other => panic!("expected if, got {:?}", other),
4357 }
4358 }
4359
4360 #[test]
4366 fn script_level1_linear() {
4367 let script = r#"
4368NAME="kaish"
4369VERSION=1
4370TIMEOUT=30
4371ITEMS="alpha beta gamma"
4372
4373echo "Starting ${NAME} v${VERSION}"
4374cat "README.md" | grep pattern="install" | head count=5
4375fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
4376echo "Items: ${ITEMS}"
4377"#;
4378 let result = parse(script).unwrap();
4379 let stmts: Vec<_> = result.statements.iter()
4380 .filter(|s| !matches!(s, Stmt::Empty))
4381 .collect();
4382
4383 assert_eq!(stmts.len(), 8);
4384 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(_))); }
4393
4394 #[test]
4396 fn script_level2_branching() {
4397 let script = r#"
4398RESULT=$(kaish-validate "input.json")
4399
4400if [[ ${RESULT.ok} == true ]]; then
4401 echo "Validation passed"
4402 process "input.json" > "output.json"
4403else
4404 echo "Validation failed: ${RESULT.err}"
4405fi
4406
4407if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
4408 echo "Count in valid range"
4409fi
4410
4411if check-network || check-cache; then
4412 fetch url=${URL}
4413fi
4414"#;
4415 let result = parse(script).unwrap();
4416 let stmts: Vec<_> = result.statements.iter()
4417 .filter(|s| !matches!(s, Stmt::Empty))
4418 .collect();
4419
4420 assert_eq!(stmts.len(), 4);
4421
4422 match stmts[0] {
4424 Stmt::Assignment(a) => {
4425 assert_eq!(a.name(), "RESULT");
4426 assert!(matches!(&a.value, Expr::CommandSubst(_)));
4427 }
4428 other => panic!("expected assignment, got {:?}", other),
4429 }
4430
4431 match stmts[1] {
4433 Stmt::If(if_stmt) => {
4434 assert_eq!(if_stmt.then_branch.len(), 2);
4435 assert!(if_stmt.else_branch.is_some());
4436 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
4437 }
4438 other => panic!("expected if, got {:?}", other),
4439 }
4440
4441 match stmts[2] {
4443 Stmt::If(if_stmt) => {
4444 match if_stmt.condition.as_ref() {
4445 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
4446 other => panic!("expected && condition, got {:?}", other),
4447 }
4448 }
4449 other => panic!("expected if, got {:?}", other),
4450 }
4451
4452 match stmts[3] {
4454 Stmt::If(if_stmt) => {
4455 match if_stmt.condition.as_ref() {
4456 Expr::BinaryOp { op, left, right } => {
4457 assert_eq!(*op, BinaryOp::Or);
4458 assert!(matches!(left.as_ref(), Expr::Command(_)));
4459 assert!(matches!(right.as_ref(), Expr::Command(_)));
4460 }
4461 other => panic!("expected || condition, got {:?}", other),
4462 }
4463 }
4464 other => panic!("expected if, got {:?}", other),
4465 }
4466 }
4467
4468 #[test]
4470 fn script_level3_loops_and_functions() {
4471 let script = r#"
4472greet() {
4473 echo "Hello, $1!"
4474}
4475
4476fetch_all() {
4477 for URL in $@; do
4478 fetch url=${URL}
4479 done
4480}
4481
4482USERS="alice bob charlie"
4483
4484for USER in ${USERS}; do
4485 greet ${USER}
4486 if [[ ${USER} == "bob" ]]; then
4487 echo "Found Bob!"
4488 fi
4489done
4490
4491long-running-task &
4492"#;
4493 let result = parse(script).unwrap();
4494 let stmts: Vec<_> = result.statements.iter()
4495 .filter(|s| !matches!(s, Stmt::Empty))
4496 .collect();
4497
4498 assert_eq!(stmts.len(), 5);
4499
4500 match stmts[0] {
4502 Stmt::ToolDef(t) => {
4503 assert_eq!(t.name, "greet");
4504 assert!(t.params.is_empty());
4505 }
4506 other => panic!("expected function def, got {:?}", other),
4507 }
4508
4509 match stmts[1] {
4511 Stmt::ToolDef(t) => {
4512 assert_eq!(t.name, "fetch_all");
4513 assert_eq!(t.body.len(), 1);
4514 assert!(matches!(&t.body[0], Stmt::For(_)));
4515 }
4516 other => panic!("expected function def, got {:?}", other),
4517 }
4518
4519 assert!(matches!(stmts[2], Stmt::Assignment(_)));
4521
4522 match stmts[3] {
4524 Stmt::For(f) => {
4525 assert_eq!(f.variable, "USER");
4526 assert_eq!(f.body.len(), 2);
4527 assert!(matches!(&f.body[0], Stmt::Command(_)));
4528 assert!(matches!(&f.body[1], Stmt::If(_)));
4529 }
4530 other => panic!("expected for loop, got {:?}", other),
4531 }
4532
4533 match stmts[4] {
4535 Stmt::Pipeline(p) => {
4536 assert!(p.background);
4537 assert_eq!(p.commands[0].name, "long-running-task");
4538 }
4539 other => panic!("expected pipeline (background), got {:?}", other),
4540 }
4541 }
4542
4543 #[test]
4545 fn script_level4_complex_nesting() {
4546 let script = r#"
4547RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
4548
4549if ping host=${HOST} && [[ ${RESULT} == true ]]; then
4550 for SERVER in "prod-1 prod-2"; do
4551 deploy target=${SERVER} port=8080
4552 if [[ $? -ne 0 ]]; then
4553 notify channel="ops" message="Deploy failed"
4554 fi
4555 done
4556fi
4557"#;
4558 let result = parse(script).unwrap();
4559 let stmts: Vec<_> = result.statements.iter()
4560 .filter(|s| !matches!(s, Stmt::Empty))
4561 .collect();
4562
4563 assert_eq!(stmts.len(), 2);
4564
4565 match stmts[0] {
4567 Stmt::Assignment(a) => {
4568 assert_eq!(a.name(), "RESULT");
4569 assert_eq!(subst_pipeline(&a.value).commands.len(), 3);
4570 }
4571 other => panic!("expected assignment, got {:?}", other),
4572 }
4573
4574 match stmts[1] {
4576 Stmt::If(if_stmt) => {
4577 match if_stmt.condition.as_ref() {
4578 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
4579 other => panic!("expected && condition, got {:?}", other),
4580 }
4581 assert_eq!(if_stmt.then_branch.len(), 1);
4582 match &if_stmt.then_branch[0] {
4583 Stmt::For(f) => {
4584 assert_eq!(f.body.len(), 2);
4585 assert!(matches!(&f.body[1], Stmt::If(_)));
4586 }
4587 other => panic!("expected for in if body, got {:?}", other),
4588 }
4589 }
4590 other => panic!("expected if, got {:?}", other),
4591 }
4592 }
4593
4594 #[test]
4596 fn script_level5_edge_cases() {
4597 let script = r#"
4598echo ""
4599echo "quotes: \"nested\" here"
4600echo "escapes: \n\t\r\\"
4601echo "unicode: \u2764"
4602
4603X=-99999
4604Y=3.14159265358979
4605Z=-0.001
4606
4607cmd a=1 b="two" c=true d=false e=null
4608
4609if true; then
4610 if false; then
4611 echo "inner"
4612 else
4613 echo "else"
4614 fi
4615fi
4616
4617for I in "a b c"; do
4618 echo ${I}
4619done
4620
4621no_params() {
4622 echo "no params"
4623}
4624
4625function all_args {
4626 echo "args: $@"
4627}
4628
4629a | b | c | d | e &
4630cmd 2> "errors.log"
4631cmd &> "all.log"
4632cmd >> "append.log"
4633cmd < "input.txt"
4634"#;
4635 let result = parse(script).unwrap();
4636 let stmts: Vec<_> = result.statements.iter()
4637 .filter(|s| !matches!(s, Stmt::Empty))
4638 .collect();
4639
4640 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
4642
4643 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
4645 assert!(bg_stmt.is_some(), "expected background pipeline");
4646
4647 match bg_stmt.unwrap() {
4648 Stmt::Pipeline(p) => {
4649 assert_eq!(p.commands.len(), 5);
4650 assert!(p.background);
4651 }
4652 _ => unreachable!(),
4653 }
4654 }
4655
4656 #[test]
4661 fn parse_keyword_as_variable_rejected() {
4662 let result = parse(r#"if="value""#);
4665 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
4666
4667 let result = parse("while=true");
4668 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
4669
4670 let result = parse(r#"then="next""#);
4671 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
4672 }
4673
4674 #[test]
4675 fn parse_set_command_with_flag() {
4676 let result = parse("set -e");
4677 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
4678 let program = result.unwrap();
4679 match &program.statements[0] {
4680 Stmt::Command(cmd) => {
4681 assert_eq!(cmd.name, "set");
4682 assert_eq!(cmd.args.len(), 1);
4683 match &cmd.args[0] {
4684 Arg::ShortFlag(f) => assert_eq!(f, "e"),
4685 other => panic!("expected ShortFlag, got {:?}", other),
4686 }
4687 }
4688 other => panic!("expected Command, got {:?}", other),
4689 }
4690 }
4691
4692 #[test]
4693 fn parse_set_command_no_args() {
4694 let result = parse("set");
4695 assert!(result.is_ok(), "failed to parse set: {:?}", result);
4696 let program = result.unwrap();
4697 match &program.statements[0] {
4698 Stmt::Command(cmd) => {
4699 assert_eq!(cmd.name, "set");
4700 assert_eq!(cmd.args.len(), 0);
4701 }
4702 other => panic!("expected Command, got {:?}", other),
4703 }
4704 }
4705
4706 #[test]
4707 fn parse_set_assignment_vs_command() {
4708 let result = parse("X=5");
4710 assert!(result.is_ok());
4711 let program = result.unwrap();
4712 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
4713
4714 let result = parse("set -e");
4716 assert!(result.is_ok());
4717 let program = result.unwrap();
4718 assert!(matches!(&program.statements[0], Stmt::Command(_)));
4719 }
4720
4721 #[test]
4722 fn parse_true_as_command() {
4723 let result = parse("true");
4724 assert!(result.is_ok());
4725 let program = result.unwrap();
4726 match &program.statements[0] {
4727 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
4728 other => panic!("expected Command(true), got {:?}", other),
4729 }
4730 }
4731
4732 #[test]
4733 fn parse_false_as_command() {
4734 let result = parse("false");
4735 assert!(result.is_ok());
4736 let program = result.unwrap();
4737 match &program.statements[0] {
4738 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
4739 other => panic!("expected Command(false), got {:?}", other),
4740 }
4741 }
4742
4743 #[test]
4744 fn parse_dot_as_source_alias() {
4745 let result = parse(". script.kai");
4746 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
4747 let program = result.unwrap();
4748 match &program.statements[0] {
4749 Stmt::Command(cmd) => {
4750 assert_eq!(cmd.name, ".");
4751 assert_eq!(cmd.args.len(), 1);
4752 }
4753 other => panic!("expected Command(.), got {:?}", other),
4754 }
4755 }
4756
4757 #[test]
4758 fn parse_source_command() {
4759 let result = parse("source utils.kai");
4760 assert!(result.is_ok(), "failed to parse source: {:?}", result);
4761 let program = result.unwrap();
4762 match &program.statements[0] {
4763 Stmt::Command(cmd) => {
4764 assert_eq!(cmd.name, "source");
4765 assert_eq!(cmd.args.len(), 1);
4766 }
4767 other => panic!("expected Command(source), got {:?}", other),
4768 }
4769 }
4770
4771 #[test]
4772 fn parse_test_expr_file_test() {
4773 let result = parse(r#"[[ -f "/path/file" ]]"#);
4775 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
4776 }
4777
4778 #[test]
4779 fn parse_test_expr_comparison() {
4780 let result = parse(r#"[[ $X == "value" ]]"#);
4781 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
4782 }
4783
4784 #[test]
4785 fn parse_test_expr_single_eq() {
4786 let result = parse(r#"[[ $X = "value" ]]"#);
4788 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
4789 let program = result.unwrap();
4790 match &program.statements[0] {
4791 Stmt::Test(TestExpr::Comparison { op, .. }) => {
4792 assert_eq!(op, &TestCmpOp::Eq);
4793 }
4794 other => panic!("expected Test(Comparison), got {:?}", other),
4795 }
4796 }
4797
4798 #[test]
4799 fn parse_while_loop() {
4800 let result = parse("while true; do echo; done");
4801 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
4802 let program = result.unwrap();
4803 assert!(matches!(&program.statements[0], Stmt::While(_)));
4804 }
4805
4806 #[test]
4807 fn parse_break_with_level() {
4808 let result = parse("break 2");
4809 assert!(result.is_ok());
4810 let program = result.unwrap();
4811 match &program.statements[0] {
4812 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
4813 other => panic!("expected Break(2), got {:?}", other),
4814 }
4815 }
4816
4817 #[test]
4818 fn parse_continue_with_level() {
4819 let result = parse("continue 3");
4820 assert!(result.is_ok());
4821 let program = result.unwrap();
4822 match &program.statements[0] {
4823 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
4824 other => panic!("expected Continue(3), got {:?}", other),
4825 }
4826 }
4827
4828 #[test]
4829 fn parse_exit_with_code() {
4830 let result = parse("exit 1");
4831 assert!(result.is_ok());
4832 let program = result.unwrap();
4833 match &program.statements[0] {
4834 Stmt::Exit(Some(expr)) => {
4835 match expr.as_ref() {
4836 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
4837 other => panic!("expected Int(1), got {:?}", other),
4838 }
4839 }
4840 other => panic!("expected Exit(1), got {:?}", other),
4841 }
4842 }
4843
4844 #[test]
4851 fn spanned_literal_only_records_byte_range() {
4852 let parts = parse_interpolated_string_spanned("hello world", 100).unwrap();
4853 assert_eq!(parts.len(), 1);
4854 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
4855 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
4856 assert_eq!(parts[0].len, 11);
4857 }
4858
4859 #[test]
4860 fn spanned_braced_var_at_zero() {
4861 let parts = parse_interpolated_string_spanned("${X}", 50).unwrap();
4862 assert_eq!(parts.len(), 1);
4863 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4864 assert_eq!(parts[0].offset, 50);
4865 assert_eq!(parts[0].len, 4); }
4867
4868 #[test]
4869 fn spanned_simple_var_then_literal() {
4870 let parts = parse_interpolated_string_spanned("$X end", 10).unwrap();
4871 assert_eq!(parts.len(), 2);
4872 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4873 assert_eq!(parts[0].offset, 10);
4874 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
4876 assert_eq!(parts[1].offset, 12);
4877 assert_eq!(parts[1].len, 4);
4878 }
4879
4880 #[test]
4881 fn spanned_mixed_literal_var_literal() {
4882 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0).unwrap();
4883 assert_eq!(parts.len(), 3);
4884 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
4886 assert_eq!(parts[0].offset, 0);
4887 assert_eq!(parts[0].len, 3);
4888 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4890 assert_eq!(parts[1].offset, 3);
4891 assert_eq!(parts[1].len, 4);
4892 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
4894 assert_eq!(parts[2].offset, 7);
4895 assert_eq!(parts[2].len, 4);
4896 }
4897
4898 #[test]
4899 fn spanned_positional_param() {
4900 let parts = parse_interpolated_string_spanned("$1 done", 0).unwrap();
4901 assert_eq!(parts.len(), 2);
4902 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
4903 assert_eq!(parts[0].offset, 0);
4904 assert_eq!(parts[0].len, 2); }
4906
4907 #[test]
4908 fn spanned_special_dollar_dollar() {
4909 let parts = parse_interpolated_string_spanned("$$", 5).unwrap();
4910 assert_eq!(parts.len(), 1);
4911 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
4912 assert_eq!(parts[0].offset, 5);
4913 assert_eq!(parts[0].len, 2);
4914 }
4915
4916 #[test]
4917 fn spanned_arithmetic_marker_recognised() {
4918 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0).unwrap();
4922 assert_eq!(parts.len(), 1);
4923 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
4924 }
4925
4926 #[test]
4927 fn spanned_default_separator_yields_var_with_default() {
4928 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0).unwrap();
4929 assert_eq!(parts.len(), 1);
4930 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
4931 assert_eq!(parts[0].offset, 0);
4932 assert_eq!(parts[0].len, 14); }
4934
4935 #[test]
4936 fn spanned_no_dollar_runs_one_literal() {
4937 let parts = parse_interpolated_string_spanned("plain text only", 7).unwrap();
4938 assert_eq!(parts.len(), 1);
4939 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
4940 assert_eq!(parts[0].offset, 7);
4941 assert_eq!(parts[0].len, 15);
4942 }
4943
4944 #[test]
4945 fn spanned_matches_unspanned_part_count() {
4946 let cases = [
4949 "hello",
4950 "$X",
4951 "${X}",
4952 "${X:-d}",
4953 "hi $A and $B",
4954 "$0 $1 $2",
4955 "$$ $? $#",
4956 ];
4957 for s in &cases {
4958 let unspanned = parse_interpolated_string(s).expect("test input parses");
4959 let spanned = parse_interpolated_string_spanned(s, 0).unwrap();
4960 assert_eq!(
4961 unspanned.len(),
4962 spanned.len(),
4963 "part count differs for {:?}",
4964 s
4965 );
4966 }
4967 }
4968
4969 #[test]
4970 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
4971 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0).unwrap();
4976 assert_eq!(parts.len(), 2);
4977
4978 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
4979 assert_eq!(parts[0].offset, 0);
4980 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
4981
4982 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4983 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
4984 assert_eq!(parts[1].len, 4);
4985 }
4986
4987 #[test]
4988 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
4989 let parts = parse_interpolated_string_spanned("hello 世界 world", 0).unwrap();
4992 assert_eq!(parts.len(), 1);
4993 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
4994 assert_eq!(parts[0].offset, 0);
4995 assert_eq!(parts[0].len, 18);
4996 }
4997
4998 #[test]
4999 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
5000 let parts = parse_interpolated_string_spanned("\\$", 0).unwrap();
5003 assert_eq!(parts.len(), 1);
5004 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
5005 assert_eq!(parts[0].offset, 0);
5006 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
5007 }
5008
5009 #[test]
5010 fn spanned_escape_backslash_collapses_pair_to_one() {
5011 let parts = parse_interpolated_string_spanned("\\\\", 0).unwrap();
5012 assert_eq!(parts.len(), 1);
5013 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
5014 assert_eq!(parts[0].len, 2);
5015 }
5016
5017 #[test]
5018 fn spanned_standalone_cr_continuation_realigns_span_start() {
5019 let parts = parse_interpolated_string_spanned("\\\rCD${x}", 0).unwrap();
5027 assert_eq!(parts.len(), 2);
5028 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "CD"));
5029 assert_eq!(parts[0].offset, 2, "literal run must start after the consumed \\+CR");
5030 assert_eq!(parts[0].len, 2);
5031 assert!(matches!(&parts[1].part, StringPart::Var(_)));
5032 assert_eq!(parts[1].offset, 4);
5033 assert_eq!(parts[1].len, 4); }
5035
5036 #[test]
5037 fn spanned_standalone_cr_continuation_mid_run_keeps_span_start() {
5038 let parts = parse_interpolated_string_spanned("AB\\\rCD${x}", 0).unwrap();
5043 assert_eq!(parts.len(), 2);
5044 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "ABCD"));
5045 assert_eq!(parts[0].offset, 0);
5046 assert_eq!(parts[0].len, 6); assert!(matches!(&parts[1].part, StringPart::Var(_)));
5048 assert_eq!(parts[1].offset, 6);
5049 assert_eq!(parts[1].len, 4); }
5051
5052 fn assignment_value(source: &str) -> Expr {
5056 let program = parse(source).unwrap_or_else(|e| panic!("parse {source:?}: {e:?}"));
5057 match program.statements.as_slice() {
5058 [Stmt::Assignment(a)] => a.value.clone(),
5059 other => panic!("expected a single assignment, got {other:?}"),
5060 }
5061 }
5062
5063 #[test]
5064 fn list_literal_three_elements() {
5065 let expr = assignment_value("xs=[a b c]");
5066 match expr {
5067 Expr::ListLiteral(elems) => {
5068 assert_eq!(elems.len(), 3);
5069 assert!(elems.iter().all(|e| matches!(e, ListElem::Item(_))));
5070 }
5071 other => panic!("expected ListLiteral, got {other:?}"),
5072 }
5073 }
5074
5075 #[test]
5076 fn list_literal_empty() {
5077 let expr = assignment_value("xs=[]");
5078 assert!(matches!(expr, Expr::ListLiteral(elems) if elems.is_empty()));
5079 }
5080
5081 #[test]
5082 fn list_literal_single_glued_dog() {
5083 let expr = assignment_value("xs=[dog]");
5087 match expr {
5088 Expr::ListLiteral(elems) => assert_eq!(elems.len(), 1),
5089 other => panic!("expected ListLiteral, got {other:?}"),
5090 }
5091 }
5092
5093 #[test]
5094 fn list_literal_single_int() {
5095 let expr = assignment_value("xs=[1]");
5096 match expr {
5097 Expr::ListLiteral(elems) => match elems.as_slice() {
5098 [ListElem::Item(Expr::Literal(Value::Int(1)))] => {}
5099 other => panic!("expected one Int(1) item, got {other:?}"),
5100 },
5101 other => panic!("expected ListLiteral, got {other:?}"),
5102 }
5103 }
5104
5105 #[test]
5106 fn record_literal_unspaced_colon_equals_spaced() {
5107 let spaced = assignment_value("x={port: 8080}");
5108 let unspaced = assignment_value("x={port:8080}");
5109 assert_eq!(spaced, unspaced, "{{port:8080}} must parse identically to {{port: 8080}}");
5110 match spaced {
5111 Expr::RecordLiteral(entries) => match entries.as_slice() {
5112 [RecordEntry { key: RecordKey::Bare(k), value: Expr::Literal(Value::Int(8080)) }] => {
5113 assert_eq!(k, "port");
5114 }
5115 other => panic!("expected one port:8080 entry, got {other:?}"),
5116 },
5117 other => panic!("expected RecordLiteral, got {other:?}"),
5118 }
5119 }
5120
5121 #[test]
5122 fn record_literal_name_role() {
5123 let expr = assignment_value("u={name: amy, role: maintainer}");
5124 match expr {
5125 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2),
5126 other => panic!("expected RecordLiteral, got {other:?}"),
5127 }
5128 }
5129
5130 #[test]
5131 fn record_literal_multiline_trailing_comma() {
5132 let source = "services={\n web: {port: 8080, replicas: 3, healthy: true},\n api: {port: 9000, replicas: 2, healthy: false},\n}";
5133 let expr = assignment_value(source);
5134 match expr {
5135 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2, "web + api entries"),
5136 other => panic!("expected RecordLiteral, got {other:?}"),
5137 }
5138 }
5139
5140 #[test]
5141 fn record_literal_quoted_key() {
5142 let expr = assignment_value(r#"r={"content-type": x}"#);
5143 match expr {
5144 Expr::RecordLiteral(entries) => match entries.as_slice() {
5145 [RecordEntry { key: RecordKey::Quoted(k), .. }] => assert_eq!(k, "content-type"),
5146 other => panic!("expected one quoted-key entry, got {other:?}"),
5147 },
5148 other => panic!("expected RecordLiteral, got {other:?}"),
5149 }
5150 }
5151
5152 #[test]
5153 fn nested_list_and_record_in_record() {
5154 let expr = assignment_value("x={tags: [a b], meta: {active: true}}");
5155 match expr {
5156 Expr::RecordLiteral(entries) => {
5157 assert_eq!(entries.len(), 2);
5158 assert!(matches!(entries[0].value, Expr::ListLiteral(_)));
5159 assert!(matches!(entries[1].value, Expr::RecordLiteral(_)));
5160 }
5161 other => panic!("expected RecordLiteral, got {other:?}"),
5162 }
5163 }
5164
5165 #[test]
5166 fn spread_and_item_elements() {
5167 let expr = assignment_value("new=[...$xs date]");
5168 match expr {
5169 Expr::ListLiteral(elems) => match elems.as_slice() {
5170 [ListElem::Spread(Expr::VarRef(_)), ListElem::Item(Expr::Literal(Value::String(s)))] => {
5171 assert_eq!(s, "date");
5172 }
5173 other => panic!("expected [Spread($xs), Item(date)], got {other:?}"),
5174 },
5175 other => panic!("expected ListLiteral, got {other:?}"),
5176 }
5177 }
5178
5179 #[test]
5180 fn spread_of_two_variables() {
5181 let expr = assignment_value("c=[...$a ...$b]");
5182 match expr {
5183 Expr::ListLiteral(elems) => {
5184 assert_eq!(elems.len(), 2);
5185 assert!(elems.iter().all(|e| matches!(e, ListElem::Spread(_))));
5186 }
5187 other => panic!("expected ListLiteral, got {other:?}"),
5188 }
5189 }
5190
5191 #[test]
5192 fn in_rhs_accepts_a_list_literal() {
5193 let program = parse("if [[ $a not in [dog] ]]; then echo hit; fi")
5194 .unwrap_or_else(|e| panic!("parse: {e:?}"));
5195 assert_eq!(program.statements.len(), 1);
5196 }
5197
5198 #[test]
5199 fn multiword_bareword_record_value_is_a_parse_error() {
5200 assert!(parse("x={msg: hello world}").is_err());
5203 }
5204
5205 #[test]
5208 fn argv_bracket_glob_stays_a_glob_pattern() {
5209 let program = parse("ls [dog]").unwrap_or_else(|e| panic!("parse: {e:?}"));
5213 assert_eq!(program.statements.len(), 1);
5214 }
5215
5216 #[test]
5217 fn brace_expansion_at_argv_position_is_unaffected() {
5218 let program = parse("cmd *.{rs,go}").unwrap_or_else(|e| panic!("parse: {e:?}"));
5223 assert_eq!(program.statements.len(), 1);
5224 }
5225
5226 #[test]
5227 fn for_head_item_is_not_a_literal() {
5228 let program = parse("for x in [a]; do echo $x; done")
5231 .unwrap_or_else(|e| panic!("parse: {e:?}"));
5232 match program.statements.as_slice() {
5233 [Stmt::For(for_loop)] => {
5234 assert_eq!(for_loop.items.len(), 1);
5235 assert!(
5236 !matches!(for_loop.items[0], Expr::ListLiteral(_)),
5237 "for-head item must not be a ListLiteral: {:?}",
5238 for_loop.items[0]
5239 );
5240 }
5241 other => panic!("expected a single For statement, got {other:?}"),
5242 }
5243 }
5244}