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
55fn unquote_default_word(word: &str) -> String {
65 let mut out = String::with_capacity(word.len());
66 let mut in_single = false;
67 let mut in_double = false;
68 for ch in word.chars() {
69 match ch {
70 '\'' if !in_double => in_single = !in_single,
73 '"' if !in_single => in_double = !in_double,
74 '$' if in_single => out.push_str("__KAISH_ESCAPED_DOLLAR__"),
76 _ => out.push(ch),
77 }
78 }
79 out
80}
81
82fn find_default_separator(raw: &str) -> Option<usize> {
84 let bytes = raw.as_bytes();
85 let mut depth = 0;
86 let mut bracket_depth = 0;
87 let mut i = 0;
88
89 while i < bytes.len() {
90 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
91 depth += 1;
92 i += 2;
93 continue;
94 }
95 if bytes[i] == b'}' && depth > 0 {
96 depth -= 1;
97 i += 1;
98 continue;
99 }
100 if bytes[i] == b'[' {
103 bracket_depth += 1;
104 } else if bytes[i] == b']' && bracket_depth > 0 {
105 bracket_depth -= 1;
106 }
107 if depth == 1
110 && bracket_depth == 0
111 && i + 1 < bytes.len()
112 && bytes[i] == b':'
113 && bytes[i + 1] == b'-'
114 {
115 return Some(i);
116 }
117 i += 1;
118 }
119 None
120}
121
122fn find_default_separator_in_content(content: &str) -> Option<usize> {
124 let bytes = content.as_bytes();
125 let mut depth = 0;
126 let mut bracket_depth = 0;
127 let mut i = 0;
128
129 while i < bytes.len() {
130 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
131 depth += 1;
132 i += 2;
133 continue;
134 }
135 if bytes[i] == b'}' && depth > 0 {
136 depth -= 1;
137 i += 1;
138 continue;
139 }
140 if bytes[i] == b'[' {
143 bracket_depth += 1;
144 } else if bytes[i] == b']' && bracket_depth > 0 {
145 bracket_depth -= 1;
146 }
147 if depth == 0
149 && bracket_depth == 0
150 && i + 1 < bytes.len()
151 && bytes[i] == b':'
152 && bytes[i + 1] == b'-'
153 {
154 return Some(i);
155 }
156 i += 1;
157 }
158 None
159}
160
161pub(crate) fn parse_varpath(raw: &str) -> VarPath {
168 let segment_strs = lexer::parse_var_ref(raw).unwrap_or_default();
169 let segments = segment_strs
170 .into_iter()
171 .enumerate()
172 .map(|(i, s)| {
173 if i == 0 {
174 VarSegment::Field(s)
176 } else if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
177 parse_subscript(inner)
178 } else {
179 VarSegment::Field(s)
182 }
183 })
184 .collect();
185 VarPath { segments }
186}
187
188fn parse_subscript(inner: &str) -> VarSegment {
195 if let Some(var) = inner.strip_prefix('$') {
197 return VarSegment::Dynamic(var.to_string());
198 }
199 if inner.len() >= 2
201 && ((inner.starts_with('"') && inner.ends_with('"'))
202 || (inner.starts_with('\'') && inner.ends_with('\'')))
203 {
204 return VarSegment::Key(inner[1..inner.len() - 1].to_string());
205 }
206 if let Some((lhs, rhs)) = inner.split_once(':') {
210 let bound = |s: &str| -> Option<Option<i64>> {
211 if s.is_empty() {
212 Some(None)
213 } else {
214 s.parse::<i64>().ok().map(Some)
215 }
216 };
217 if let (Some(start), Some(end)) = (bound(lhs), bound(rhs)) {
218 return VarSegment::Slice(start, end);
219 }
220 }
221 if let Ok(i) = inner.parse::<i64>() {
223 return VarSegment::Index(i);
224 }
225 VarSegment::Key(inner.to_string())
227}
228
229fn strip_empty_stmts(statements: Vec<Stmt>) -> Vec<Stmt> {
232 statements
233 .into_iter()
234 .filter(|s| !matches!(s, Stmt::Empty))
235 .collect()
236}
237
238fn parse_interpolated_string_spanned(s: &str, base_offset: usize) -> Vec<SpannedPart> {
257 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
258
259 let chars_vec: Vec<char> = s.chars().collect();
260 let mut i = 0;
261 let mut pos: usize = 0;
262
263 let mut parts: Vec<SpannedPart> = Vec::new();
264 let mut current_text = String::new();
265 let mut current_text_start: usize = pos;
266
267 let push_literal =
268 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
269 if !current_text.is_empty() {
270 parts.push(SpannedPart {
271 part: StringPart::Literal(std::mem::take(current_text)),
272 offset: base_offset + *start,
273 len: end - *start,
274 });
275 *start = end;
276 }
277 };
278
279 while i < chars_vec.len() {
280 let ch = chars_vec[i];
281
282 if ch == '\x00' {
283 let start = pos;
285 i += 1;
286 pos += 1;
287 let mut marker = String::new();
288 while let Some(&c) = chars_vec.get(i) {
289 if c == '\x00' {
290 i += 1;
291 pos += 1;
292 break;
293 }
294 marker.push(c);
295 i += 1;
296 pos += c.len_utf8();
297 }
298 if marker == "DOLLAR" {
299 if current_text.is_empty() {
300 current_text_start = start;
301 }
302 current_text.push('$');
303 }
304 } else if ch == '\\' {
305 let next = chars_vec.get(i + 1).copied();
311 match next {
312 Some('$') => {
313 if current_text.is_empty() {
314 current_text_start = pos;
315 }
316 current_text.push('$');
317 i += 2;
318 pos += 2;
319 }
320 Some('\\') => {
321 if current_text.is_empty() {
322 current_text_start = pos;
323 }
324 current_text.push('\\');
325 i += 2;
326 pos += 2;
327 }
328 Some('\n') => {
329 i += 2;
332 pos += 2;
333 if current_text.is_empty() {
334 current_text_start = pos;
335 }
336 }
337 Some('\r') => {
338 i += 2;
340 pos += 2;
341 if chars_vec.get(i) == Some(&'\n') {
342 i += 1;
343 pos += 1;
344 }
345 if current_text.is_empty() {
346 current_text_start = pos;
347 }
348 }
349 _ => {
350 if current_text.is_empty() {
354 current_text_start = pos;
355 }
356 current_text.push('\\');
357 i += 1;
358 pos += 1;
359 }
360 }
361 } else if ch == '$' {
362 let part_start = pos;
364 let next = chars_vec.get(i + 1).copied();
365
366 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
367 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
369 i += 2; pos += 2;
371 let mut cmd_content = String::new();
372 let mut depth = 1;
373 while let Some(&c) = chars_vec.get(i) {
374 i += 1;
375 pos += c.len_utf8();
376 if c == '(' {
377 depth += 1;
378 cmd_content.push(c);
379 } else if c == ')' {
380 depth -= 1;
381 if depth == 0 {
382 break;
383 }
384 cmd_content.push(c);
385 } else {
386 cmd_content.push(c);
387 }
388 }
389 let inserted = if let Ok(program) = parse(&cmd_content) {
390 let stmts = strip_empty_stmts(program.statements);
393 if stmts.is_empty() {
394 false
395 } else {
396 parts.push(SpannedPart {
397 part: StringPart::CommandSubst(stmts),
398 offset: base_offset + part_start,
399 len: pos - part_start,
400 });
401 true
402 }
403 } else {
404 false
405 };
406 if inserted {
407 current_text_start = pos;
410 } else {
411 if current_text.is_empty() {
416 current_text_start = part_start;
417 }
418 current_text.push_str("$(");
419 current_text.push_str(&cmd_content);
420 current_text.push(')');
421 }
422 } else if next == Some('{') {
423 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
424 i += 2; pos += 2;
426 let mut var_content = String::new();
427 let mut depth = 1;
428 while let Some(&c) = chars_vec.get(i) {
429 i += 1;
430 pos += c.len_utf8();
431 if c == '{' && var_content.ends_with('$') {
432 depth += 1;
433 var_content.push(c);
434 } else if c == '}' {
435 depth -= 1;
436 if depth == 0 {
437 break;
438 }
439 var_content.push(c);
440 } else {
441 var_content.push(c);
442 }
443 }
444 let part = if let Some(name) = var_content.strip_prefix('#') {
445 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
446 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
447 let expr = var_content
448 .strip_prefix("__ARITH:")
449 .and_then(|s| s.strip_suffix("__"))
450 .unwrap_or("");
451 StringPart::Arithmetic(expr.to_string())
452 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
453 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
454 let default_str = &var_content[colon_idx + 2..];
455 let default_word = unquote_default_word(default_str);
460 let default = parse_interpolated_string(&default_word)
461 .unwrap_or_else(|_| vec![StringPart::Literal(default_word.clone())]);
462 StringPart::VarWithDefault { path, default }
463 } else {
464 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
465 };
466 parts.push(SpannedPart {
467 part,
468 offset: base_offset + part_start,
469 len: pos - part_start,
470 });
471 current_text_start = pos;
472 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
473 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
474 i += 1; pos += 1;
476 if let Some(&digit) = chars_vec.get(i) {
477 let n = digit.to_digit(10).unwrap_or(0) as usize;
478 i += 1;
479 pos += digit.len_utf8();
480 parts.push(SpannedPart {
481 part: StringPart::Positional(n),
482 offset: base_offset + part_start,
483 len: pos - part_start,
484 });
485 }
486 current_text_start = pos;
487 } else if next == Some('@') {
488 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
489 i += 2; pos += 2;
491 parts.push(SpannedPart {
492 part: StringPart::AllArgs,
493 offset: base_offset + part_start,
494 len: pos - part_start,
495 });
496 current_text_start = pos;
497 } else if next == Some('#') {
498 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
499 i += 2; pos += 2;
501 parts.push(SpannedPart {
502 part: StringPart::ArgCount,
503 offset: base_offset + part_start,
504 len: pos - part_start,
505 });
506 current_text_start = pos;
507 } else if next == Some('?') {
508 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
509 i += 2; pos += 2;
511 parts.push(SpannedPart {
512 part: StringPart::LastExitCode,
513 offset: base_offset + part_start,
514 len: pos - part_start,
515 });
516 current_text_start = pos;
517 } else if next == Some('$') {
518 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
519 i += 2; pos += 2;
521 parts.push(SpannedPart {
522 part: StringPart::CurrentPid,
523 offset: base_offset + part_start,
524 len: pos - part_start,
525 });
526 current_text_start = pos;
527 } else if next.map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
528 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
529 i += 1; pos += 1;
531 let mut var_name = String::new();
532 while let Some(&c) = chars_vec.get(i) {
533 if c.is_ascii_alphanumeric() || c == '_' {
534 var_name.push(c);
535 i += 1;
536 pos += c.len_utf8();
537 } else {
538 break;
539 }
540 }
541 parts.push(SpannedPart {
542 part: StringPart::Var(VarPath::simple(var_name)),
543 offset: base_offset + part_start,
544 len: pos - part_start,
545 });
546 current_text_start = pos;
547 } else {
548 if current_text.is_empty() {
550 current_text_start = pos;
551 }
552 current_text.push(ch);
553 i += 1;
554 pos += 1;
555 }
556 } else {
557 if current_text.is_empty() {
558 current_text_start = pos;
559 }
560 current_text.push(ch);
561 i += 1;
562 pos += ch.len_utf8();
563 }
564 }
565
566 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
567
568 parts
569}
570
571fn parse_interpolated_string(s: &str) -> Result<Vec<StringPart>, String> {
572 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
575
576 let mut parts = Vec::new();
577 let mut current_text = String::new();
578 let mut chars = s.chars().peekable();
579
580 while let Some(ch) = chars.next() {
581 if ch == '\x00' {
582 let mut marker = String::new();
584 while let Some(&c) = chars.peek() {
585 if c == '\x00' {
586 chars.next(); break;
588 }
589 if let Some(c) = chars.next() {
590 marker.push(c);
591 }
592 }
593 if marker == "DOLLAR" {
594 current_text.push('$');
595 }
596 } else if ch == '$' {
597 if chars.peek() == Some(&'(') {
599 if !current_text.is_empty() {
601 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
602 }
603
604 chars.next();
606
607 let mut cmd_content = String::new();
609 let mut paren_depth = 1;
610 for c in chars.by_ref() {
611 if c == '(' {
612 paren_depth += 1;
613 cmd_content.push(c);
614 } else if c == ')' {
615 paren_depth -= 1;
616 if paren_depth == 0 {
617 break;
618 }
619 cmd_content.push(c);
620 } else {
621 cmd_content.push(c);
622 }
623 }
624
625 match parse(&cmd_content) {
628 Ok(program) => {
629 let stmts = strip_empty_stmts(program.statements);
630 if stmts.is_empty() {
631 current_text.push_str("$(");
634 current_text.push_str(&cmd_content);
635 current_text.push(')');
636 } else {
637 parts.push(StringPart::CommandSubst(stmts));
638 }
639 }
640 Err(_) => {
641 return Err(format!(
645 "syntax error in command substitution: $({cmd_content})"
646 ));
647 }
648 }
649 } else if chars.peek() == Some(&'{') {
650 if !current_text.is_empty() {
652 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
653 }
654
655 chars.next();
657
658 let mut var_content = String::new();
660 let mut depth = 1;
661 for c in chars.by_ref() {
662 if c == '{' && var_content.ends_with('$') {
663 depth += 1;
664 var_content.push(c);
665 } else if c == '}' {
666 depth -= 1;
667 if depth == 0 {
668 break;
669 }
670 var_content.push(c);
671 } else {
672 var_content.push(c);
673 }
674 }
675
676 let part = if let Some(name) = var_content.strip_prefix('#') {
678 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
680 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
681 let expr = var_content
683 .strip_prefix("__ARITH:")
684 .and_then(|s| s.strip_suffix("__"))
685 .unwrap_or("");
686 StringPart::Arithmetic(expr.to_string())
687 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
688 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
690 let default_str = &var_content[colon_idx + 2..];
691 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
692 StringPart::VarWithDefault { path, default }
693 } else {
694 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
696 };
697 parts.push(part);
698 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
699 if !current_text.is_empty() {
701 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
702 }
703 if let Some(digit) = chars.next() {
704 let n = digit.to_digit(10).unwrap_or(0) as usize;
705 parts.push(StringPart::Positional(n));
706 }
707 } else if chars.peek() == Some(&'@') {
708 if !current_text.is_empty() {
710 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
711 }
712 chars.next(); parts.push(StringPart::AllArgs);
714 } else if chars.peek() == Some(&'#') {
715 if !current_text.is_empty() {
717 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
718 }
719 chars.next(); parts.push(StringPart::ArgCount);
721 } else if chars.peek() == Some(&'?') {
722 if !current_text.is_empty() {
724 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
725 }
726 chars.next(); parts.push(StringPart::LastExitCode);
728 } else if chars.peek() == Some(&'$') {
729 if !current_text.is_empty() {
731 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
732 }
733 chars.next(); parts.push(StringPart::CurrentPid);
735 } else if chars.peek().map(|c| c.is_ascii_alphabetic() || *c == '_').unwrap_or(false) {
736 if !current_text.is_empty() {
738 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
739 }
740
741 let mut var_name = String::new();
743 while let Some(&c) = chars.peek() {
744 if c.is_ascii_alphanumeric() || c == '_' {
745 if let Some(c) = chars.next() {
746 var_name.push(c);
747 }
748 } else {
749 break;
750 }
751 }
752
753 parts.push(StringPart::Var(VarPath::simple(var_name)));
754 } else {
755 current_text.push(ch);
757 }
758 } else {
759 current_text.push(ch);
760 }
761 }
762
763 if !current_text.is_empty() {
764 parts.push(StringPart::Literal(current_text));
765 }
766
767 Ok(parts)
768}
769
770#[derive(Debug, Clone)]
772pub struct ParseError {
773 pub span: Span,
774 pub message: String,
775}
776
777impl ParseError {
778 pub fn format(&self, source: &str) -> String {
783 let start = self.span.start;
784 let mut line = 1usize;
785 let mut col = 1usize;
786 for (i, ch) in source.char_indices() {
787 if i >= start {
788 break;
789 }
790 if ch == '\n' {
791 line += 1;
792 col = 1;
793 } else {
794 col += 1;
795 }
796 }
797 let line_content = {
798 let line_start = source[..start.min(source.len())]
799 .rfind('\n')
800 .map_or(0, |i| i + 1);
801 let line_end = source[start.min(source.len())..]
802 .find('\n')
803 .map_or(source.len(), |i| start + i);
804 source.get(line_start..line_end).unwrap_or("")
805 };
806 if line_content.is_empty() {
807 format!("{}:{} [parse]: {}", line, col, self.message)
808 } else {
809 format!(
810 "{}:{} [parse]: {}\n | {}",
811 line, col, self.message, line_content
812 )
813 }
814 }
815}
816
817impl std::fmt::Display for ParseError {
818 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819 write!(f, "{} at {:?}", self.message, self.span)
820 }
821}
822
823impl std::error::Error for ParseError {}
824
825pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
827 let tokens = lexer::tokenize(source).map_err(|errs| {
829 errs.into_iter()
830 .map(|e| ParseError {
831 span: (e.span.start..e.span.end).into(),
832 message: format!("lexer error: {}", e.token),
833 })
834 .collect::<Vec<_>>()
835 })?;
836
837 let tokens: Vec<(Token, Span)> = tokens
839 .into_iter()
840 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
841 .collect();
842
843 let end_span: Span = (source.len()..source.len()).into();
845
846 let parser = program_parser();
848 let result = parser.parse(tokens.as_slice().map(end_span, |(t, s)| (t, s)));
849
850 let program = result.into_result().map_err(|errs| {
851 errs.into_iter()
852 .map(|e| ParseError {
853 span: *e.span(),
854 message: e.to_string(),
855 })
856 .collect::<Vec<_>>()
857 })?;
858
859 if first_ambiguous_stdin(&program.statements) {
864 return Err(vec![ParseError {
865 span: (0..0).into(),
869 message: "multiple stdin redirects on one command are ambiguous; \
870 use exactly one of `<`, `<<`, or `<<<`"
871 .to_string(),
872 }]);
873 }
874
875 Ok(program)
876}
877
878pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
880 let program = parse(source)?;
881 program
882 .statements
883 .into_iter()
884 .find(|s| !matches!(s, Stmt::Empty))
885 .ok_or_else(|| {
886 vec![ParseError {
887 span: (0..source.len()).into(),
888 message: "empty input".to_string(),
889 }]
890 })
891}
892
893fn program_parser<'tokens, 'src: 'tokens, I>(
899) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
900where
901 I: ValueInput<'tokens, Token = Token, Span = Span>,
902{
903 statement_parser()
904 .repeated()
905 .collect::<Vec<_>>()
906 .map(|statements| Program { statements })
907}
908
909fn statement_parser<'tokens, I>(
912) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
913where
914 I: ValueInput<'tokens, Token = Token, Span = Span>,
915{
916 recursive(|stmt| {
917 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
918
919 let break_stmt = just(Token::Break)
921 .ignore_then(
922 select! { Token::Int(n) => n as usize }.or_not()
923 )
924 .map(Stmt::Break);
925
926 let continue_stmt = just(Token::Continue)
928 .ignore_then(
929 select! { Token::Int(n) => n as usize }.or_not()
930 )
931 .map(Stmt::Continue);
932
933 let return_stmt = just(Token::Return)
935 .ignore_then(primary_expr_parser().or_not())
936 .map(|e| Stmt::Return(e.map(Box::new)));
937
938 let exit_stmt = just(Token::Exit)
940 .ignore_then(primary_expr_parser().or_not())
941 .map(|e| Stmt::Exit(e.map(Box::new)));
942
943 let set_flag_arg = choice((
952 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
953 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
954 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
956 ));
957
958 let option_value_str = select! {
962 Token::NumberIdent(s) => s,
963 Token::Int(n) => n.to_string(),
964 Token::Ident(s) => s,
965 };
966
967 let set_option_assign = ident_parser()
971 .then_ignore(just(Token::Eq))
972 .then(option_value_str)
973 .map(|(name, value)| {
974 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
975 });
976
977 let set_quoted_arg = select! {
981 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
982 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
983 };
984
985 let set_with_flags = just(Token::Set)
987 .then(set_flag_arg)
988 .then(
989 choice((
990 set_flag_arg,
991 set_option_assign,
993 set_quoted_arg,
994 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
996 ))
997 .repeated()
998 .collect::<Vec<_>>(),
999 )
1000 .map(|((_, first_arg), mut rest_args)| {
1001 let mut args = vec![first_arg];
1002 args.append(&mut rest_args);
1003 Stmt::Command(Command {
1004 name: "set".to_string(),
1005 args,
1006 redirects: vec![],
1007 })
1008 });
1009
1010 let set_no_args = just(Token::Set)
1013 .then(
1014 choice((
1015 just(Token::Newline).to(()),
1016 just(Token::Semi).to(()),
1017 just(Token::And).to(()),
1018 just(Token::Or).to(()),
1019 end(),
1020 ))
1021 .rewind(),
1022 )
1023 .map(|_| Stmt::Command(Command {
1024 name: "set".to_string(),
1025 args: vec![],
1026 redirects: vec![],
1027 }));
1028
1029 let set_command = set_with_flags.or(set_no_args);
1033
1034 let env_prefix_assign = ident_parser()
1051 .then_ignore(just(Token::Eq))
1052 .then(value_expr_parser())
1053 .map(|(name, value)| Assignment { path: VarPath::simple(name), value, local: false });
1054 let env_scoped = env_prefix_assign
1055 .repeated()
1056 .at_least(1)
1057 .collect::<Vec<_>>()
1058 .then(pipeline_parser().map(pipeline_into_stmt))
1059 .map(|(assignments, body)| Stmt::EnvScoped {
1060 assignments,
1061 body: Box::new(body),
1062 });
1063
1064 let base_statement = choice((
1066 just(Token::Newline).to(Stmt::Empty),
1067 set_command,
1068 env_scoped,
1069 assignment_parser().map(Stmt::Assignment),
1070 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), if_parser(stmt.clone()).map(Stmt::If),
1074 for_parser(stmt.clone()).map(Stmt::For),
1075 while_parser(stmt.clone()).map(Stmt::While),
1076 case_parser(stmt.clone()).map(Stmt::Case),
1077 break_stmt,
1078 continue_stmt,
1079 return_stmt,
1080 exit_stmt,
1081 test_expr_stmt_parser().map(Stmt::Test),
1082 pipeline_parser().map(pipeline_into_stmt),
1084 ))
1085 .boxed();
1086
1087 base_statement
1093 .clone()
1094 .foldl(
1095 choice((
1096 just(Token::And).to(true), just(Token::Or).to(false), ))
1099 .then(base_statement)
1100 .repeated(),
1101 |left, (is_and, right): (bool, Stmt)| {
1102 if is_and {
1103 Stmt::AndChain {
1104 left: Box::new(left),
1105 right: Box::new(right),
1106 }
1107 } else {
1108 Stmt::OrChain {
1109 left: Box::new(left),
1110 right: Box::new(right),
1111 }
1112 }
1113 },
1114 )
1115 .then_ignore(terminator)
1116 })
1117}
1118
1119fn lvalue_subscript_parser<'tokens, I>(
1129) -> impl Parser<'tokens, I, VarSegment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1130where
1131 I: ValueInput<'tokens, Token = Token, Span = Span>,
1132{
1133 let interior = choice((
1134 select! { Token::SimpleVarRef(name) => VarSegment::Dynamic(name) },
1135 select! { Token::String(s) => VarSegment::Key(s) },
1136 select! { Token::SingleString(s) => VarSegment::Key(s) },
1137 select! { Token::Int(n) => VarSegment::Index(n) },
1138 select! { Token::Ident(s) => parse_subscript(&s) },
1139 ));
1140
1141 just(Token::LBracket)
1142 .ignore_then(interior)
1143 .then_ignore(just(Token::RBracket))
1144 .labelled("subscript")
1145}
1146
1147fn lvalue_path_parser<'tokens, I>(
1153) -> impl Parser<'tokens, I, VarPath, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1154where
1155 I: ValueInput<'tokens, Token = Token, Span = Span>,
1156{
1157 ident_parser()
1158 .then(lvalue_subscript_parser().repeated().collect::<Vec<_>>())
1159 .map(|(name, subscripts)| {
1160 let mut segments = vec![VarSegment::Field(name)];
1161 segments.extend(subscripts);
1162 VarPath { segments }
1163 })
1164 .labelled("lvalue path")
1165}
1166
1167fn assignment_parser<'tokens, I>(
1173) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1174where
1175 I: ValueInput<'tokens, Token = Token, Span = Span>,
1176{
1177 let local_assignment = just(Token::Local)
1179 .ignore_then(lvalue_path_parser())
1180 .then_ignore(just(Token::Eq))
1181 .then(value_expr_parser())
1182 .map(|(path, value)| Assignment {
1183 path,
1184 value,
1185 local: true,
1186 });
1187
1188 let bash_assignment = lvalue_path_parser()
1191 .then_ignore(just(Token::Eq))
1192 .then(value_expr_parser())
1193 .map(|(path, value)| Assignment {
1194 path,
1195 value,
1196 local: false,
1197 });
1198
1199 choice((local_assignment, bash_assignment))
1200 .labelled("assignment")
1201 .boxed()
1202}
1203
1204fn posix_function_parser<'tokens, I, S>(
1208 stmt: S,
1209) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1210where
1211 I: ValueInput<'tokens, Token = Token, Span = Span>,
1212 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1213{
1214 ident_parser()
1215 .then_ignore(just(Token::LParen))
1216 .then_ignore(just(Token::RParen))
1217 .then_ignore(just(Token::LBrace))
1218 .then_ignore(just(Token::Newline).repeated())
1219 .then(
1220 stmt.repeated()
1221 .collect::<Vec<_>>()
1222 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1223 )
1224 .then_ignore(just(Token::Newline).repeated())
1225 .then_ignore(just(Token::RBrace))
1226 .map(|(name, body)| ToolDef { name, params: vec![], body })
1227 .labelled("POSIX function")
1228 .boxed()
1229}
1230
1231fn bash_function_parser<'tokens, I, S>(
1235 stmt: S,
1236) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1237where
1238 I: ValueInput<'tokens, Token = Token, Span = Span>,
1239 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1240{
1241 just(Token::Function)
1242 .ignore_then(ident_parser())
1243 .then_ignore(just(Token::LBrace))
1244 .then_ignore(just(Token::Newline).repeated())
1245 .then(
1246 stmt.repeated()
1247 .collect::<Vec<_>>()
1248 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1249 )
1250 .then_ignore(just(Token::Newline).repeated())
1251 .then_ignore(just(Token::RBrace))
1252 .map(|(name, body)| ToolDef { name, params: vec![], body })
1253 .labelled("bash function")
1254 .boxed()
1255}
1256
1257fn if_parser<'tokens, I, S>(
1264 stmt: S,
1265) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1266where
1267 I: ValueInput<'tokens, Token = Token, Span = Span>,
1268 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1269{
1270 let branch = condition_parser()
1272 .then_ignore(just(Token::Semi).or_not())
1273 .then_ignore(just(Token::Newline).repeated())
1274 .then_ignore(just(Token::Then))
1275 .then_ignore(just(Token::Newline).repeated())
1276 .then(
1277 stmt.clone()
1278 .repeated()
1279 .collect::<Vec<_>>()
1280 .map(|stmts: Vec<Stmt>| {
1281 stmts
1282 .into_iter()
1283 .filter(|s| !matches!(s, Stmt::Empty))
1284 .collect::<Vec<_>>()
1285 }),
1286 );
1287
1288 let elif_branch = just(Token::Elif)
1290 .ignore_then(condition_parser())
1291 .then_ignore(just(Token::Semi).or_not())
1292 .then_ignore(just(Token::Newline).repeated())
1293 .then_ignore(just(Token::Then))
1294 .then_ignore(just(Token::Newline).repeated())
1295 .then(
1296 stmt.clone()
1297 .repeated()
1298 .collect::<Vec<_>>()
1299 .map(|stmts: Vec<Stmt>| {
1300 stmts
1301 .into_iter()
1302 .filter(|s| !matches!(s, Stmt::Empty))
1303 .collect::<Vec<_>>()
1304 }),
1305 );
1306
1307 let else_branch = just(Token::Else)
1309 .ignore_then(just(Token::Newline).repeated())
1310 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1311 .map(|stmts: Vec<Stmt>| {
1312 stmts
1313 .into_iter()
1314 .filter(|s| !matches!(s, Stmt::Empty))
1315 .collect::<Vec<_>>()
1316 });
1317
1318 just(Token::If)
1319 .ignore_then(branch)
1320 .then(elif_branch.repeated().collect::<Vec<_>>())
1321 .then(else_branch.or_not())
1322 .then_ignore(just(Token::Fi))
1323 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1324 build_if_chain(condition, then_branch, elif_branches, else_branch)
1326 })
1327 .labelled("if statement")
1328 .boxed()
1329}
1330
1331fn build_if_chain(
1338 condition: Expr,
1339 then_branch: Vec<Stmt>,
1340 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1341 else_branch: Option<Vec<Stmt>>,
1342) -> IfStmt {
1343 if elif_branches.is_empty() {
1344 IfStmt {
1346 condition: Box::new(condition),
1347 then_branch,
1348 else_branch,
1349 }
1350 } else {
1351 let (elif_cond, elif_then) = elif_branches.remove(0);
1353 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1354 IfStmt {
1355 condition: Box::new(condition),
1356 then_branch,
1357 else_branch: Some(vec![Stmt::If(nested_if)]),
1358 }
1359 }
1360}
1361
1362fn for_parser<'tokens, I, S>(
1364 stmt: S,
1365) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1366where
1367 I: ValueInput<'tokens, Token = Token, Span = Span>,
1368 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1369{
1370 just(Token::For)
1371 .ignore_then(ident_parser())
1372 .then_ignore(just(Token::In))
1373 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1374 .then_ignore(just(Token::Semi).or_not())
1375 .then_ignore(just(Token::Newline).repeated())
1376 .then_ignore(just(Token::Do))
1377 .then_ignore(just(Token::Newline).repeated())
1378 .then(
1379 stmt.repeated()
1380 .collect::<Vec<_>>()
1381 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1382 )
1383 .then_ignore(just(Token::Done))
1384 .map(|((variable, items), body)| ForLoop {
1385 variable,
1386 items,
1387 body,
1388 })
1389 .labelled("for loop")
1390 .boxed()
1391}
1392
1393fn while_parser<'tokens, I, S>(
1395 stmt: S,
1396) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1397where
1398 I: ValueInput<'tokens, Token = Token, Span = Span>,
1399 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1400{
1401 just(Token::While)
1402 .ignore_then(condition_parser())
1403 .then_ignore(just(Token::Semi).or_not())
1404 .then_ignore(just(Token::Newline).repeated())
1405 .then_ignore(just(Token::Do))
1406 .then_ignore(just(Token::Newline).repeated())
1407 .then(
1408 stmt.repeated()
1409 .collect::<Vec<_>>()
1410 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1411 )
1412 .then_ignore(just(Token::Done))
1413 .map(|(condition, body)| WhileLoop {
1414 condition: Box::new(condition),
1415 body,
1416 })
1417 .labelled("while loop")
1418 .boxed()
1419}
1420
1421fn case_parser<'tokens, I, S>(
1428 stmt: S,
1429) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1430where
1431 I: ValueInput<'tokens, Token = Token, Span = Span>,
1432 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1433{
1434 let pattern_part = choice((
1437 select! { Token::GlobWord(s) => s },
1438 select! { Token::Ident(s) => s },
1439 select! { Token::NumberIdent(s) => s },
1440 select! { Token::DashNumWord(s) => s },
1441 select! { Token::AtWord(s) => s },
1442 select! { Token::DottedIdent(s) => s },
1443 select! { Token::String(s) => s },
1444 select! { Token::SingleString(s) => s },
1445 select! { Token::Int(n) => n.to_string() },
1446 select! { Token::Star => "*".to_string() },
1447 select! { Token::Question => "?".to_string() },
1448 select! { Token::Dot => ".".to_string() },
1449 select! { Token::DotDot => "..".to_string() },
1450 select! { Token::Tilde => "~".to_string() },
1451 select! { Token::TildePath(s) => s },
1452 select! { Token::RelativePath(s) => s },
1453 select! { Token::DotSlashPath(s) => s },
1454 select! { Token::Path(p) => p },
1455 select! { Token::VarRef(v) => v },
1456 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1457 just(Token::LBracket)
1459 .ignore_then(
1460 choice((
1461 select! { Token::Ident(s) => s },
1462 select! { Token::Int(n) => n.to_string() },
1463 just(Token::Colon).to(":".to_string()),
1464 just(Token::Bang).to("!".to_string()),
1466 select! { Token::ShortFlag(s) => format!("-{}", s) },
1468 ))
1469 .repeated()
1470 .at_least(1)
1471 .collect::<Vec<String>>()
1472 )
1473 .then_ignore(just(Token::RBracket))
1474 .map(|parts| format!("[{}]", parts.join(""))),
1475 just(Token::LBrace)
1477 .ignore_then(
1478 choice((
1479 select! { Token::Ident(s) => s },
1480 select! { Token::Int(n) => n.to_string() },
1481 ))
1482 .separated_by(just(Token::Comma))
1483 .at_least(1)
1484 .collect::<Vec<String>>()
1485 )
1486 .then_ignore(just(Token::RBrace))
1487 .map(|parts| format!("{{{}}}", parts.join(","))),
1488 ));
1489
1490 let pattern = pattern_part
1493 .repeated()
1494 .at_least(1)
1495 .collect::<Vec<String>>()
1496 .map(|parts| parts.join(""))
1497 .labelled("case pattern");
1498
1499 let patterns = pattern
1501 .separated_by(just(Token::Pipe))
1502 .at_least(1)
1503 .collect::<Vec<String>>()
1504 .labelled("case patterns");
1505
1506 let branch = just(Token::LParen)
1508 .or_not()
1509 .ignore_then(just(Token::Newline).repeated())
1510 .ignore_then(patterns)
1511 .then_ignore(just(Token::RParen))
1512 .then_ignore(just(Token::Newline).repeated())
1513 .then(
1514 stmt.clone()
1515 .repeated()
1516 .collect::<Vec<_>>()
1517 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1518 )
1519 .then_ignore(just(Token::DoubleSemi))
1520 .then_ignore(just(Token::Newline).repeated())
1521 .map(|(patterns, body)| CaseBranch { patterns, body })
1522 .labelled("case branch");
1523
1524 just(Token::Case)
1525 .ignore_then(expr_parser())
1526 .then_ignore(just(Token::In))
1527 .then_ignore(just(Token::Newline).repeated())
1528 .then(branch.repeated().collect::<Vec<_>>())
1529 .then_ignore(just(Token::Esac))
1530 .map(|(expr, branches)| CaseStmt { expr, branches })
1531 .labelled("case statement")
1532 .boxed()
1533}
1534
1535fn pipeline_parser<'tokens, I>(
1537) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1538where
1539 I: ValueInput<'tokens, Token = Token, Span = Span>,
1540{
1541 command_parser()
1542 .separated_by(just(Token::Pipe))
1543 .at_least(1)
1544 .collect::<Vec<_>>()
1545 .then(just(Token::Amp).or_not())
1546 .map(|(commands, bg)| Pipeline {
1547 commands,
1548 background: bg.is_some(),
1549 })
1550 .labelled("pipeline")
1551 .boxed()
1552}
1553
1554fn command_parser<'tokens, I>(
1557) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1558where
1559 I: ValueInput<'tokens, Token = Token, Span = Span>,
1560{
1561 let command_name = choice((
1563 ident_parser(),
1564 path_parser(),
1565 select! { Token::DotSlashPath(s) => s },
1566 just(Token::True).to("true".to_string()),
1567 just(Token::False).to("false".to_string()),
1568 just(Token::Dot).to(".".to_string()),
1569 ));
1570
1571 command_name
1581 .then(args_list_parser())
1582 .then(redirect_parser(primary_expr_parser()).repeated().collect::<Vec<_>>())
1583 .map(|((name, args), redirects)| Command {
1584 name,
1585 args,
1586 redirects,
1587 })
1588 .labelled("command")
1589 .boxed()
1590}
1591
1592fn pipeline_into_stmt(p: Pipeline) -> Stmt {
1597 if p.commands.len() == 1 && !p.background && p.commands[0].redirects.is_empty() {
1598 match p.commands.into_iter().next() {
1599 Some(cmd) => Stmt::Command(cmd),
1600 None => Stmt::Empty, }
1602 } else {
1603 Stmt::Pipeline(p)
1604 }
1605}
1606
1607fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
1611 cmd.redirects
1612 .iter()
1613 .filter(|r| {
1614 matches!(
1615 r.kind,
1616 RedirectKind::Stdin | RedirectKind::HereDoc | RedirectKind::HereString
1617 )
1618 })
1619 .count()
1620 > 1
1621}
1622
1623fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
1627 stmts.iter().any(stmt_has_ambiguous_stdin)
1628}
1629
1630fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
1631 match stmt {
1632 Stmt::Command(c) => command_has_ambiguous_stdin(c),
1633 Stmt::Pipeline(p) => p.commands.iter().any(command_has_ambiguous_stdin),
1634 Stmt::If(i) => {
1635 first_ambiguous_stdin(&i.then_branch)
1636 || i.else_branch
1637 .as_deref()
1638 .is_some_and(first_ambiguous_stdin)
1639 }
1640 Stmt::For(f) => first_ambiguous_stdin(&f.body),
1641 Stmt::While(w) => first_ambiguous_stdin(&w.body),
1642 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
1643 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
1644 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
1645 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
1646 }
1647 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
1648 Stmt::Assignment(_)
1649 | Stmt::Break(_)
1650 | Stmt::Continue(_)
1651 | Stmt::Return(_)
1652 | Stmt::Exit(_)
1653 | Stmt::Test(_)
1654 | Stmt::Empty => false,
1655 }
1656}
1657
1658fn is_comma_literal_arg(arg: &Arg) -> bool {
1661 matches!(arg, Arg::Positional(Expr::Literal(Value::String(s))) if s == ",")
1662}
1663
1664fn args_list_parser<'tokens, I>(
1668) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1669where
1670 I: ValueInput<'tokens, Token = Token, Span = Span>,
1671{
1672 let pre_dash = arg_before_double_dash_parser()
1679 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1680 .repeated()
1681 .collect::<Vec<(Arg, Span)>>()
1682 .try_map(|args, _span| {
1683 for pair in args.windows(2) {
1684 let (prev, prev_span) = &pair[0];
1685 let (next, next_span) = &pair[1];
1686 if matches!(prev, Arg::Positional(_))
1687 && matches!(next, Arg::Positional(_))
1688 && prev_span.end == next_span.start
1689 {
1690 let msg = if is_comma_literal_arg(prev) || is_comma_literal_arg(next) {
1695 "an unquoted comma splits this into separate words — kaish reserves \
1696 `,` (brace expansion, lists); quote a comma-bearing argument to keep \
1697 it one word, e.g. cut -f \"1,3\", sort -k \"2,2n\", or echo \"a,b\""
1698 } else {
1699 "adjacent words with no space between them are not joined into one \
1700 argument (kaish does no token pasting); quote the whole word, e.g. \
1701 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\""
1702 };
1703 return Err(Rich::custom(*next_span, msg));
1704 }
1705 }
1706 Ok(args.into_iter().map(|(arg, _)| arg).collect::<Vec<Arg>>())
1707 });
1708
1709 let double_dash = select! {
1711 Token::DoubleDash => Arg::DoubleDash,
1712 };
1713
1714 let post_dash_arg = choice((
1716 select! {
1718 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
1719 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
1720 },
1721 word_assign_arg_parser(),
1728 test_operator_arg_parser(),
1730 primary_expr_parser().map(Arg::Positional),
1732 ));
1733
1734 let post_dash = post_dash_arg.repeated().collect::<Vec<_>>();
1735
1736 pre_dash
1738 .then(double_dash.then(post_dash).or_not())
1739 .map(|(mut args, maybe_dd)| {
1740 if let Some((dd, post)) = maybe_dd {
1741 args.push(dd);
1742 args.extend(post);
1743 }
1744 args
1745 })
1746}
1747
1748fn keyword_word<'tokens, I>(
1757) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1758where
1759 I: ValueInput<'tokens, Token = Token, Span = Span>,
1760{
1761 select! {
1762 Token::Set => "set",
1763 Token::Local => "local",
1764 Token::If => "if",
1765 Token::Then => "then",
1766 Token::Else => "else",
1767 Token::Elif => "elif",
1768 Token::Fi => "fi",
1769 Token::For => "for",
1770 Token::While => "while",
1771 Token::In => "in",
1772 Token::Do => "do",
1773 Token::Done => "done",
1774 Token::Case => "case",
1775 Token::Esac => "esac",
1776 Token::Function => "function",
1777 Token::Break => "break",
1778 Token::Continue => "continue",
1779 Token::Return => "return",
1780 Token::Exit => "exit",
1781 }
1782 .map(|s| s.to_string())
1783}
1784
1785fn word_assign_arg_parser<'tokens, I>(
1795) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1796where
1797 I: ValueInput<'tokens, Token = Token, Span = Span>,
1798{
1799 choice((
1800 select! { Token::Ident(s) => s },
1801 keyword_word(),
1802 ))
1803 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1804 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1805 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1806 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1807 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1809 Err(Rich::custom(
1810 span,
1811 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
1812 ))
1813 } else {
1814 Ok(Arg::WordAssign { key, value })
1815 }
1816 })
1817}
1818
1819fn test_operator_arg_parser<'tokens, I>(
1836) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1837where
1838 I: ValueInput<'tokens, Token = Token, Span = Span>,
1839{
1840 select! {
1841 Token::Eq => "=",
1842 Token::EqEq => "==",
1843 Token::NotEq => "!=",
1844 Token::Bang => "!",
1845 }
1846 .map(|s| Arg::Positional(Expr::Literal(Value::String(s.to_string()))))
1847}
1848
1849fn arg_before_double_dash_parser<'tokens, I>(
1851) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1852where
1853 I: ValueInput<'tokens, Token = Token, Span = Span>,
1854{
1855 let long_flag_with_value = select! {
1857 Token::LongFlag(name) => name,
1858 }
1859 .then_ignore(just(Token::Eq))
1860 .then(primary_expr_parser())
1861 .map(|(key, value)| Arg::Named { key, value });
1862
1863 let long_flag = select! {
1865 Token::LongFlag(name) => Arg::LongFlag(name),
1866 };
1867
1868 let short_flag = select! {
1870 Token::ShortFlag(name) => Arg::ShortFlag(name),
1871 };
1872
1873 let named = word_assign_arg_parser();
1875
1876 let positional = primary_expr_parser().map(Arg::Positional);
1878
1879 let test_operator = test_operator_arg_parser();
1883
1884 choice((
1887 long_flag_with_value,
1888 long_flag,
1889 short_flag,
1890 named,
1891 test_operator,
1892 positional,
1893 ))
1894 .boxed()
1895}
1896
1897fn redirect_parser<'tokens, I, T>(
1907 target: T,
1908) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1909where
1910 I: ValueInput<'tokens, Token = Token, Span = Span>,
1911 T: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1912{
1913 let regular_redirect = select! {
1915 Token::GtGt => RedirectKind::StdoutAppend,
1916 Token::Gt => RedirectKind::StdoutOverwrite,
1917 Token::Lt => RedirectKind::Stdin,
1918 Token::Stderr => RedirectKind::Stderr,
1919 Token::Both => RedirectKind::Both,
1920 }
1921 .then(target.clone())
1922 .map(|(kind, target)| Redirect { kind, target });
1923
1924 let heredoc_redirect = just(Token::HereDocStart)
1932 .ignore_then(select! { Token::HereDoc(data) => data })
1933 .map(|data: HereDocData| {
1934 let target = if data.literal {
1935 let body = if data.strip_tabs {
1936 crate::interpreter::strip_leading_tabs(&data.content)
1937 } else {
1938 data.content
1939 };
1940 Expr::Literal(Value::String(body))
1941 } else {
1942 let parts = parse_interpolated_string_spanned(
1943 &data.content,
1944 data.body_start_offset,
1945 );
1946 if parts.len() == 1 && !data.strip_tabs {
1950 if let StringPart::Literal(text) = &parts[0].part {
1951 return Redirect {
1952 kind: RedirectKind::HereDoc,
1953 target: Expr::Literal(Value::String(text.clone())),
1954 };
1955 }
1956 }
1957 Expr::HereDocBody {
1958 parts,
1959 strip_tabs: data.strip_tabs,
1960 }
1961 };
1962 Redirect {
1963 kind: RedirectKind::HereDoc,
1964 target,
1965 }
1966 });
1967
1968 let herestring_redirect = just(Token::HereString)
1972 .ignore_then(target.clone())
1973 .map(|target| Redirect {
1974 kind: RedirectKind::HereString,
1975 target,
1976 });
1977
1978 let merge_stderr_redirect = just(Token::StderrToStdout)
1980 .map(|_| Redirect {
1981 kind: RedirectKind::MergeStderr,
1982 target: Expr::Literal(Value::Null),
1984 });
1985
1986 let merge_stdout_redirect = choice((
1988 just(Token::StdoutToStderr),
1989 just(Token::StdoutToStderr2),
1990 ))
1991 .map(|_| Redirect {
1992 kind: RedirectKind::MergeStdout,
1993 target: Expr::Literal(Value::Null),
1995 });
1996
1997 choice((
1998 heredoc_redirect,
1999 herestring_redirect,
2000 merge_stderr_redirect,
2001 merge_stdout_redirect,
2002 regular_redirect,
2003 ))
2004 .labelled("redirect")
2005 .boxed()
2006}
2007
2008fn test_expr_stmt_parser<'tokens, I>(
2020) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2021where
2022 I: ValueInput<'tokens, Token = Token, Span = Span>,
2023{
2024 let file_test_op = select! {
2026 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
2027 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
2028 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
2029 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
2030 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
2031 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
2032 };
2033
2034 let string_test_op = select! {
2038 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
2039 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
2040 Token::ShortFlag(s) if s == "list" => StringTestOp::IsList,
2041 Token::ShortFlag(s) if s == "record" => StringTestOp::IsRecord,
2042 };
2043
2044 let cmp_op = choice((
2047 just(Token::EqEq).to(TestCmpOp::Eq),
2048 just(Token::Eq).to(TestCmpOp::Eq),
2049 just(Token::NotEq).to(TestCmpOp::NotEq),
2050 just(Token::Match).to(TestCmpOp::Match),
2051 just(Token::NotMatch).to(TestCmpOp::NotMatch),
2052 just(Token::Gt).to(TestCmpOp::Gt),
2053 just(Token::Lt).to(TestCmpOp::Lt),
2054 just(Token::GtEq).to(TestCmpOp::GtEq),
2055 just(Token::LtEq).to(TestCmpOp::LtEq),
2056 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
2057 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
2058 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
2059 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
2060 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
2061 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
2062 ));
2063
2064 let file_test = file_test_op
2066 .then(primary_expr_parser())
2067 .map(|(op, path)| TestExpr::FileTest {
2068 op,
2069 path: Box::new(path),
2070 });
2071
2072 let string_test = string_test_op
2074 .then(primary_expr_parser())
2075 .map(|(op, value)| TestExpr::StringTest {
2076 op,
2077 value: Box::new(value),
2078 });
2079
2080 let comparison = primary_expr_parser()
2082 .then(cmp_op)
2083 .then(primary_expr_parser())
2084 .map(|((left, op), right)| TestExpr::Comparison {
2085 left: Box::new(left),
2086 op,
2087 right: Box::new(right),
2088 });
2089
2090 let not_in = primary_expr_parser()
2097 .then_ignore(select! { Token::Ident(s) if s == "not" => () })
2098 .then_ignore(just(Token::In))
2099 .then(value_primary_parser())
2100 .map(|(left, right)| TestExpr::NotIn {
2101 left: Box::new(left),
2102 right: Box::new(right),
2103 });
2104
2105 let in_ = primary_expr_parser()
2106 .then_ignore(just(Token::In))
2107 .then(value_primary_parser())
2108 .map(|(left, right)| TestExpr::In {
2109 left: Box::new(left),
2110 right: Box::new(right),
2111 });
2112
2113 let primary_test = choice((file_test, string_test, not_in, in_, comparison));
2115
2116 let unary = recursive(|unary| {
2130 let not_expr = just(Token::Bang)
2131 .ignore_then(unary)
2132 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
2133 choice((not_expr, primary_test.clone()))
2134 });
2135
2136 let and_expr = unary.clone().foldl(
2138 just(Token::And).ignore_then(unary).repeated(),
2139 |left, right| TestExpr::And {
2140 left: Box::new(left),
2141 right: Box::new(right),
2142 },
2143 );
2144
2145 let compound_test = and_expr.clone().foldl(
2147 just(Token::Or).ignore_then(and_expr).repeated(),
2148 |left, right| TestExpr::Or {
2149 left: Box::new(left),
2150 right: Box::new(right),
2151 },
2152 );
2153
2154 just(Token::LBracket)
2157 .then(just(Token::LBracket))
2158 .ignore_then(compound_test)
2159 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
2160 .labelled("test expression")
2161 .boxed()
2162}
2163
2164fn condition_parser<'tokens, I>(
2179) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2180where
2181 I: ValueInput<'tokens, Token = Token, Span = Span>,
2182{
2183 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
2185
2186 let command_condition = command_parser().map(Expr::Command);
2189
2190 let base = choice((test_expr_condition, command_condition));
2192
2193 let and_expr = base.clone().foldl(
2196 just(Token::And).ignore_then(base).repeated(),
2197 |left, right| Expr::BinaryOp {
2198 left: Box::new(left),
2199 op: BinaryOp::And,
2200 right: Box::new(right),
2201 },
2202 );
2203
2204 and_expr
2206 .clone()
2207 .foldl(
2208 just(Token::Or).ignore_then(and_expr).repeated(),
2209 |left, right| Expr::BinaryOp {
2210 left: Box::new(left),
2211 op: BinaryOp::Or,
2212 right: Box::new(right),
2213 },
2214 )
2215 .labelled("condition")
2216 .boxed()
2217}
2218
2219fn expr_parser<'tokens, I>(
2226) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2227where
2228 I: ValueInput<'tokens, Token = Token, Span = Span>,
2229{
2230 primary_expr_parser()
2232}
2233
2234fn value_expr_parser<'tokens, I>(
2239) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2240where
2241 I: ValueInput<'tokens, Token = Token, Span = Span>,
2242{
2243 value_literal_parser()
2244}
2245
2246fn value_primary_parser<'tokens, I>(
2252) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2253where
2254 I: ValueInput<'tokens, Token = Token, Span = Span>,
2255{
2256 value_literal_parser()
2257}
2258
2259fn value_literal_parser<'tokens, I>(
2271) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2272where
2273 I: ValueInput<'tokens, Token = Token, Span = Span>,
2274{
2275 recursive(|value| {
2276 choice((
2277 list_literal_parser(value.clone()),
2278 record_literal_parser(value.clone()),
2279 primary_expr_parser(),
2280 ))
2281 })
2282 .boxed()
2283}
2284
2285fn list_literal_parser<'tokens, I, V>(
2293 value: V,
2294) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2295where
2296 I: ValueInput<'tokens, Token = Token, Span = Span>,
2297 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2298{
2299 let spread_elem = just(Token::DotDotDot)
2300 .ignore_then(value.clone())
2301 .map(ListElem::Spread);
2302 let item_elem = value.map(ListElem::Item);
2303 let elem = choice((spread_elem, item_elem));
2304
2305 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2306
2307 just(Token::LBracket)
2308 .ignore_then(just(Token::Newline).repeated())
2309 .ignore_then(elem.then_ignore(sep).repeated().collect::<Vec<_>>())
2310 .then_ignore(just(Token::RBracket))
2311 .map(Expr::ListLiteral)
2312 .labelled("list literal")
2313}
2314
2315fn record_literal_parser<'tokens, I, V>(
2323 value: V,
2324) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2325where
2326 I: ValueInput<'tokens, Token = Token, Span = Span>,
2327 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2328{
2329 let bare_key = select! { Token::Ident(s) => RecordKey::Bare(s) };
2330 let double_key = select! { Token::String(s) => s }.try_map(|s, span| {
2336 let parts = parse_interpolated_string(&s)
2337 .map_err(|e| Rich::custom(span, format!("record key: {e}")))?;
2338 Ok(match parts.as_slice() {
2339 [] => RecordKey::Quoted(String::new()),
2340 [StringPart::Literal(lit)] => RecordKey::Quoted(lit.clone()),
2341 _ => RecordKey::Interpolated(parts),
2342 })
2343 });
2344 let single_key = select! { Token::SingleString(s) => RecordKey::Quoted(s) };
2345 let key = choice((double_key, single_key, bare_key)).labelled("record key");
2346
2347 let entry = key
2348 .then_ignore(just(Token::Colon))
2349 .then(value)
2350 .map(|(key, value)| RecordEntry { key, value });
2351
2352 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2353
2354 just(Token::LBrace)
2355 .ignore_then(just(Token::Newline).repeated())
2356 .ignore_then(entry.then_ignore(sep).repeated().collect::<Vec<_>>())
2357 .then_ignore(just(Token::RBrace))
2358 .map(Expr::RecordLiteral)
2359 .labelled("record literal")
2360}
2361
2362fn primary_expr_parser<'tokens, I>(
2366) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2367where
2368 I: ValueInput<'tokens, Token = Token, Span = Span>,
2369{
2370 let positional = select! {
2372 Token::Positional(n) => Expr::Positional(n),
2373 Token::AllArgs => Expr::AllArgs,
2374 Token::ArgCount => Expr::ArgCount,
2375 Token::VarLength(name) => Expr::VarLength(parse_varpath(&format!("${{{name}}}"))),
2376 Token::LastExitCode => Expr::LastExitCode,
2377 Token::CurrentPid => Expr::CurrentPid,
2378 };
2379
2380 let arithmetic = select! {
2382 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
2383 };
2384
2385 let keyword_as_bareword = select! {
2388 Token::Done => "done",
2389 Token::Fi => "fi",
2390 Token::Then => "then",
2391 Token::Else => "else",
2392 Token::Elif => "elif",
2393 Token::In => "in",
2394 Token::Do => "do",
2395 Token::Esac => "esac",
2396 Token::Set => "set",
2401 }
2402 .map(|s| Expr::Literal(Value::String(s.to_string())));
2403
2404 let plus_minus_bare = select! {
2406 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
2407 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
2408 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
2409 };
2410
2411 let glob_pattern = select! {
2413 Token::GlobWord(s) => Expr::GlobPattern(s),
2414 Token::Star => Expr::GlobPattern("*".to_string()),
2415 Token::Question => Expr::GlobPattern("?".to_string()),
2416 };
2417
2418 recursive(|expr| {
2419 choice((
2420 positional,
2421 arithmetic,
2422 cmd_subst_parser(expr.clone()),
2423 var_expr_parser(),
2424 interpolated_string_parser(),
2425 literal_parser().map(Expr::Literal),
2426 glob_pattern,
2428 ident_parser().map(|s| Expr::Literal(Value::String(s))),
2430 path_parser().map(|s| Expr::Literal(Value::String(s))),
2432 select! {
2435 Token::Dot => Expr::Literal(Value::String(".".into())),
2441 Token::DotDot => Expr::Literal(Value::String("..".into())),
2442 Token::Comma => Expr::Literal(Value::String(",".into())),
2449 Token::Colon => Expr::Literal(Value::String(":".into())),
2456 Token::Tilde => Expr::Literal(Value::String("~".into())),
2457 Token::TildePath(s) => Expr::Literal(Value::String(s)),
2458 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
2459 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
2460 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
2462 Token::DashNumWord(s) => Expr::Literal(Value::String(s)),
2465 Token::AtWord(s) => Expr::Literal(Value::String(s)),
2467 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
2472 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
2475 },
2476 plus_minus_bare,
2477 keyword_as_bareword,
2479 ))
2480 .labelled("expression")
2481 })
2482 .boxed()
2483}
2484
2485fn var_expr_parser<'tokens, I>(
2488) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2489where
2490 I: ValueInput<'tokens, Token = Token, Span = Span>,
2491{
2492 select! {
2493 Token::VarRef(raw) => parse_var_expr(&raw),
2494 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
2495 }
2496 .labelled("variable reference")
2497}
2498
2499fn cmd_subst_parser<'tokens, I, E>(
2503 expr: E,
2504) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2505where
2506 I: ValueInput<'tokens, Token = Token, Span = Span>,
2507 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2508{
2509 let long_flag_with_value = select! {
2512 Token::LongFlag(name) => name,
2513 }
2514 .then_ignore(just(Token::Eq))
2515 .then(expr.clone())
2516 .map(|(key, value)| Arg::Named { key, value });
2517
2518 let long_flag = select! {
2520 Token::LongFlag(name) => Arg::LongFlag(name),
2521 };
2522
2523 let short_flag = select! {
2525 Token::ShortFlag(name) => Arg::ShortFlag(name),
2526 };
2527
2528 let named = choice((ident_parser(), keyword_word()))
2531 .then_ignore(just(Token::Eq))
2532 .then(expr.clone())
2533 .map(|(key, value)| Arg::WordAssign { key, value });
2534
2535 let positional = expr.clone().map(Arg::Positional);
2537
2538 let arg = choice((
2539 long_flag_with_value,
2540 long_flag,
2541 short_flag,
2542 named,
2543 positional,
2544 ));
2545
2546 let command_name = choice((
2548 ident_parser(),
2549 just(Token::True).to("true".to_string()),
2550 just(Token::False).to("false".to_string()),
2551 ));
2552
2553 let command = command_name
2560 .then(arg.repeated().collect::<Vec<_>>())
2561 .then(
2562 redirect_parser(expr.clone())
2563 .repeated()
2564 .collect::<Vec<_>>(),
2565 )
2566 .map(|((name, args), redirects)| Command {
2567 name,
2568 args,
2569 redirects,
2570 });
2571
2572 let pipeline = command
2574 .separated_by(just(Token::Pipe))
2575 .at_least(1)
2576 .collect::<Vec<_>>()
2577 .map(|commands| Pipeline {
2578 commands,
2579 background: false,
2580 });
2581
2582 let pipeline_stmt = pipeline.map(pipeline_into_stmt);
2585
2586 let chained = pipeline_stmt.clone().foldl(
2594 choice((
2595 just(Token::And).to(true), just(Token::Or).to(false), ))
2598 .then(pipeline_stmt.clone())
2599 .repeated(),
2600 |left, (is_and, right): (bool, Stmt)| {
2601 if is_and {
2602 Stmt::AndChain {
2603 left: Box::new(left),
2604 right: Box::new(right),
2605 }
2606 } else {
2607 Stmt::OrChain {
2608 left: Box::new(left),
2609 right: Box::new(right),
2610 }
2611 }
2612 },
2613 );
2614
2615 let separator = choice((just(Token::Newline), just(Token::Semi)));
2620 let body = separator
2621 .clone()
2622 .repeated()
2623 .ignore_then(
2624 chained
2625 .separated_by(separator.clone().repeated().at_least(1))
2626 .allow_trailing()
2627 .collect::<Vec<_>>(),
2628 )
2629 .then_ignore(separator.repeated());
2630
2631 just(Token::CmdSubstStart)
2632 .ignore_then(body)
2633 .then_ignore(just(Token::RParen))
2634 .map(Expr::CommandSubst)
2635 .labelled("command substitution")
2636}
2637
2638fn interpolated_string_parser<'tokens, I>(
2640) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2641where
2642 I: ValueInput<'tokens, Token = Token, Span = Span>,
2643{
2644 let double_quoted = select! {
2646 Token::String(s) => s,
2647 }
2648 .try_map(|s, span| {
2649 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
2651 let parts = parse_interpolated_string(&s)
2654 .map_err(|msg| Rich::custom(span, msg))?;
2655 if parts.len() == 1
2656 && let StringPart::Literal(text) = &parts[0] {
2657 return Ok(Expr::Literal(Value::String(text.clone())));
2658 }
2659 Ok(Expr::Interpolated(parts))
2660 } else {
2661 Ok(Expr::Literal(Value::String(s)))
2662 }
2663 });
2664
2665 let single_quoted = select! {
2667 Token::SingleString(s) => Expr::Literal(Value::String(s)),
2668 };
2669
2670 choice((single_quoted, double_quoted)).labelled("string")
2671}
2672
2673fn literal_parser<'tokens, I>(
2675) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2676where
2677 I: ValueInput<'tokens, Token = Token, Span = Span>,
2678{
2679 choice((
2680 select! {
2681 Token::True => Value::Bool(true),
2682 Token::False => Value::Bool(false),
2683 },
2684 select! {
2685 Token::Int(n) => Value::Int(n),
2686 Token::Float(f) => Value::Float(f),
2687 },
2688 ))
2689 .labelled("literal")
2690 .boxed()
2691}
2692
2693fn ident_parser<'tokens, I>(
2695) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2696where
2697 I: ValueInput<'tokens, Token = Token, Span = Span>,
2698{
2699 select! {
2700 Token::Ident(s) => s,
2701 }
2702 .labelled("identifier")
2703}
2704
2705fn path_parser<'tokens, I>(
2707) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2708where
2709 I: ValueInput<'tokens, Token = Token, Span = Span>,
2710{
2711 select! {
2712 Token::Path(s) => s,
2713 }
2714 .labelled("path")
2715}
2716
2717#[cfg(test)]
2718#[allow(clippy::approx_constant)]
2719mod tests {
2720 use super::*;
2721
2722 fn subst_cmd(expr: &Expr) -> &Command {
2724 match expr {
2725 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2726 [Stmt::Command(cmd)] => cmd,
2727 other => panic!("expected a single command in $(), got {other:?}"),
2728 },
2729 other => panic!("expected command subst, got {other:?}"),
2730 }
2731 }
2732
2733 fn subst_pipeline(expr: &Expr) -> &Pipeline {
2735 match expr {
2736 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2737 [Stmt::Pipeline(p)] => p,
2738 other => panic!("expected a single pipeline in $(), got {other:?}"),
2739 },
2740 other => panic!("expected command subst, got {other:?}"),
2741 }
2742 }
2743
2744 #[test]
2745 fn parse_empty() {
2746 let result = parse("");
2747 assert!(result.is_ok());
2748 assert_eq!(result.expect("ok").statements.len(), 0);
2749 }
2750
2751 #[test]
2752 fn parse_newlines_only() {
2753 let result = parse("\n\n\n");
2754 assert!(result.is_ok());
2755 }
2756
2757 #[test]
2758 fn parse_simple_command() {
2759 let result = parse("echo");
2760 assert!(result.is_ok());
2761 let program = result.expect("ok");
2762 assert_eq!(program.statements.len(), 1);
2763 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2764 }
2765
2766 #[test]
2767 fn parse_command_with_string_arg() {
2768 let result = parse(r#"echo "hello""#);
2769 assert!(result.is_ok());
2770 let program = result.expect("ok");
2771 match &program.statements[0] {
2772 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
2773 _ => panic!("expected Command"),
2774 }
2775 }
2776
2777 #[test]
2778 fn parse_assignment() {
2779 let result = parse("X=5");
2780 assert!(result.is_ok());
2781 let program = result.expect("ok");
2782 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2783 }
2784
2785 #[test]
2786 fn parse_pipeline() {
2787 let result = parse("a | b | c");
2788 assert!(result.is_ok());
2789 let program = result.expect("ok");
2790 match &program.statements[0] {
2791 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2792 _ => panic!("expected Pipeline"),
2793 }
2794 }
2795
2796 #[test]
2797 fn parse_background_job() {
2798 let result = parse("cmd &");
2799 assert!(result.is_ok());
2800 let program = result.expect("ok");
2801 match &program.statements[0] {
2802 Stmt::Pipeline(p) => assert!(p.background),
2803 _ => panic!("expected Pipeline with background"),
2804 }
2805 }
2806
2807 #[test]
2808 fn parse_if_simple() {
2809 let result = parse("if true; then echo; fi");
2810 assert!(result.is_ok());
2811 let program = result.expect("ok");
2812 assert!(matches!(&program.statements[0], Stmt::If(_)));
2813 }
2814
2815 #[test]
2816 fn parse_if_else() {
2817 let result = parse("if true; then echo; else echo; fi");
2818 assert!(result.is_ok());
2819 let program = result.expect("ok");
2820 match &program.statements[0] {
2821 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
2822 _ => panic!("expected If"),
2823 }
2824 }
2825
2826 #[test]
2827 fn parse_elif_simple() {
2828 let result = parse("if true; then echo a; elif false; then echo b; fi");
2829 assert!(result.is_ok(), "parse failed: {:?}", result);
2830 let program = result.expect("ok");
2831 match &program.statements[0] {
2832 Stmt::If(if_stmt) => {
2833 assert!(if_stmt.else_branch.is_some());
2835 let else_branch = if_stmt.else_branch.as_ref().unwrap();
2836 assert_eq!(else_branch.len(), 1);
2837 assert!(matches!(&else_branch[0], Stmt::If(_)));
2838 }
2839 _ => panic!("expected If"),
2840 }
2841 }
2842
2843 #[test]
2844 fn parse_elif_with_else() {
2845 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
2846 assert!(result.is_ok(), "parse failed: {:?}", result);
2847 let program = result.expect("ok");
2848 match &program.statements[0] {
2849 Stmt::If(outer_if) => {
2850 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
2852 assert_eq!(else_branch.len(), 1);
2853 match &else_branch[0] {
2854 Stmt::If(inner_if) => {
2855 assert!(inner_if.else_branch.is_some());
2857 }
2858 _ => panic!("expected nested If from elif"),
2859 }
2860 }
2861 _ => panic!("expected If"),
2862 }
2863 }
2864
2865 #[test]
2866 fn parse_multiple_elif() {
2867 let result = parse(
2869 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
2870 );
2871 assert!(result.is_ok(), "parse failed: {:?}", result);
2872 }
2873
2874 #[test]
2875 fn parse_for_loop() {
2876 let result = parse("for X in items; do echo; done");
2877 assert!(result.is_ok());
2878 let program = result.expect("ok");
2879 assert!(matches!(&program.statements[0], Stmt::For(_)));
2880 }
2881
2882 #[test]
2883 fn parse_brackets_not_array_literal() {
2884 let result = parse("cmd [1");
2886 let _ = result;
2889 }
2890
2891 #[test]
2892 fn parse_named_arg() {
2893 let result = parse("cmd foo=5");
2897 assert!(result.is_ok());
2898 let program = result.expect("ok");
2899 match &program.statements[0] {
2900 Stmt::Command(cmd) => {
2901 assert_eq!(cmd.args.len(), 1);
2902 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
2903 }
2904 _ => panic!("expected Command"),
2905 }
2906 }
2907
2908 #[test]
2909 fn parse_short_flag() {
2910 let result = parse("ls -l");
2911 assert!(result.is_ok());
2912 let program = result.expect("ok");
2913 match &program.statements[0] {
2914 Stmt::Command(cmd) => {
2915 assert_eq!(cmd.name, "ls");
2916 assert_eq!(cmd.args.len(), 1);
2917 match &cmd.args[0] {
2918 Arg::ShortFlag(name) => assert_eq!(name, "l"),
2919 _ => panic!("expected ShortFlag"),
2920 }
2921 }
2922 _ => panic!("expected Command"),
2923 }
2924 }
2925
2926 #[test]
2927 fn parse_long_flag() {
2928 let result = parse("git push --force");
2929 assert!(result.is_ok());
2930 let program = result.expect("ok");
2931 match &program.statements[0] {
2932 Stmt::Command(cmd) => {
2933 assert_eq!(cmd.name, "git");
2934 assert_eq!(cmd.args.len(), 2);
2935 match &cmd.args[0] {
2936 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
2937 _ => panic!("expected Positional push"),
2938 }
2939 match &cmd.args[1] {
2940 Arg::LongFlag(name) => assert_eq!(name, "force"),
2941 _ => panic!("expected LongFlag"),
2942 }
2943 }
2944 _ => panic!("expected Command"),
2945 }
2946 }
2947
2948 #[test]
2949 fn parse_long_flag_with_value() {
2950 let result = parse(r#"git commit --message="hello""#);
2951 assert!(result.is_ok());
2952 let program = result.expect("ok");
2953 match &program.statements[0] {
2954 Stmt::Command(cmd) => {
2955 assert_eq!(cmd.name, "git");
2956 assert_eq!(cmd.args.len(), 2);
2957 match &cmd.args[1] {
2958 Arg::Named { key, value } => {
2959 assert_eq!(key, "message");
2960 match value {
2961 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
2962 _ => panic!("expected String value"),
2963 }
2964 }
2965 _ => panic!("expected Named from --flag=value"),
2966 }
2967 }
2968 _ => panic!("expected Command"),
2969 }
2970 }
2971
2972 #[test]
2973 fn parse_mixed_flags_and_args() {
2974 let result = parse(r#"git commit -m "message" --amend"#);
2975 assert!(result.is_ok());
2976 let program = result.expect("ok");
2977 match &program.statements[0] {
2978 Stmt::Command(cmd) => {
2979 assert_eq!(cmd.name, "git");
2980 assert_eq!(cmd.args.len(), 4);
2981 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
2983 match &cmd.args[1] {
2985 Arg::ShortFlag(name) => assert_eq!(name, "m"),
2986 _ => panic!("expected ShortFlag -m"),
2987 }
2988 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
2990 match &cmd.args[3] {
2992 Arg::LongFlag(name) => assert_eq!(name, "amend"),
2993 _ => panic!("expected LongFlag --amend"),
2994 }
2995 }
2996 _ => panic!("expected Command"),
2997 }
2998 }
2999
3000 #[test]
3001 fn parse_redirect_stdout() {
3002 let result = parse("cmd > file");
3003 assert!(result.is_ok());
3004 let program = result.expect("ok");
3005 match &program.statements[0] {
3007 Stmt::Pipeline(p) => {
3008 assert_eq!(p.commands.len(), 1);
3009 let cmd = &p.commands[0];
3010 assert_eq!(cmd.redirects.len(), 1);
3011 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
3012 }
3013 _ => panic!("expected Pipeline"),
3014 }
3015 }
3016
3017 #[test]
3018 fn parse_var_ref() {
3019 let result = parse("echo ${VAR}");
3020 assert!(result.is_ok());
3021 let program = result.expect("ok");
3022 match &program.statements[0] {
3023 Stmt::Command(cmd) => {
3024 assert_eq!(cmd.args.len(), 1);
3025 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
3026 }
3027 _ => panic!("expected Command"),
3028 }
3029 }
3030
3031 #[test]
3032 fn parse_multiple_statements() {
3033 let result = parse("a\nb\nc");
3034 assert!(result.is_ok());
3035 let program = result.expect("ok");
3036 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3037 assert_eq!(non_empty.len(), 3);
3038 }
3039
3040 #[test]
3041 fn parse_semicolon_separated() {
3042 let result = parse("a; b; c");
3043 assert!(result.is_ok());
3044 let program = result.expect("ok");
3045 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3046 assert_eq!(non_empty.len(), 3);
3047 }
3048
3049 #[test]
3050 fn parse_complex_pipeline() {
3051 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
3052 assert!(result.is_ok());
3053 let program = result.expect("ok");
3054 match &program.statements[0] {
3055 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
3056 _ => panic!("expected Pipeline"),
3057 }
3058 }
3059
3060 #[test]
3061 fn parse_json_as_string_arg() {
3062 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
3064 assert!(result.is_ok());
3065 }
3066
3067 #[test]
3068 fn parse_mixed_args() {
3069 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
3070 assert!(result.is_ok());
3071 let program = result.expect("ok");
3072 match &program.statements[0] {
3073 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
3074 _ => panic!("expected Command"),
3075 }
3076 }
3077
3078 #[test]
3079 fn error_unterminated_string() {
3080 let result = parse(r#"echo "hello"#);
3081 assert!(result.is_err());
3082 }
3083
3084 #[test]
3085 fn error_unterminated_var_ref() {
3086 let result = parse("echo ${VAR");
3087 assert!(result.is_err());
3088 }
3089
3090 #[test]
3091 fn error_missing_fi() {
3092 let result = parse("if true; then echo");
3093 assert!(result.is_err());
3094 }
3095
3096 #[test]
3097 fn error_missing_done() {
3098 let result = parse("for X in items; do echo");
3099 assert!(result.is_err());
3100 }
3101
3102 #[test]
3103 fn parse_lvalue_single_index() {
3104 let result = parse("xs[0]=9").unwrap();
3105 match &result.statements[0] {
3106 Stmt::Assignment(a) => {
3107 assert_eq!(a.name(), "xs");
3108 assert_eq!(
3109 a.path.segments,
3110 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
3111 );
3112 assert!(!a.local);
3113 }
3114 other => panic!("expected assignment, got {:?}", other),
3115 }
3116 }
3117
3118 #[test]
3119 fn parse_lvalue_negative_index() {
3120 let result = parse("xs[-1]=7").unwrap();
3121 match &result.statements[0] {
3122 Stmt::Assignment(a) => assert_eq!(
3123 a.path.segments,
3124 vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)]
3125 ),
3126 other => panic!("expected assignment, got {:?}", other),
3127 }
3128 }
3129
3130 #[test]
3131 fn parse_lvalue_bareword_key() {
3132 let result = parse("user[email]=x").unwrap();
3133 match &result.statements[0] {
3134 Stmt::Assignment(a) => assert_eq!(
3135 a.path.segments,
3136 vec![
3137 VarSegment::Field("user".into()),
3138 VarSegment::Key("email".into())
3139 ]
3140 ),
3141 other => panic!("expected assignment, got {:?}", other),
3142 }
3143 }
3144
3145 #[test]
3146 fn parse_lvalue_chained_keys() {
3147 let result = parse("s[web][port]=9000").unwrap();
3148 match &result.statements[0] {
3149 Stmt::Assignment(a) => assert_eq!(
3150 a.path.segments,
3151 vec![
3152 VarSegment::Field("s".into()),
3153 VarSegment::Key("web".into()),
3154 VarSegment::Key("port".into())
3155 ]
3156 ),
3157 other => panic!("expected assignment, got {:?}", other),
3158 }
3159 }
3160
3161 #[test]
3162 fn parse_lvalue_dynamic_key() {
3163 let result = parse("r[$k]=v").unwrap();
3164 match &result.statements[0] {
3165 Stmt::Assignment(a) => assert_eq!(
3166 a.path.segments,
3167 vec![
3168 VarSegment::Field("r".into()),
3169 VarSegment::Dynamic("k".into())
3170 ]
3171 ),
3172 other => panic!("expected assignment, got {:?}", other),
3173 }
3174 }
3175
3176 #[test]
3177 fn parse_local_lvalue_spaced() {
3178 let result = parse("local xs[0] = 9").unwrap();
3179 match &result.statements[0] {
3180 Stmt::Assignment(a) => {
3181 assert!(a.local);
3182 assert_eq!(
3183 a.path.segments,
3184 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
3185 );
3186 }
3187 other => panic!("expected assignment, got {:?}", other),
3188 }
3189 }
3190
3191 #[test]
3192 fn env_prefix_subscripted_target_is_not_captured_as_env_scoped() {
3193 let result = parse("user={}\nuser[email]=x echo hi").unwrap();
3203 for stmt in &result.statements {
3204 assert!(
3205 !matches!(stmt, Stmt::EnvScoped { .. }),
3206 "a subscripted assignment must never be captured into EnvScoped: {stmt:?}"
3207 );
3208 }
3209 assert!(matches!(&result.statements[1], Stmt::Assignment(a) if a.name() == "user"));
3211 assert!(matches!(&result.statements[2], Stmt::Command(c) if c.name == "echo"));
3212 }
3213
3214 #[test]
3215 fn parse_nested_cmd_subst() {
3216 let result = parse("X=$(echo $(date))").unwrap();
3218 match &result.statements[0] {
3219 Stmt::Assignment(a) => {
3220 assert_eq!(a.name(), "X");
3221 let outer = subst_cmd(&a.value);
3222 assert_eq!(outer.name, "echo");
3223 match &outer.args[0] {
3225 Arg::Positional(inner_expr) => {
3226 assert_eq!(subst_cmd(inner_expr).name, "date");
3227 }
3228 other => panic!("expected nested cmd subst arg, got {:?}", other),
3229 }
3230 }
3231 other => panic!("expected assignment, got {:?}", other),
3232 }
3233 }
3234
3235 #[test]
3236 fn parse_deeply_nested_cmd_subst() {
3237 let result = parse("X=$(a $(b $(c)))").unwrap();
3239 match &result.statements[0] {
3240 Stmt::Assignment(a) => {
3241 let level1 = subst_cmd(&a.value);
3242 assert_eq!(level1.name, "a");
3243 match &level1.args[0] {
3244 Arg::Positional(level2_expr) => {
3245 let level2 = subst_cmd(level2_expr);
3246 assert_eq!(level2.name, "b");
3247 match &level2.args[0] {
3248 Arg::Positional(level3_expr) => {
3249 assert_eq!(subst_cmd(level3_expr).name, "c");
3250 }
3251 other => panic!("expected level3 cmd subst, got {:?}", other),
3252 }
3253 }
3254 other => panic!("expected level2 cmd subst, got {:?}", other),
3255 }
3256 }
3257 other => panic!("expected assignment, got {:?}", other),
3258 }
3259 }
3260
3261 #[test]
3266 fn value_int_preserved() {
3267 let result = parse("X=42").unwrap();
3268 match &result.statements[0] {
3269 Stmt::Assignment(a) => {
3270 assert_eq!(a.name(), "X");
3271 match &a.value {
3272 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
3273 other => panic!("expected int literal, got {:?}", other),
3274 }
3275 }
3276 other => panic!("expected assignment, got {:?}", other),
3277 }
3278 }
3279
3280 #[test]
3281 fn value_negative_int_preserved() {
3282 let result = parse("X=-99").unwrap();
3283 match &result.statements[0] {
3284 Stmt::Assignment(a) => match &a.value {
3285 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
3286 other => panic!("expected int, got {:?}", other),
3287 },
3288 other => panic!("expected assignment, got {:?}", other),
3289 }
3290 }
3291
3292 #[test]
3293 fn value_float_preserved() {
3294 let result = parse("PI=3.14").unwrap();
3295 match &result.statements[0] {
3296 Stmt::Assignment(a) => match &a.value {
3297 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
3298 other => panic!("expected float, got {:?}", other),
3299 },
3300 other => panic!("expected assignment, got {:?}", other),
3301 }
3302 }
3303
3304 #[test]
3305 fn value_string_preserved() {
3306 let result = parse(r#"echo "hello world""#).unwrap();
3307 match &result.statements[0] {
3308 Stmt::Command(cmd) => {
3309 assert_eq!(cmd.name, "echo");
3310 match &cmd.args[0] {
3311 Arg::Positional(Expr::Literal(Value::String(s))) => {
3312 assert_eq!(s, "hello world");
3313 }
3314 other => panic!("expected string arg, got {:?}", other),
3315 }
3316 }
3317 other => panic!("expected command, got {:?}", other),
3318 }
3319 }
3320
3321 #[test]
3322 fn value_string_with_escapes_preserved() {
3323 let result = parse(r#"echo "line1\nline2""#).unwrap();
3324 match &result.statements[0] {
3325 Stmt::Command(cmd) => match &cmd.args[0] {
3326 Arg::Positional(Expr::Literal(Value::String(s))) => {
3327 assert_eq!(s, "line1\nline2");
3328 }
3329 other => panic!("expected string, got {:?}", other),
3330 },
3331 other => panic!("expected command, got {:?}", other),
3332 }
3333 }
3334
3335 #[test]
3336 fn value_command_name_preserved() {
3337 let result = parse("my-command").unwrap();
3338 match &result.statements[0] {
3339 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
3340 other => panic!("expected command, got {:?}", other),
3341 }
3342 }
3343
3344 #[test]
3345 fn value_assignment_name_preserved() {
3346 let result = parse("MY_VAR=1").unwrap();
3347 match &result.statements[0] {
3348 Stmt::Assignment(a) => assert_eq!(a.name(), "MY_VAR"),
3349 other => panic!("expected assignment, got {:?}", other),
3350 }
3351 }
3352
3353 #[test]
3354 fn value_for_variable_preserved() {
3355 let result = parse("for ITEM in items; do echo; done").unwrap();
3356 match &result.statements[0] {
3357 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
3358 other => panic!("expected for, got {:?}", other),
3359 }
3360 }
3361
3362 #[test]
3363 fn value_varref_name_preserved() {
3364 let result = parse("echo ${MESSAGE}").unwrap();
3365 match &result.statements[0] {
3366 Stmt::Command(cmd) => match &cmd.args[0] {
3367 Arg::Positional(Expr::VarRef(path)) => {
3368 assert_eq!(path.segments.len(), 1);
3369 let VarSegment::Field(name) = &path.segments[0] else {
3370 panic!("expected root field, got {:?}", path.segments[0]);
3371 };
3372 assert_eq!(name, "MESSAGE");
3373 }
3374 other => panic!("expected varref, got {:?}", other),
3375 },
3376 other => panic!("expected command, got {:?}", other),
3377 }
3378 }
3379
3380 #[test]
3381 fn value_varref_field_access_preserved() {
3382 let result = parse("echo ${RESULT.data}").unwrap();
3383 match &result.statements[0] {
3384 Stmt::Command(cmd) => match &cmd.args[0] {
3385 Arg::Positional(Expr::VarRef(path)) => {
3386 assert_eq!(path.segments.len(), 2);
3390 let VarSegment::Field(a) = &path.segments[0] else {
3391 panic!("expected field, got {:?}", path.segments[0]);
3392 };
3393 let VarSegment::Field(b) = &path.segments[1] else {
3394 panic!("expected field, got {:?}", path.segments[1]);
3395 };
3396 assert_eq!(a, "RESULT");
3397 assert_eq!(b, "data");
3398 }
3399 other => panic!("expected varref, got {:?}", other),
3400 },
3401 other => panic!("expected command, got {:?}", other),
3402 }
3403 }
3404
3405 #[test]
3406 fn value_varref_index_parsed() {
3407 let result = parse("echo ${ITEMS[0]}").unwrap();
3410 match &result.statements[0] {
3411 Stmt::Command(cmd) => match &cmd.args[0] {
3412 Arg::Positional(Expr::VarRef(path)) => {
3413 assert_eq!(path.segments.len(), 2);
3414 let VarSegment::Field(name) = &path.segments[0] else {
3415 panic!("expected root field, got {:?}", path.segments[0]);
3416 };
3417 assert_eq!(name, "ITEMS");
3418 assert_eq!(path.segments[1], VarSegment::Index(0));
3419 }
3420 other => panic!("expected varref, got {:?}", other),
3421 },
3422 other => panic!("expected command, got {:?}", other),
3423 }
3424 }
3425
3426 #[test]
3427 fn value_named_arg_preserved() {
3428 let result = parse("cmd count=42").unwrap();
3432 match &result.statements[0] {
3433 Stmt::Command(cmd) => {
3434 assert_eq!(cmd.name, "cmd");
3435 match &cmd.args[0] {
3436 Arg::WordAssign { key, value } => {
3437 assert_eq!(key, "count");
3438 match value {
3439 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
3440 other => panic!("expected int, got {:?}", other),
3441 }
3442 }
3443 other => panic!("expected WordAssign arg, got {:?}", other),
3444 }
3445 }
3446 other => panic!("expected command, got {:?}", other),
3447 }
3448 }
3449
3450 #[test]
3451 fn value_function_def_name_preserved() {
3452 let result = parse("greet() { echo }").unwrap();
3453 match &result.statements[0] {
3454 Stmt::ToolDef(t) => {
3455 assert_eq!(t.name, "greet");
3456 assert!(t.params.is_empty());
3457 }
3458 other => panic!("expected function def, got {:?}", other),
3459 }
3460 }
3461
3462 #[test]
3467 fn parse_comparison_equals() {
3468 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
3470 match &result.statements[0] {
3471 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3472 Expr::Test(test) => match test.as_ref() {
3473 TestExpr::Comparison { left, op, right } => {
3474 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3475 assert_eq!(*op, TestCmpOp::Eq);
3476 match right.as_ref() {
3477 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
3478 other => panic!("expected int, got {:?}", other),
3479 }
3480 }
3481 other => panic!("expected comparison, got {:?}", other),
3482 },
3483 other => panic!("expected test expr, got {:?}", other),
3484 },
3485 other => panic!("expected if, got {:?}", other),
3486 }
3487 }
3488
3489 #[test]
3490 fn parse_comparison_not_equals() {
3491 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
3492 match &result.statements[0] {
3493 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3494 Expr::Test(test) => match test.as_ref() {
3495 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
3496 other => panic!("expected comparison, got {:?}", other),
3497 },
3498 other => panic!("expected test expr, got {:?}", other),
3499 },
3500 other => panic!("expected if, got {:?}", other),
3501 }
3502 }
3503
3504 #[test]
3505 fn parse_comparison_less_than() {
3506 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
3507 match &result.statements[0] {
3508 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3509 Expr::Test(test) => match test.as_ref() {
3510 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
3511 other => panic!("expected comparison, got {:?}", other),
3512 },
3513 other => panic!("expected test expr, got {:?}", other),
3514 },
3515 other => panic!("expected if, got {:?}", other),
3516 }
3517 }
3518
3519 #[test]
3520 fn parse_comparison_greater_than() {
3521 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
3522 match &result.statements[0] {
3523 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3524 Expr::Test(test) => match test.as_ref() {
3525 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
3526 other => panic!("expected comparison, got {:?}", other),
3527 },
3528 other => panic!("expected test expr, got {:?}", other),
3529 },
3530 other => panic!("expected if, got {:?}", other),
3531 }
3532 }
3533
3534 #[test]
3535 fn parse_comparison_less_equal() {
3536 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
3537 match &result.statements[0] {
3538 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3539 Expr::Test(test) => match test.as_ref() {
3540 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
3541 other => panic!("expected comparison, got {:?}", other),
3542 },
3543 other => panic!("expected test expr, got {:?}", other),
3544 },
3545 other => panic!("expected if, got {:?}", other),
3546 }
3547 }
3548
3549 #[test]
3550 fn parse_comparison_greater_equal() {
3551 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
3552 match &result.statements[0] {
3553 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3554 Expr::Test(test) => match test.as_ref() {
3555 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
3556 other => panic!("expected comparison, got {:?}", other),
3557 },
3558 other => panic!("expected test expr, got {:?}", other),
3559 },
3560 other => panic!("expected if, got {:?}", other),
3561 }
3562 }
3563
3564 #[test]
3565 fn parse_regex_match() {
3566 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
3567 match &result.statements[0] {
3568 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3569 Expr::Test(test) => match test.as_ref() {
3570 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
3571 other => panic!("expected comparison, got {:?}", other),
3572 },
3573 other => panic!("expected test expr, got {:?}", other),
3574 },
3575 other => panic!("expected if, got {:?}", other),
3576 }
3577 }
3578
3579 #[test]
3580 fn parse_regex_not_match() {
3581 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
3582 match &result.statements[0] {
3583 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3584 Expr::Test(test) => match test.as_ref() {
3585 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
3586 other => panic!("expected comparison, got {:?}", other),
3587 },
3588 other => panic!("expected test expr, got {:?}", other),
3589 },
3590 other => panic!("expected if, got {:?}", other),
3591 }
3592 }
3593
3594 #[test]
3595 fn parse_string_interpolation() {
3596 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
3597 match &result.statements[0] {
3598 Stmt::Command(cmd) => match &cmd.args[0] {
3599 Arg::Positional(Expr::Interpolated(parts)) => {
3600 assert_eq!(parts.len(), 3);
3601 match &parts[0] {
3602 StringPart::Literal(s) => assert_eq!(s, "Hello "),
3603 other => panic!("expected literal, got {:?}", other),
3604 }
3605 match &parts[1] {
3606 StringPart::Var(path) => {
3607 assert_eq!(path.segments.len(), 1);
3608 let VarSegment::Field(name) = &path.segments[0] else {
3609 panic!("expected root field, got {:?}", path.segments[0]);
3610 };
3611 assert_eq!(name, "NAME");
3612 }
3613 other => panic!("expected var, got {:?}", other),
3614 }
3615 match &parts[2] {
3616 StringPart::Literal(s) => assert_eq!(s, "!"),
3617 other => panic!("expected literal, got {:?}", other),
3618 }
3619 }
3620 other => panic!("expected interpolated, got {:?}", other),
3621 },
3622 other => panic!("expected command, got {:?}", other),
3623 }
3624 }
3625
3626 #[test]
3627 fn parse_string_interpolation_multiple_vars() {
3628 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
3629 match &result.statements[0] {
3630 Stmt::Command(cmd) => match &cmd.args[0] {
3631 Arg::Positional(Expr::Interpolated(parts)) => {
3632 assert_eq!(parts.len(), 3);
3634 assert!(matches!(&parts[0], StringPart::Var(_)));
3635 assert!(matches!(&parts[1], StringPart::Literal(_)));
3636 assert!(matches!(&parts[2], StringPart::Var(_)));
3637 }
3638 other => panic!("expected interpolated, got {:?}", other),
3639 },
3640 other => panic!("expected command, got {:?}", other),
3641 }
3642 }
3643
3644 #[test]
3645 fn parse_empty_function_body() {
3646 let result = parse("empty() { }").unwrap();
3647 match &result.statements[0] {
3648 Stmt::ToolDef(t) => {
3649 assert_eq!(t.name, "empty");
3650 assert!(t.params.is_empty());
3651 assert!(t.body.is_empty());
3652 }
3653 other => panic!("expected function def, got {:?}", other),
3654 }
3655 }
3656
3657 #[test]
3658 fn parse_bash_style_function() {
3659 let result = parse("function greet { echo hello }").unwrap();
3660 match &result.statements[0] {
3661 Stmt::ToolDef(t) => {
3662 assert_eq!(t.name, "greet");
3663 assert!(t.params.is_empty());
3664 assert_eq!(t.body.len(), 1);
3665 }
3666 other => panic!("expected function def, got {:?}", other),
3667 }
3668 }
3669
3670 #[test]
3671 fn parse_comparison_string_values() {
3672 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
3673 match &result.statements[0] {
3674 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3675 Expr::Test(test) => match test.as_ref() {
3676 TestExpr::Comparison { left, op, right } => {
3677 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3678 assert_eq!(*op, TestCmpOp::Eq);
3679 match right.as_ref() {
3680 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
3681 other => panic!("expected string, got {:?}", other),
3682 }
3683 }
3684 other => panic!("expected comparison, got {:?}", other),
3685 },
3686 other => panic!("expected test expr, got {:?}", other),
3687 },
3688 other => panic!("expected if, got {:?}", other),
3689 }
3690 }
3691
3692 #[test]
3697 fn parse_cmd_subst_simple() {
3698 let result = parse("X=$(echo)").unwrap();
3699 match &result.statements[0] {
3700 Stmt::Assignment(a) => {
3701 assert_eq!(a.name(), "X");
3702 assert_eq!(subst_cmd(&a.value).name, "echo");
3703 }
3704 other => panic!("expected assignment, got {:?}", other),
3705 }
3706 }
3707
3708 #[test]
3709 fn parse_cmd_subst_with_args() {
3710 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
3711 match &result.statements[0] {
3712 Stmt::Assignment(a) => {
3713 let cmd = subst_cmd(&a.value);
3714 assert_eq!(cmd.name, "fetch");
3715 assert_eq!(cmd.args.len(), 1);
3716 match &cmd.args[0] {
3717 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
3718 other => panic!("expected WordAssign arg, got {:?}", other),
3719 }
3720 }
3721 other => panic!("expected assignment, got {:?}", other),
3722 }
3723 }
3724
3725 #[test]
3726 fn parse_cmd_subst_pipeline() {
3727 let result = parse("X=$(cat file | grep pattern)").unwrap();
3728 match &result.statements[0] {
3729 Stmt::Assignment(a) => {
3730 let pipeline = subst_pipeline(&a.value);
3731 assert_eq!(pipeline.commands.len(), 2);
3732 assert_eq!(pipeline.commands[0].name, "cat");
3733 assert_eq!(pipeline.commands[1].name, "grep");
3734 }
3735 other => panic!("expected assignment, got {:?}", other),
3736 }
3737 }
3738
3739 #[test]
3740 fn parse_cmd_subst_with_redirect() {
3741 let result = parse("X=$(echo hi > out.txt)").unwrap();
3746 match &result.statements[0] {
3747 Stmt::Assignment(a) => {
3748 let pipeline = subst_pipeline(&a.value);
3749 assert_eq!(pipeline.commands.len(), 1);
3750 let cmd = &pipeline.commands[0];
3751 assert_eq!(cmd.name, "echo");
3752 assert_eq!(cmd.redirects.len(), 1);
3753 assert!(matches!(
3754 cmd.redirects[0].kind,
3755 RedirectKind::StdoutOverwrite
3756 ));
3757 }
3758 other => panic!("expected assignment, got {:?}", other),
3759 }
3760 }
3761
3762 #[test]
3763 fn parse_cmd_subst_redirect_target_with_nested_subst() {
3764 let result = parse("X=$(echo hi > $(echo f))").unwrap();
3770 match &result.statements[0] {
3771 Stmt::Assignment(a) => {
3772 let pipeline = subst_pipeline(&a.value);
3773 assert_eq!(pipeline.commands.len(), 1);
3774 let cmd = &pipeline.commands[0];
3775 assert_eq!(cmd.name, "echo");
3776 assert_eq!(cmd.redirects.len(), 1);
3777 assert!(
3778 matches!(cmd.redirects[0].target, Expr::CommandSubst(_)),
3779 "redirect target should be a nested command substitution, got {:?}",
3780 cmd.redirects[0].target
3781 );
3782 }
3783 other => panic!("expected assignment, got {:?}", other),
3784 }
3785 }
3786
3787 #[test]
3788 fn parse_cmd_subst_chain_with_redirect() {
3789 let result = parse("X=$(echo a && echo b > out.txt)").unwrap();
3793 let stmts = match &result.statements[0] {
3794 Stmt::Assignment(a) => match &a.value {
3795 Expr::CommandSubst(s) => s,
3796 other => panic!("expected command subst, got {:?}", other),
3797 },
3798 other => panic!("expected assignment, got {:?}", other),
3799 };
3800 match stmts.as_slice() {
3801 [Stmt::AndChain { left, right }] => {
3802 assert!(
3804 matches!(**left, Stmt::Command(_)),
3805 "left of && should be a bare command, got {:?}",
3806 left
3807 );
3808 match &**right {
3810 Stmt::Pipeline(p) => {
3811 assert_eq!(p.commands.len(), 1);
3812 assert_eq!(p.commands[0].name, "echo");
3813 assert_eq!(p.commands[0].redirects.len(), 1);
3814 }
3815 other => panic!("right should be a redirect-bearing pipeline, got {:?}", other),
3816 }
3817 }
3818 other => panic!("expected a single AndChain, got {:?}", other),
3819 }
3820 }
3821
3822 #[test]
3823 fn parse_cmd_subst_in_condition() {
3824 let result = parse("if kaish-validate; then echo; fi").unwrap();
3826 match &result.statements[0] {
3827 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3828 Expr::Command(cmd) => {
3829 assert_eq!(cmd.name, "kaish-validate");
3830 }
3831 other => panic!("expected command, got {:?}", other),
3832 },
3833 other => panic!("expected if, got {:?}", other),
3834 }
3835 }
3836
3837 #[test]
3842 fn parse_env_prefix_single() {
3843 let result = parse("FOO=bar echo hi").unwrap();
3844 match &result.statements[0] {
3845 Stmt::EnvScoped { assignments, body } => {
3846 assert_eq!(assignments.len(), 1);
3847 assert_eq!(assignments[0].name(), "FOO");
3848 assert!(!assignments[0].local);
3849 match body.as_ref() {
3850 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
3851 other => panic!("expected command body, got {other:?}"),
3852 }
3853 }
3854 other => panic!("expected env-scoped, got {other:?}"),
3855 }
3856 }
3857
3858 #[test]
3859 fn parse_env_prefix_multiple() {
3860 let result = parse("A=1 B=2 run").unwrap();
3861 match &result.statements[0] {
3862 Stmt::EnvScoped { assignments, body } => {
3863 assert_eq!(assignments.len(), 2);
3864 assert_eq!(assignments[0].name(), "A");
3865 assert_eq!(assignments[1].name(), "B");
3866 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
3867 }
3868 other => panic!("expected env-scoped, got {other:?}"),
3869 }
3870 }
3871
3872 #[test]
3873 fn parse_bare_assignment_is_not_env_scoped() {
3874 let result = parse("FOO=bar").unwrap();
3876 assert!(
3877 matches!(&result.statements[0], Stmt::Assignment(a) if a.name() == "FOO"),
3878 "got {:?}",
3879 result.statements[0]
3880 );
3881 }
3882
3883 #[test]
3884 fn parse_assignment_then_and_chain_does_not_over_capture() {
3885 let result = parse("FOO=bar && echo hi").unwrap();
3888 match &result.statements[0] {
3889 Stmt::AndChain { left, right } => {
3890 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name() == "FOO"));
3891 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
3892 }
3893 other => panic!("expected and-chain, got {other:?}"),
3894 }
3895 }
3896
3897 #[test]
3898 fn parse_env_prefix_pipeline_body() {
3899 let result = parse("FOO=bar cat | grep x").unwrap();
3900 match &result.statements[0] {
3901 Stmt::EnvScoped { assignments, body } => {
3902 assert_eq!(assignments[0].name(), "FOO");
3903 match body.as_ref() {
3904 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 2),
3905 other => panic!("expected pipeline body, got {other:?}"),
3906 }
3907 }
3908 other => panic!("expected env-scoped, got {other:?}"),
3909 }
3910 }
3911
3912 fn parse_err_message(source: &str) -> String {
3917 parse(source)
3918 .expect_err("expected a parse error")
3919 .iter()
3920 .map(|e| e.message.clone())
3921 .collect::<Vec<_>>()
3922 .join(" ")
3923 }
3924
3925 #[test]
3926 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
3927 let msg = parse_err_message("echo /tmp/$(echo x).txt");
3930 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
3931 }
3932
3933 #[test]
3934 fn argv_splat_var_glued_to_path_is_rejected() {
3935 assert!(parse("echo $dir/out.txt").is_err());
3936 }
3937
3938 #[test]
3939 fn argv_splat_three_way_glue_is_rejected() {
3940 assert!(parse("echo foo$(echo bar)baz").is_err());
3941 }
3942
3943 #[test]
3944 fn argv_splat_quoted_word_is_accepted() {
3945 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
3947 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
3948 }
3949
3950 #[test]
3951 fn argv_single_token_words_are_not_splat() {
3952 assert!(parse("echo file.txt").is_ok(), "file.txt");
3954 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
3955 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
3956 }
3957
3958 #[test]
3959 fn argv_spaced_words_are_not_splat() {
3960 assert!(parse("echo a b c").is_ok());
3961 assert!(parse("echo /tmp/x $(echo y)").is_ok());
3962 }
3963
3964 #[test]
3965 fn parse_cmd_subst_in_command_arg() {
3966 let result = parse("echo $(whoami)").unwrap();
3967 match &result.statements[0] {
3968 Stmt::Command(cmd) => {
3969 assert_eq!(cmd.name, "echo");
3970 match &cmd.args[0] {
3971 Arg::Positional(expr) => {
3972 assert_eq!(subst_cmd(expr).name, "whoami");
3973 }
3974 other => panic!("expected command subst, got {:?}", other),
3975 }
3976 }
3977 other => panic!("expected command, got {:?}", other),
3978 }
3979 }
3980
3981 #[test]
3986 fn parse_condition_and() {
3987 let result = parse("if check-a && check-b; then echo; fi").unwrap();
3989 match &result.statements[0] {
3990 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3991 Expr::BinaryOp { left, op, right } => {
3992 assert_eq!(*op, BinaryOp::And);
3993 assert!(matches!(left.as_ref(), Expr::Command(_)));
3994 assert!(matches!(right.as_ref(), Expr::Command(_)));
3995 }
3996 other => panic!("expected binary op, got {:?}", other),
3997 },
3998 other => panic!("expected if, got {:?}", other),
3999 }
4000 }
4001
4002 #[test]
4003 fn parse_condition_or() {
4004 let result = parse("if try-a || try-b; then echo; fi").unwrap();
4005 match &result.statements[0] {
4006 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4007 Expr::BinaryOp { left, op, right } => {
4008 assert_eq!(*op, BinaryOp::Or);
4009 assert!(matches!(left.as_ref(), Expr::Command(_)));
4010 assert!(matches!(right.as_ref(), Expr::Command(_)));
4011 }
4012 other => panic!("expected binary op, got {:?}", other),
4013 },
4014 other => panic!("expected if, got {:?}", other),
4015 }
4016 }
4017
4018 #[test]
4019 fn parse_condition_and_or_precedence() {
4020 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
4022 match &result.statements[0] {
4023 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4024 Expr::BinaryOp { left, op, right } => {
4025 assert_eq!(*op, BinaryOp::Or);
4027 match left.as_ref() {
4029 Expr::BinaryOp { op: inner_op, .. } => {
4030 assert_eq!(*inner_op, BinaryOp::And);
4031 }
4032 other => panic!("expected binary op (&&), got {:?}", other),
4033 }
4034 assert!(matches!(right.as_ref(), Expr::Command(_)));
4036 }
4037 other => panic!("expected binary op, got {:?}", other),
4038 },
4039 other => panic!("expected if, got {:?}", other),
4040 }
4041 }
4042
4043 #[test]
4044 fn parse_condition_multiple_and() {
4045 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
4046 match &result.statements[0] {
4047 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4048 Expr::BinaryOp { left, op, .. } => {
4049 assert_eq!(*op, BinaryOp::And);
4050 match left.as_ref() {
4052 Expr::BinaryOp { op: inner_op, .. } => {
4053 assert_eq!(*inner_op, BinaryOp::And);
4054 }
4055 other => panic!("expected binary op, got {:?}", other),
4056 }
4057 }
4058 other => panic!("expected binary op, got {:?}", other),
4059 },
4060 other => panic!("expected if, got {:?}", other),
4061 }
4062 }
4063
4064 #[test]
4065 fn parse_condition_mixed_comparison_and_logical() {
4066 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
4068 match &result.statements[0] {
4069 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4070 Expr::BinaryOp { left, op, right } => {
4071 assert_eq!(*op, BinaryOp::And);
4072 match left.as_ref() {
4074 Expr::Test(test) => match test.as_ref() {
4075 TestExpr::Comparison { op: left_op, .. } => {
4076 assert_eq!(*left_op, TestCmpOp::Eq);
4077 }
4078 other => panic!("expected comparison, got {:?}", other),
4079 },
4080 other => panic!("expected test, got {:?}", other),
4081 }
4082 match right.as_ref() {
4084 Expr::Test(test) => match test.as_ref() {
4085 TestExpr::Comparison { op: right_op, .. } => {
4086 assert_eq!(*right_op, TestCmpOp::NumGt);
4087 }
4088 other => panic!("expected comparison, got {:?}", other),
4089 },
4090 other => panic!("expected test, got {:?}", other),
4091 }
4092 }
4093 other => panic!("expected binary op, got {:?}", other),
4094 },
4095 other => panic!("expected if, got {:?}", other),
4096 }
4097 }
4098
4099 #[test]
4105 fn script_level1_linear() {
4106 let script = r#"
4107NAME="kaish"
4108VERSION=1
4109TIMEOUT=30
4110ITEMS="alpha beta gamma"
4111
4112echo "Starting ${NAME} v${VERSION}"
4113cat "README.md" | grep pattern="install" | head count=5
4114fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
4115echo "Items: ${ITEMS}"
4116"#;
4117 let result = parse(script).unwrap();
4118 let stmts: Vec<_> = result.statements.iter()
4119 .filter(|s| !matches!(s, Stmt::Empty))
4120 .collect();
4121
4122 assert_eq!(stmts.len(), 8);
4123 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(_))); }
4132
4133 #[test]
4135 fn script_level2_branching() {
4136 let script = r#"
4137RESULT=$(kaish-validate "input.json")
4138
4139if [[ ${RESULT.ok} == true ]]; then
4140 echo "Validation passed"
4141 process "input.json" > "output.json"
4142else
4143 echo "Validation failed: ${RESULT.err}"
4144fi
4145
4146if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
4147 echo "Count in valid range"
4148fi
4149
4150if check-network || check-cache; then
4151 fetch url=${URL}
4152fi
4153"#;
4154 let result = parse(script).unwrap();
4155 let stmts: Vec<_> = result.statements.iter()
4156 .filter(|s| !matches!(s, Stmt::Empty))
4157 .collect();
4158
4159 assert_eq!(stmts.len(), 4);
4160
4161 match stmts[0] {
4163 Stmt::Assignment(a) => {
4164 assert_eq!(a.name(), "RESULT");
4165 assert!(matches!(&a.value, Expr::CommandSubst(_)));
4166 }
4167 other => panic!("expected assignment, got {:?}", other),
4168 }
4169
4170 match stmts[1] {
4172 Stmt::If(if_stmt) => {
4173 assert_eq!(if_stmt.then_branch.len(), 2);
4174 assert!(if_stmt.else_branch.is_some());
4175 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
4176 }
4177 other => panic!("expected if, got {:?}", other),
4178 }
4179
4180 match stmts[2] {
4182 Stmt::If(if_stmt) => {
4183 match if_stmt.condition.as_ref() {
4184 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
4185 other => panic!("expected && condition, got {:?}", other),
4186 }
4187 }
4188 other => panic!("expected if, got {:?}", other),
4189 }
4190
4191 match stmts[3] {
4193 Stmt::If(if_stmt) => {
4194 match if_stmt.condition.as_ref() {
4195 Expr::BinaryOp { op, left, right } => {
4196 assert_eq!(*op, BinaryOp::Or);
4197 assert!(matches!(left.as_ref(), Expr::Command(_)));
4198 assert!(matches!(right.as_ref(), Expr::Command(_)));
4199 }
4200 other => panic!("expected || condition, got {:?}", other),
4201 }
4202 }
4203 other => panic!("expected if, got {:?}", other),
4204 }
4205 }
4206
4207 #[test]
4209 fn script_level3_loops_and_functions() {
4210 let script = r#"
4211greet() {
4212 echo "Hello, $1!"
4213}
4214
4215fetch_all() {
4216 for URL in $@; do
4217 fetch url=${URL}
4218 done
4219}
4220
4221USERS="alice bob charlie"
4222
4223for USER in ${USERS}; do
4224 greet ${USER}
4225 if [[ ${USER} == "bob" ]]; then
4226 echo "Found Bob!"
4227 fi
4228done
4229
4230long-running-task &
4231"#;
4232 let result = parse(script).unwrap();
4233 let stmts: Vec<_> = result.statements.iter()
4234 .filter(|s| !matches!(s, Stmt::Empty))
4235 .collect();
4236
4237 assert_eq!(stmts.len(), 5);
4238
4239 match stmts[0] {
4241 Stmt::ToolDef(t) => {
4242 assert_eq!(t.name, "greet");
4243 assert!(t.params.is_empty());
4244 }
4245 other => panic!("expected function def, got {:?}", other),
4246 }
4247
4248 match stmts[1] {
4250 Stmt::ToolDef(t) => {
4251 assert_eq!(t.name, "fetch_all");
4252 assert_eq!(t.body.len(), 1);
4253 assert!(matches!(&t.body[0], Stmt::For(_)));
4254 }
4255 other => panic!("expected function def, got {:?}", other),
4256 }
4257
4258 assert!(matches!(stmts[2], Stmt::Assignment(_)));
4260
4261 match stmts[3] {
4263 Stmt::For(f) => {
4264 assert_eq!(f.variable, "USER");
4265 assert_eq!(f.body.len(), 2);
4266 assert!(matches!(&f.body[0], Stmt::Command(_)));
4267 assert!(matches!(&f.body[1], Stmt::If(_)));
4268 }
4269 other => panic!("expected for loop, got {:?}", other),
4270 }
4271
4272 match stmts[4] {
4274 Stmt::Pipeline(p) => {
4275 assert!(p.background);
4276 assert_eq!(p.commands[0].name, "long-running-task");
4277 }
4278 other => panic!("expected pipeline (background), got {:?}", other),
4279 }
4280 }
4281
4282 #[test]
4284 fn script_level4_complex_nesting() {
4285 let script = r#"
4286RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
4287
4288if ping host=${HOST} && [[ ${RESULT} == true ]]; then
4289 for SERVER in "prod-1 prod-2"; do
4290 deploy target=${SERVER} port=8080
4291 if [[ $? -ne 0 ]]; then
4292 notify channel="ops" message="Deploy failed"
4293 fi
4294 done
4295fi
4296"#;
4297 let result = parse(script).unwrap();
4298 let stmts: Vec<_> = result.statements.iter()
4299 .filter(|s| !matches!(s, Stmt::Empty))
4300 .collect();
4301
4302 assert_eq!(stmts.len(), 2);
4303
4304 match stmts[0] {
4306 Stmt::Assignment(a) => {
4307 assert_eq!(a.name(), "RESULT");
4308 assert_eq!(subst_pipeline(&a.value).commands.len(), 3);
4309 }
4310 other => panic!("expected assignment, got {:?}", other),
4311 }
4312
4313 match stmts[1] {
4315 Stmt::If(if_stmt) => {
4316 match if_stmt.condition.as_ref() {
4317 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
4318 other => panic!("expected && condition, got {:?}", other),
4319 }
4320 assert_eq!(if_stmt.then_branch.len(), 1);
4321 match &if_stmt.then_branch[0] {
4322 Stmt::For(f) => {
4323 assert_eq!(f.body.len(), 2);
4324 assert!(matches!(&f.body[1], Stmt::If(_)));
4325 }
4326 other => panic!("expected for in if body, got {:?}", other),
4327 }
4328 }
4329 other => panic!("expected if, got {:?}", other),
4330 }
4331 }
4332
4333 #[test]
4335 fn script_level5_edge_cases() {
4336 let script = r#"
4337echo ""
4338echo "quotes: \"nested\" here"
4339echo "escapes: \n\t\r\\"
4340echo "unicode: \u2764"
4341
4342X=-99999
4343Y=3.14159265358979
4344Z=-0.001
4345
4346cmd a=1 b="two" c=true d=false e=null
4347
4348if true; then
4349 if false; then
4350 echo "inner"
4351 else
4352 echo "else"
4353 fi
4354fi
4355
4356for I in "a b c"; do
4357 echo ${I}
4358done
4359
4360no_params() {
4361 echo "no params"
4362}
4363
4364function all_args {
4365 echo "args: $@"
4366}
4367
4368a | b | c | d | e &
4369cmd 2> "errors.log"
4370cmd &> "all.log"
4371cmd >> "append.log"
4372cmd < "input.txt"
4373"#;
4374 let result = parse(script).unwrap();
4375 let stmts: Vec<_> = result.statements.iter()
4376 .filter(|s| !matches!(s, Stmt::Empty))
4377 .collect();
4378
4379 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
4381
4382 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
4384 assert!(bg_stmt.is_some(), "expected background pipeline");
4385
4386 match bg_stmt.unwrap() {
4387 Stmt::Pipeline(p) => {
4388 assert_eq!(p.commands.len(), 5);
4389 assert!(p.background);
4390 }
4391 _ => unreachable!(),
4392 }
4393 }
4394
4395 #[test]
4400 fn parse_keyword_as_variable_rejected() {
4401 let result = parse(r#"if="value""#);
4404 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
4405
4406 let result = parse("while=true");
4407 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
4408
4409 let result = parse(r#"then="next""#);
4410 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
4411 }
4412
4413 #[test]
4414 fn parse_set_command_with_flag() {
4415 let result = parse("set -e");
4416 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
4417 let program = result.unwrap();
4418 match &program.statements[0] {
4419 Stmt::Command(cmd) => {
4420 assert_eq!(cmd.name, "set");
4421 assert_eq!(cmd.args.len(), 1);
4422 match &cmd.args[0] {
4423 Arg::ShortFlag(f) => assert_eq!(f, "e"),
4424 other => panic!("expected ShortFlag, got {:?}", other),
4425 }
4426 }
4427 other => panic!("expected Command, got {:?}", other),
4428 }
4429 }
4430
4431 #[test]
4432 fn parse_set_command_no_args() {
4433 let result = parse("set");
4434 assert!(result.is_ok(), "failed to parse set: {:?}", result);
4435 let program = result.unwrap();
4436 match &program.statements[0] {
4437 Stmt::Command(cmd) => {
4438 assert_eq!(cmd.name, "set");
4439 assert_eq!(cmd.args.len(), 0);
4440 }
4441 other => panic!("expected Command, got {:?}", other),
4442 }
4443 }
4444
4445 #[test]
4446 fn parse_set_assignment_vs_command() {
4447 let result = parse("X=5");
4449 assert!(result.is_ok());
4450 let program = result.unwrap();
4451 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
4452
4453 let result = parse("set -e");
4455 assert!(result.is_ok());
4456 let program = result.unwrap();
4457 assert!(matches!(&program.statements[0], Stmt::Command(_)));
4458 }
4459
4460 #[test]
4461 fn parse_true_as_command() {
4462 let result = parse("true");
4463 assert!(result.is_ok());
4464 let program = result.unwrap();
4465 match &program.statements[0] {
4466 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
4467 other => panic!("expected Command(true), got {:?}", other),
4468 }
4469 }
4470
4471 #[test]
4472 fn parse_false_as_command() {
4473 let result = parse("false");
4474 assert!(result.is_ok());
4475 let program = result.unwrap();
4476 match &program.statements[0] {
4477 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
4478 other => panic!("expected Command(false), got {:?}", other),
4479 }
4480 }
4481
4482 #[test]
4483 fn parse_dot_as_source_alias() {
4484 let result = parse(". script.kai");
4485 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
4486 let program = result.unwrap();
4487 match &program.statements[0] {
4488 Stmt::Command(cmd) => {
4489 assert_eq!(cmd.name, ".");
4490 assert_eq!(cmd.args.len(), 1);
4491 }
4492 other => panic!("expected Command(.), got {:?}", other),
4493 }
4494 }
4495
4496 #[test]
4497 fn parse_source_command() {
4498 let result = parse("source utils.kai");
4499 assert!(result.is_ok(), "failed to parse source: {:?}", result);
4500 let program = result.unwrap();
4501 match &program.statements[0] {
4502 Stmt::Command(cmd) => {
4503 assert_eq!(cmd.name, "source");
4504 assert_eq!(cmd.args.len(), 1);
4505 }
4506 other => panic!("expected Command(source), got {:?}", other),
4507 }
4508 }
4509
4510 #[test]
4511 fn parse_test_expr_file_test() {
4512 let result = parse(r#"[[ -f "/path/file" ]]"#);
4514 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
4515 }
4516
4517 #[test]
4518 fn parse_test_expr_comparison() {
4519 let result = parse(r#"[[ $X == "value" ]]"#);
4520 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
4521 }
4522
4523 #[test]
4524 fn parse_test_expr_single_eq() {
4525 let result = parse(r#"[[ $X = "value" ]]"#);
4527 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
4528 let program = result.unwrap();
4529 match &program.statements[0] {
4530 Stmt::Test(TestExpr::Comparison { op, .. }) => {
4531 assert_eq!(op, &TestCmpOp::Eq);
4532 }
4533 other => panic!("expected Test(Comparison), got {:?}", other),
4534 }
4535 }
4536
4537 #[test]
4538 fn parse_while_loop() {
4539 let result = parse("while true; do echo; done");
4540 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
4541 let program = result.unwrap();
4542 assert!(matches!(&program.statements[0], Stmt::While(_)));
4543 }
4544
4545 #[test]
4546 fn parse_break_with_level() {
4547 let result = parse("break 2");
4548 assert!(result.is_ok());
4549 let program = result.unwrap();
4550 match &program.statements[0] {
4551 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
4552 other => panic!("expected Break(2), got {:?}", other),
4553 }
4554 }
4555
4556 #[test]
4557 fn parse_continue_with_level() {
4558 let result = parse("continue 3");
4559 assert!(result.is_ok());
4560 let program = result.unwrap();
4561 match &program.statements[0] {
4562 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
4563 other => panic!("expected Continue(3), got {:?}", other),
4564 }
4565 }
4566
4567 #[test]
4568 fn parse_exit_with_code() {
4569 let result = parse("exit 1");
4570 assert!(result.is_ok());
4571 let program = result.unwrap();
4572 match &program.statements[0] {
4573 Stmt::Exit(Some(expr)) => {
4574 match expr.as_ref() {
4575 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
4576 other => panic!("expected Int(1), got {:?}", other),
4577 }
4578 }
4579 other => panic!("expected Exit(1), got {:?}", other),
4580 }
4581 }
4582
4583 #[test]
4590 fn spanned_literal_only_records_byte_range() {
4591 let parts = parse_interpolated_string_spanned("hello world", 100);
4592 assert_eq!(parts.len(), 1);
4593 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
4594 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
4595 assert_eq!(parts[0].len, 11);
4596 }
4597
4598 #[test]
4599 fn spanned_braced_var_at_zero() {
4600 let parts = parse_interpolated_string_spanned("${X}", 50);
4601 assert_eq!(parts.len(), 1);
4602 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4603 assert_eq!(parts[0].offset, 50);
4604 assert_eq!(parts[0].len, 4); }
4606
4607 #[test]
4608 fn spanned_simple_var_then_literal() {
4609 let parts = parse_interpolated_string_spanned("$X end", 10);
4610 assert_eq!(parts.len(), 2);
4611 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4612 assert_eq!(parts[0].offset, 10);
4613 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
4615 assert_eq!(parts[1].offset, 12);
4616 assert_eq!(parts[1].len, 4);
4617 }
4618
4619 #[test]
4620 fn spanned_mixed_literal_var_literal() {
4621 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0);
4622 assert_eq!(parts.len(), 3);
4623 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
4625 assert_eq!(parts[0].offset, 0);
4626 assert_eq!(parts[0].len, 3);
4627 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4629 assert_eq!(parts[1].offset, 3);
4630 assert_eq!(parts[1].len, 4);
4631 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
4633 assert_eq!(parts[2].offset, 7);
4634 assert_eq!(parts[2].len, 4);
4635 }
4636
4637 #[test]
4638 fn spanned_positional_param() {
4639 let parts = parse_interpolated_string_spanned("$1 done", 0);
4640 assert_eq!(parts.len(), 2);
4641 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
4642 assert_eq!(parts[0].offset, 0);
4643 assert_eq!(parts[0].len, 2); }
4645
4646 #[test]
4647 fn spanned_special_dollar_dollar() {
4648 let parts = parse_interpolated_string_spanned("$$", 5);
4649 assert_eq!(parts.len(), 1);
4650 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
4651 assert_eq!(parts[0].offset, 5);
4652 assert_eq!(parts[0].len, 2);
4653 }
4654
4655 #[test]
4656 fn spanned_arithmetic_marker_recognised() {
4657 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0);
4661 assert_eq!(parts.len(), 1);
4662 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
4663 }
4664
4665 #[test]
4666 fn spanned_default_separator_yields_var_with_default() {
4667 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0);
4668 assert_eq!(parts.len(), 1);
4669 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
4670 assert_eq!(parts[0].offset, 0);
4671 assert_eq!(parts[0].len, 14); }
4673
4674 #[test]
4675 fn spanned_no_dollar_runs_one_literal() {
4676 let parts = parse_interpolated_string_spanned("plain text only", 7);
4677 assert_eq!(parts.len(), 1);
4678 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
4679 assert_eq!(parts[0].offset, 7);
4680 assert_eq!(parts[0].len, 15);
4681 }
4682
4683 #[test]
4684 fn spanned_matches_unspanned_part_count() {
4685 let cases = [
4688 "hello",
4689 "$X",
4690 "${X}",
4691 "${X:-d}",
4692 "hi $A and $B",
4693 "$0 $1 $2",
4694 "$$ $? $#",
4695 ];
4696 for s in &cases {
4697 let unspanned = parse_interpolated_string(s).expect("test input parses");
4698 let spanned = parse_interpolated_string_spanned(s, 0);
4699 assert_eq!(
4700 unspanned.len(),
4701 spanned.len(),
4702 "part count differs for {:?}",
4703 s
4704 );
4705 }
4706 }
4707
4708 #[test]
4709 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
4710 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0);
4715 assert_eq!(parts.len(), 2);
4716
4717 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
4718 assert_eq!(parts[0].offset, 0);
4719 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
4720
4721 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4722 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
4723 assert_eq!(parts[1].len, 4);
4724 }
4725
4726 #[test]
4727 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
4728 let parts = parse_interpolated_string_spanned("hello 世界 world", 0);
4731 assert_eq!(parts.len(), 1);
4732 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
4733 assert_eq!(parts[0].offset, 0);
4734 assert_eq!(parts[0].len, 18);
4735 }
4736
4737 #[test]
4738 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
4739 let parts = parse_interpolated_string_spanned("\\$", 0);
4742 assert_eq!(parts.len(), 1);
4743 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
4744 assert_eq!(parts[0].offset, 0);
4745 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
4746 }
4747
4748 #[test]
4749 fn spanned_escape_backslash_collapses_pair_to_one() {
4750 let parts = parse_interpolated_string_spanned("\\\\", 0);
4751 assert_eq!(parts.len(), 1);
4752 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
4753 assert_eq!(parts[0].len, 2);
4754 }
4755
4756 #[test]
4757 fn spanned_standalone_cr_continuation_realigns_span_start() {
4758 let parts = parse_interpolated_string_spanned("\\\rCD${x}", 0);
4766 assert_eq!(parts.len(), 2);
4767 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "CD"));
4768 assert_eq!(parts[0].offset, 2, "literal run must start after the consumed \\+CR");
4769 assert_eq!(parts[0].len, 2);
4770 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4771 assert_eq!(parts[1].offset, 4);
4772 assert_eq!(parts[1].len, 4); }
4774
4775 #[test]
4776 fn spanned_standalone_cr_continuation_mid_run_keeps_span_start() {
4777 let parts = parse_interpolated_string_spanned("AB\\\rCD${x}", 0);
4782 assert_eq!(parts.len(), 2);
4783 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "ABCD"));
4784 assert_eq!(parts[0].offset, 0);
4785 assert_eq!(parts[0].len, 6); assert!(matches!(&parts[1].part, StringPart::Var(_)));
4787 assert_eq!(parts[1].offset, 6);
4788 assert_eq!(parts[1].len, 4); }
4790
4791 fn assignment_value(source: &str) -> Expr {
4795 let program = parse(source).unwrap_or_else(|e| panic!("parse {source:?}: {e:?}"));
4796 match program.statements.as_slice() {
4797 [Stmt::Assignment(a)] => a.value.clone(),
4798 other => panic!("expected a single assignment, got {other:?}"),
4799 }
4800 }
4801
4802 #[test]
4803 fn list_literal_three_elements() {
4804 let expr = assignment_value("xs=[a b c]");
4805 match expr {
4806 Expr::ListLiteral(elems) => {
4807 assert_eq!(elems.len(), 3);
4808 assert!(elems.iter().all(|e| matches!(e, ListElem::Item(_))));
4809 }
4810 other => panic!("expected ListLiteral, got {other:?}"),
4811 }
4812 }
4813
4814 #[test]
4815 fn list_literal_empty() {
4816 let expr = assignment_value("xs=[]");
4817 assert!(matches!(expr, Expr::ListLiteral(elems) if elems.is_empty()));
4818 }
4819
4820 #[test]
4821 fn list_literal_single_glued_dog() {
4822 let expr = assignment_value("xs=[dog]");
4826 match expr {
4827 Expr::ListLiteral(elems) => assert_eq!(elems.len(), 1),
4828 other => panic!("expected ListLiteral, got {other:?}"),
4829 }
4830 }
4831
4832 #[test]
4833 fn list_literal_single_int() {
4834 let expr = assignment_value("xs=[1]");
4835 match expr {
4836 Expr::ListLiteral(elems) => match elems.as_slice() {
4837 [ListElem::Item(Expr::Literal(Value::Int(1)))] => {}
4838 other => panic!("expected one Int(1) item, got {other:?}"),
4839 },
4840 other => panic!("expected ListLiteral, got {other:?}"),
4841 }
4842 }
4843
4844 #[test]
4845 fn record_literal_unspaced_colon_equals_spaced() {
4846 let spaced = assignment_value("x={port: 8080}");
4847 let unspaced = assignment_value("x={port:8080}");
4848 assert_eq!(spaced, unspaced, "{{port:8080}} must parse identically to {{port: 8080}}");
4849 match spaced {
4850 Expr::RecordLiteral(entries) => match entries.as_slice() {
4851 [RecordEntry { key: RecordKey::Bare(k), value: Expr::Literal(Value::Int(8080)) }] => {
4852 assert_eq!(k, "port");
4853 }
4854 other => panic!("expected one port:8080 entry, got {other:?}"),
4855 },
4856 other => panic!("expected RecordLiteral, got {other:?}"),
4857 }
4858 }
4859
4860 #[test]
4861 fn record_literal_name_role() {
4862 let expr = assignment_value("u={name: amy, role: maintainer}");
4863 match expr {
4864 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2),
4865 other => panic!("expected RecordLiteral, got {other:?}"),
4866 }
4867 }
4868
4869 #[test]
4870 fn record_literal_multiline_trailing_comma() {
4871 let source = "services={\n web: {port: 8080, replicas: 3, healthy: true},\n api: {port: 9000, replicas: 2, healthy: false},\n}";
4872 let expr = assignment_value(source);
4873 match expr {
4874 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2, "web + api entries"),
4875 other => panic!("expected RecordLiteral, got {other:?}"),
4876 }
4877 }
4878
4879 #[test]
4880 fn record_literal_quoted_key() {
4881 let expr = assignment_value(r#"r={"content-type": x}"#);
4882 match expr {
4883 Expr::RecordLiteral(entries) => match entries.as_slice() {
4884 [RecordEntry { key: RecordKey::Quoted(k), .. }] => assert_eq!(k, "content-type"),
4885 other => panic!("expected one quoted-key entry, got {other:?}"),
4886 },
4887 other => panic!("expected RecordLiteral, got {other:?}"),
4888 }
4889 }
4890
4891 #[test]
4892 fn nested_list_and_record_in_record() {
4893 let expr = assignment_value("x={tags: [a b], meta: {active: true}}");
4894 match expr {
4895 Expr::RecordLiteral(entries) => {
4896 assert_eq!(entries.len(), 2);
4897 assert!(matches!(entries[0].value, Expr::ListLiteral(_)));
4898 assert!(matches!(entries[1].value, Expr::RecordLiteral(_)));
4899 }
4900 other => panic!("expected RecordLiteral, got {other:?}"),
4901 }
4902 }
4903
4904 #[test]
4905 fn spread_and_item_elements() {
4906 let expr = assignment_value("new=[...$xs date]");
4907 match expr {
4908 Expr::ListLiteral(elems) => match elems.as_slice() {
4909 [ListElem::Spread(Expr::VarRef(_)), ListElem::Item(Expr::Literal(Value::String(s)))] => {
4910 assert_eq!(s, "date");
4911 }
4912 other => panic!("expected [Spread($xs), Item(date)], got {other:?}"),
4913 },
4914 other => panic!("expected ListLiteral, got {other:?}"),
4915 }
4916 }
4917
4918 #[test]
4919 fn spread_of_two_variables() {
4920 let expr = assignment_value("c=[...$a ...$b]");
4921 match expr {
4922 Expr::ListLiteral(elems) => {
4923 assert_eq!(elems.len(), 2);
4924 assert!(elems.iter().all(|e| matches!(e, ListElem::Spread(_))));
4925 }
4926 other => panic!("expected ListLiteral, got {other:?}"),
4927 }
4928 }
4929
4930 #[test]
4931 fn in_rhs_accepts_a_list_literal() {
4932 let program = parse("if [[ $a not in [dog] ]]; then echo hit; fi")
4933 .unwrap_or_else(|e| panic!("parse: {e:?}"));
4934 assert_eq!(program.statements.len(), 1);
4935 }
4936
4937 #[test]
4938 fn multiword_bareword_record_value_is_a_parse_error() {
4939 assert!(parse("x={msg: hello world}").is_err());
4942 }
4943
4944 #[test]
4947 fn argv_bracket_glob_stays_a_glob_pattern() {
4948 let program = parse("ls [dog]").unwrap_or_else(|e| panic!("parse: {e:?}"));
4952 assert_eq!(program.statements.len(), 1);
4953 }
4954
4955 #[test]
4956 fn brace_expansion_at_argv_position_is_unaffected() {
4957 let program = parse("cmd *.{rs,go}").unwrap_or_else(|e| panic!("parse: {e:?}"));
4962 assert_eq!(program.statements.len(), 1);
4963 }
4964
4965 #[test]
4966 fn for_head_item_is_not_a_literal() {
4967 let program = parse("for x in [a]; do echo $x; done")
4970 .unwrap_or_else(|e| panic!("parse: {e:?}"));
4971 match program.statements.as_slice() {
4972 [Stmt::For(for_loop)] => {
4973 assert_eq!(for_loop.items.len(), 1);
4974 assert!(
4975 !matches!(for_loop.items[0], Expr::ListLiteral(_)),
4976 "for-head item must not be a ListLiteral: {:?}",
4977 for_loop.items[0]
4978 );
4979 }
4980 other => panic!("expected a single For statement, got {other:?}"),
4981 }
4982 }
4983}