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 {
85 let mut out = String::with_capacity(word.len());
86 let mut in_single = false;
87 let mut in_double = false;
88 let chars: Vec<char> = word.chars().collect();
89 let mut i = 0;
90 while i < chars.len() {
91 let ch = chars[i];
92 if ch == '\\' && !in_single {
96 let run_start = i;
97 while i < chars.len() && chars[i] == '\\' {
98 i += 1;
99 }
100 let run_len = i - run_start;
101 let next_is_quote =
104 chars.get(i).is_some_and(|c| *c == '"' || (*c == '\'' && !in_double));
105 if next_is_quote {
106 if run_len / 2 > 0 {
107 out.push_str(&"\\".repeat(run_len / 2));
108 }
109 if run_len % 2 == 1 {
110 out.push(chars[i]);
114 i += 1;
115 }
116 } else {
119 out.push_str(&"\\".repeat(run_len));
120 }
121 continue;
122 }
123 i += 1;
124 match ch {
125 '\'' if !in_double => in_single = !in_single,
128 '"' if !in_single => in_double = !in_double,
129 '$' if in_single => out.push_str("__KAISH_ESCAPED_DOLLAR__"),
131 _ => out.push(ch),
132 }
133 }
134 out
135}
136
137fn find_default_separator(raw: &str) -> Option<usize> {
139 let bytes = raw.as_bytes();
140 let mut depth = 0;
141 let mut bracket_depth = 0;
142 let mut i = 0;
143
144 while i < bytes.len() {
145 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
146 depth += 1;
147 i += 2;
148 continue;
149 }
150 if bytes[i] == b'}' && depth > 0 {
151 depth -= 1;
152 i += 1;
153 continue;
154 }
155 if bytes[i] == b'[' {
158 bracket_depth += 1;
159 } else if bytes[i] == b']' && bracket_depth > 0 {
160 bracket_depth -= 1;
161 }
162 if depth == 1
165 && bracket_depth == 0
166 && i + 1 < bytes.len()
167 && bytes[i] == b':'
168 && bytes[i + 1] == b'-'
169 {
170 return Some(i);
171 }
172 i += 1;
173 }
174 None
175}
176
177fn find_default_separator_in_content(content: &str) -> Option<usize> {
179 let bytes = content.as_bytes();
180 let mut depth = 0;
181 let mut bracket_depth = 0;
182 let mut i = 0;
183
184 while i < bytes.len() {
185 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
186 depth += 1;
187 i += 2;
188 continue;
189 }
190 if bytes[i] == b'}' && depth > 0 {
191 depth -= 1;
192 i += 1;
193 continue;
194 }
195 if bytes[i] == b'[' {
198 bracket_depth += 1;
199 } else if bytes[i] == b']' && bracket_depth > 0 {
200 bracket_depth -= 1;
201 }
202 if depth == 0
204 && bracket_depth == 0
205 && i + 1 < bytes.len()
206 && bytes[i] == b':'
207 && bytes[i + 1] == b'-'
208 {
209 return Some(i);
210 }
211 i += 1;
212 }
213 None
214}
215
216pub(crate) fn parse_varpath(raw: &str) -> VarPath {
223 let segment_strs = lexer::parse_var_ref(raw).unwrap_or_default();
224 let segments = segment_strs
225 .into_iter()
226 .enumerate()
227 .map(|(i, s)| {
228 if i == 0 {
229 VarSegment::Field(s)
231 } else if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
232 parse_subscript(inner)
233 } else {
234 VarSegment::Field(s)
237 }
238 })
239 .collect();
240 VarPath { segments }
241}
242
243fn parse_subscript(inner: &str) -> VarSegment {
250 if let Some(var) = inner.strip_prefix('$') {
252 return VarSegment::Dynamic(var.to_string());
253 }
254 if inner.len() >= 2
256 && ((inner.starts_with('"') && inner.ends_with('"'))
257 || (inner.starts_with('\'') && inner.ends_with('\'')))
258 {
259 return VarSegment::Key(inner[1..inner.len() - 1].to_string());
260 }
261 if let Some((lhs, rhs)) = inner.split_once(':') {
265 let bound = |s: &str| -> Option<Option<i64>> {
266 if s.is_empty() {
267 Some(None)
268 } else {
269 s.parse::<i64>().ok().map(Some)
270 }
271 };
272 if let (Some(start), Some(end)) = (bound(lhs), bound(rhs)) {
273 return VarSegment::Slice(start, end);
274 }
275 }
276 if let Ok(i) = inner.parse::<i64>() {
278 return VarSegment::Index(i);
279 }
280 VarSegment::Key(inner.to_string())
282}
283
284fn strip_empty_stmts(statements: Vec<Stmt>) -> Vec<Stmt> {
287 statements
288 .into_iter()
289 .filter(|s| !matches!(s, Stmt::Empty))
290 .collect()
291}
292
293fn parse_interpolated_string_spanned(s: &str, base_offset: usize) -> Vec<SpannedPart> {
312 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
313
314 let chars_vec: Vec<char> = s.chars().collect();
315 let mut i = 0;
316 let mut pos: usize = 0;
317
318 let mut parts: Vec<SpannedPart> = Vec::new();
319 let mut current_text = String::new();
320 let mut current_text_start: usize = pos;
321
322 let push_literal =
323 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
324 if !current_text.is_empty() {
325 parts.push(SpannedPart {
326 part: StringPart::Literal(std::mem::take(current_text)),
327 offset: base_offset + *start,
328 len: end - *start,
329 });
330 *start = end;
331 }
332 };
333
334 while i < chars_vec.len() {
335 let ch = chars_vec[i];
336
337 if ch == '\x00' {
338 let start = pos;
340 i += 1;
341 pos += 1;
342 let mut marker = String::new();
343 while let Some(&c) = chars_vec.get(i) {
344 if c == '\x00' {
345 i += 1;
346 pos += 1;
347 break;
348 }
349 marker.push(c);
350 i += 1;
351 pos += c.len_utf8();
352 }
353 if marker == "DOLLAR" {
354 if current_text.is_empty() {
355 current_text_start = start;
356 }
357 current_text.push('$');
358 }
359 } else if ch == '\\' {
360 let next = chars_vec.get(i + 1).copied();
366 match next {
367 Some('$') => {
368 if current_text.is_empty() {
369 current_text_start = pos;
370 }
371 current_text.push('$');
372 i += 2;
373 pos += 2;
374 }
375 Some('\\') => {
376 if current_text.is_empty() {
377 current_text_start = pos;
378 }
379 current_text.push('\\');
380 i += 2;
381 pos += 2;
382 }
383 Some('\n') => {
384 i += 2;
387 pos += 2;
388 if current_text.is_empty() {
389 current_text_start = pos;
390 }
391 }
392 Some('\r') => {
393 i += 2;
395 pos += 2;
396 if chars_vec.get(i) == Some(&'\n') {
397 i += 1;
398 pos += 1;
399 }
400 if current_text.is_empty() {
401 current_text_start = pos;
402 }
403 }
404 _ => {
405 if current_text.is_empty() {
409 current_text_start = pos;
410 }
411 current_text.push('\\');
412 i += 1;
413 pos += 1;
414 }
415 }
416 } else if ch == '$' {
417 let part_start = pos;
419 let next = chars_vec.get(i + 1).copied();
420
421 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
422 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
424 i += 2; pos += 2;
426 let mut cmd_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 == '(' {
432 depth += 1;
433 cmd_content.push(c);
434 } else if c == ')' {
435 depth -= 1;
436 if depth == 0 {
437 break;
438 }
439 cmd_content.push(c);
440 } else {
441 cmd_content.push(c);
442 }
443 }
444 let inserted = if let Ok(program) = parse(&cmd_content) {
445 let stmts = strip_empty_stmts(program.statements);
448 if stmts.is_empty() {
449 false
450 } else {
451 parts.push(SpannedPart {
452 part: StringPart::CommandSubst(stmts),
453 offset: base_offset + part_start,
454 len: pos - part_start,
455 });
456 true
457 }
458 } else {
459 false
460 };
461 if inserted {
462 current_text_start = pos;
465 } else {
466 if current_text.is_empty() {
471 current_text_start = part_start;
472 }
473 current_text.push_str("$(");
474 current_text.push_str(&cmd_content);
475 current_text.push(')');
476 }
477 } else if next == Some('{') {
478 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
479 i += 2; pos += 2;
481 let mut var_content = String::new();
482 let mut depth = 1;
483 while let Some(&c) = chars_vec.get(i) {
484 i += 1;
485 pos += c.len_utf8();
486 if c == '{' && var_content.ends_with('$') {
487 depth += 1;
488 var_content.push(c);
489 } else if c == '}' {
490 depth -= 1;
491 if depth == 0 {
492 break;
493 }
494 var_content.push(c);
495 } else {
496 var_content.push(c);
497 }
498 }
499 let part = if let Some(name) = var_content.strip_prefix('#') {
500 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
501 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
502 let expr = var_content
503 .strip_prefix("__ARITH:")
504 .and_then(|s| s.strip_suffix("__"))
505 .unwrap_or("");
506 StringPart::Arithmetic(expr.to_string())
507 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
508 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
509 let default_str = &var_content[colon_idx + 2..];
510 let default_word = unquote_default_word(default_str);
515 let default = parse_interpolated_string(&default_word)
516 .unwrap_or_else(|_| vec![StringPart::Literal(default_word.clone())]);
517 StringPart::VarWithDefault { path, default }
518 } else {
519 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
520 };
521 parts.push(SpannedPart {
522 part,
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_digit()).unwrap_or(false) {
528 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
529 i += 1; pos += 1;
531 if let Some(&digit) = chars_vec.get(i) {
532 let n = digit.to_digit(10).unwrap_or(0) as usize;
533 i += 1;
534 pos += digit.len_utf8();
535 parts.push(SpannedPart {
536 part: StringPart::Positional(n),
537 offset: base_offset + part_start,
538 len: pos - part_start,
539 });
540 }
541 current_text_start = pos;
542 } else if next == Some('@') {
543 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
544 i += 2; pos += 2;
546 parts.push(SpannedPart {
547 part: StringPart::AllArgs,
548 offset: base_offset + part_start,
549 len: pos - part_start,
550 });
551 current_text_start = pos;
552 } else if next == Some('#') {
553 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
554 i += 2; pos += 2;
556 parts.push(SpannedPart {
557 part: StringPart::ArgCount,
558 offset: base_offset + part_start,
559 len: pos - part_start,
560 });
561 current_text_start = pos;
562 } else if next == Some('?') {
563 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
564 i += 2; pos += 2;
566 parts.push(SpannedPart {
567 part: StringPart::LastExitCode,
568 offset: base_offset + part_start,
569 len: pos - part_start,
570 });
571 current_text_start = pos;
572 } else if next == Some('$') {
573 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
574 i += 2; pos += 2;
576 parts.push(SpannedPart {
577 part: StringPart::CurrentPid,
578 offset: base_offset + part_start,
579 len: pos - part_start,
580 });
581 current_text_start = pos;
582 } else if next.map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
583 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
584 i += 1; pos += 1;
586 let mut var_name = String::new();
587 while let Some(&c) = chars_vec.get(i) {
588 if c.is_ascii_alphanumeric() || c == '_' {
589 var_name.push(c);
590 i += 1;
591 pos += c.len_utf8();
592 } else {
593 break;
594 }
595 }
596 parts.push(SpannedPart {
597 part: StringPart::Var(VarPath::simple(var_name)),
598 offset: base_offset + part_start,
599 len: pos - part_start,
600 });
601 current_text_start = pos;
602 } else {
603 if current_text.is_empty() {
605 current_text_start = pos;
606 }
607 current_text.push(ch);
608 i += 1;
609 pos += 1;
610 }
611 } else {
612 if current_text.is_empty() {
613 current_text_start = pos;
614 }
615 current_text.push(ch);
616 i += 1;
617 pos += ch.len_utf8();
618 }
619 }
620
621 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
622
623 parts
624}
625
626fn parse_interpolated_string(s: &str) -> Result<Vec<StringPart>, String> {
627 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
630
631 let mut parts = Vec::new();
632 let mut current_text = String::new();
633 let mut chars = s.chars().peekable();
634
635 while let Some(ch) = chars.next() {
636 if ch == '\x00' {
637 let mut marker = String::new();
639 while let Some(&c) = chars.peek() {
640 if c == '\x00' {
641 chars.next(); break;
643 }
644 if let Some(c) = chars.next() {
645 marker.push(c);
646 }
647 }
648 if marker == "DOLLAR" {
649 current_text.push('$');
650 }
651 } else if ch == '$' {
652 if chars.peek() == Some(&'(') {
654 if !current_text.is_empty() {
656 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
657 }
658
659 chars.next();
661
662 let mut cmd_content = String::new();
664 let mut paren_depth = 1;
665 for c in chars.by_ref() {
666 if c == '(' {
667 paren_depth += 1;
668 cmd_content.push(c);
669 } else if c == ')' {
670 paren_depth -= 1;
671 if paren_depth == 0 {
672 break;
673 }
674 cmd_content.push(c);
675 } else {
676 cmd_content.push(c);
677 }
678 }
679
680 match parse(&cmd_content) {
683 Ok(program) => {
684 let stmts = strip_empty_stmts(program.statements);
685 if stmts.is_empty() {
686 current_text.push_str("$(");
689 current_text.push_str(&cmd_content);
690 current_text.push(')');
691 } else {
692 parts.push(StringPart::CommandSubst(stmts));
693 }
694 }
695 Err(_) => {
696 return Err(format!(
700 "syntax error in command substitution: $({cmd_content})"
701 ));
702 }
703 }
704 } else if chars.peek() == Some(&'{') {
705 if !current_text.is_empty() {
707 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
708 }
709
710 chars.next();
712
713 let mut var_content = String::new();
715 let mut depth = 1;
716 for c in chars.by_ref() {
717 if c == '{' && var_content.ends_with('$') {
718 depth += 1;
719 var_content.push(c);
720 } else if c == '}' {
721 depth -= 1;
722 if depth == 0 {
723 break;
724 }
725 var_content.push(c);
726 } else {
727 var_content.push(c);
728 }
729 }
730
731 let part = if let Some(name) = var_content.strip_prefix('#') {
733 StringPart::VarLength(parse_varpath(&format!("${{{name}}}")))
735 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
736 let expr = var_content
738 .strip_prefix("__ARITH:")
739 .and_then(|s| s.strip_suffix("__"))
740 .unwrap_or("");
741 StringPart::Arithmetic(expr.to_string())
742 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
743 let path = parse_varpath(&format!("${{{}}}", &var_content[..colon_idx]));
745 let default_str = &var_content[colon_idx + 2..];
746 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
747 StringPart::VarWithDefault { path, default }
748 } else {
749 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
751 };
752 parts.push(part);
753 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
754 if !current_text.is_empty() {
756 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
757 }
758 if let Some(digit) = chars.next() {
759 let n = digit.to_digit(10).unwrap_or(0) as usize;
760 parts.push(StringPart::Positional(n));
761 }
762 } else if chars.peek() == Some(&'@') {
763 if !current_text.is_empty() {
765 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
766 }
767 chars.next(); parts.push(StringPart::AllArgs);
769 } else if chars.peek() == Some(&'#') {
770 if !current_text.is_empty() {
772 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
773 }
774 chars.next(); parts.push(StringPart::ArgCount);
776 } else if chars.peek() == Some(&'?') {
777 if !current_text.is_empty() {
779 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
780 }
781 chars.next(); parts.push(StringPart::LastExitCode);
783 } else if chars.peek() == Some(&'$') {
784 if !current_text.is_empty() {
786 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
787 }
788 chars.next(); parts.push(StringPart::CurrentPid);
790 } else if chars.peek().map(|c| c.is_ascii_alphabetic() || *c == '_').unwrap_or(false) {
791 if !current_text.is_empty() {
793 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
794 }
795
796 let mut var_name = String::new();
798 while let Some(&c) = chars.peek() {
799 if c.is_ascii_alphanumeric() || c == '_' {
800 if let Some(c) = chars.next() {
801 var_name.push(c);
802 }
803 } else {
804 break;
805 }
806 }
807
808 parts.push(StringPart::Var(VarPath::simple(var_name)));
809 } else {
810 current_text.push(ch);
812 }
813 } else {
814 current_text.push(ch);
815 }
816 }
817
818 if !current_text.is_empty() {
819 parts.push(StringPart::Literal(current_text));
820 }
821
822 Ok(parts)
823}
824
825#[derive(Debug, Clone)]
827pub struct ParseError {
828 pub span: Span,
829 pub message: String,
830}
831
832impl ParseError {
833 pub fn format(&self, source: &str) -> String {
838 let start = self.span.start;
839 let mut line = 1usize;
840 let mut col = 1usize;
841 for (i, ch) in source.char_indices() {
842 if i >= start {
843 break;
844 }
845 if ch == '\n' {
846 line += 1;
847 col = 1;
848 } else {
849 col += 1;
850 }
851 }
852 let line_content = {
853 let line_start = source[..start.min(source.len())]
854 .rfind('\n')
855 .map_or(0, |i| i + 1);
856 let line_end = source[start.min(source.len())..]
857 .find('\n')
858 .map_or(source.len(), |i| start + i);
859 source.get(line_start..line_end).unwrap_or("")
860 };
861 if line_content.is_empty() {
862 format!("{}:{} [parse]: {}", line, col, self.message)
863 } else {
864 format!(
865 "{}:{} [parse]: {}\n | {}",
866 line, col, self.message, line_content
867 )
868 }
869 }
870}
871
872impl std::fmt::Display for ParseError {
873 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
874 write!(f, "{} at {:?}", self.message, self.span)
875 }
876}
877
878impl std::error::Error for ParseError {}
879
880pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
882 let tokens = lexer::tokenize(source).map_err(|errs| {
884 errs.into_iter()
885 .map(|e| ParseError {
886 span: (e.span.start..e.span.end).into(),
887 message: format!("lexer error: {}", e.token),
888 })
889 .collect::<Vec<_>>()
890 })?;
891
892 let tokens: Vec<(Token, Span)> = tokens
894 .into_iter()
895 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
896 .collect();
897
898 let end_span: Span = (source.len()..source.len()).into();
900
901 let parser = program_parser();
903 let result = parser.parse(tokens.as_slice().map(end_span, |(t, s)| (t, s)));
904
905 let program = result.into_result().map_err(|errs| {
906 errs.into_iter()
907 .map(|e| ParseError {
908 span: *e.span(),
909 message: e.to_string(),
910 })
911 .collect::<Vec<_>>()
912 })?;
913
914 if first_ambiguous_stdin(&program.statements) {
919 return Err(vec![ParseError {
920 span: (0..0).into(),
924 message: "multiple stdin redirects on one command are ambiguous; \
925 use exactly one of `<`, `<<`, or `<<<`"
926 .to_string(),
927 }]);
928 }
929
930 Ok(program)
931}
932
933pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
935 let program = parse(source)?;
936 program
937 .statements
938 .into_iter()
939 .find(|s| !matches!(s, Stmt::Empty))
940 .ok_or_else(|| {
941 vec![ParseError {
942 span: (0..source.len()).into(),
943 message: "empty input".to_string(),
944 }]
945 })
946}
947
948fn program_parser<'tokens, 'src: 'tokens, I>(
954) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
955where
956 I: ValueInput<'tokens, Token = Token, Span = Span>,
957{
958 statement_parser()
959 .repeated()
960 .collect::<Vec<_>>()
961 .map(|statements| Program { statements })
962}
963
964fn statement_parser<'tokens, I>(
967) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
968where
969 I: ValueInput<'tokens, Token = Token, Span = Span>,
970{
971 recursive(|stmt| {
972 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
973
974 let break_stmt = just(Token::Break)
976 .ignore_then(
977 select! { Token::Int(n) => n as usize }.or_not()
978 )
979 .map(Stmt::Break);
980
981 let continue_stmt = just(Token::Continue)
983 .ignore_then(
984 select! { Token::Int(n) => n as usize }.or_not()
985 )
986 .map(Stmt::Continue);
987
988 let return_stmt = just(Token::Return)
990 .ignore_then(primary_expr_parser().or_not())
991 .map(|e| Stmt::Return(e.map(Box::new)));
992
993 let exit_stmt = just(Token::Exit)
995 .ignore_then(primary_expr_parser().or_not())
996 .map(|e| Stmt::Exit(e.map(Box::new)));
997
998 let set_flag_arg = choice((
1007 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
1008 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
1009 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
1011 ));
1012
1013 let option_value_str = select! {
1017 Token::NumberIdent(s) => s,
1018 Token::Int(n) => n.to_string(),
1019 Token::Ident(s) => s,
1020 };
1021
1022 let set_option_assign = ident_parser()
1026 .then_ignore(just(Token::Eq))
1027 .then(option_value_str)
1028 .map(|(name, value)| {
1029 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
1030 });
1031
1032 let set_quoted_arg = select! {
1036 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1037 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
1038 };
1039
1040 let set_with_flags = just(Token::Set)
1042 .then(set_flag_arg)
1043 .then(
1044 choice((
1045 set_flag_arg,
1046 set_option_assign,
1048 set_quoted_arg,
1049 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
1051 ))
1052 .repeated()
1053 .collect::<Vec<_>>(),
1054 )
1055 .map(|((_, first_arg), mut rest_args)| {
1056 let mut args = vec![first_arg];
1057 args.append(&mut rest_args);
1058 Stmt::Command(Command {
1059 name: "set".to_string(),
1060 args,
1061 redirects: vec![],
1062 })
1063 });
1064
1065 let set_no_args = just(Token::Set)
1068 .then(
1069 choice((
1070 just(Token::Newline).to(()),
1071 just(Token::Semi).to(()),
1072 just(Token::And).to(()),
1073 just(Token::Or).to(()),
1074 end(),
1075 ))
1076 .rewind(),
1077 )
1078 .map(|_| Stmt::Command(Command {
1079 name: "set".to_string(),
1080 args: vec![],
1081 redirects: vec![],
1082 }));
1083
1084 let set_command = set_with_flags.or(set_no_args);
1088
1089 let env_prefix_assign = ident_parser()
1106 .then_ignore(just(Token::Eq))
1107 .then(value_expr_parser())
1108 .map(|(name, value)| Assignment { path: VarPath::simple(name), value, local: false });
1109 let env_scoped = env_prefix_assign
1110 .repeated()
1111 .at_least(1)
1112 .collect::<Vec<_>>()
1113 .then(pipeline_parser().map(pipeline_into_stmt))
1114 .map(|(assignments, body)| Stmt::EnvScoped {
1115 assignments,
1116 body: Box::new(body),
1117 });
1118
1119 let base_statement = choice((
1121 just(Token::Newline).to(Stmt::Empty),
1122 set_command,
1123 env_scoped,
1124 assignment_parser().map(Stmt::Assignment),
1125 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), if_parser(stmt.clone()).map(Stmt::If),
1129 for_parser(stmt.clone()).map(Stmt::For),
1130 while_parser(stmt.clone()).map(Stmt::While),
1131 case_parser(stmt.clone()).map(Stmt::Case),
1132 break_stmt,
1133 continue_stmt,
1134 return_stmt,
1135 exit_stmt,
1136 test_expr_stmt_parser().map(Stmt::Test),
1137 pipeline_parser().map(pipeline_into_stmt),
1139 ))
1140 .boxed();
1141
1142 base_statement
1148 .clone()
1149 .foldl(
1150 choice((
1151 just(Token::And).to(true), just(Token::Or).to(false), ))
1154 .then(base_statement)
1155 .repeated(),
1156 |left, (is_and, right): (bool, Stmt)| {
1157 if is_and {
1158 Stmt::AndChain {
1159 left: Box::new(left),
1160 right: Box::new(right),
1161 }
1162 } else {
1163 Stmt::OrChain {
1164 left: Box::new(left),
1165 right: Box::new(right),
1166 }
1167 }
1168 },
1169 )
1170 .then_ignore(terminator)
1171 })
1172}
1173
1174fn lvalue_subscript_parser<'tokens, I>(
1184) -> impl Parser<'tokens, I, VarSegment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1185where
1186 I: ValueInput<'tokens, Token = Token, Span = Span>,
1187{
1188 let interior = choice((
1189 select! { Token::SimpleVarRef(name) => VarSegment::Dynamic(name) },
1190 select! { Token::String(s) => VarSegment::Key(s) },
1191 select! { Token::SingleString(s) => VarSegment::Key(s) },
1192 select! { Token::Int(n) => VarSegment::Index(n) },
1193 select! { Token::Ident(s) => parse_subscript(&s) },
1194 ));
1195
1196 just(Token::LBracket)
1197 .ignore_then(interior)
1198 .then_ignore(just(Token::RBracket))
1199 .labelled("subscript")
1200}
1201
1202fn lvalue_path_parser<'tokens, I>(
1208) -> impl Parser<'tokens, I, VarPath, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1209where
1210 I: ValueInput<'tokens, Token = Token, Span = Span>,
1211{
1212 ident_parser()
1213 .then(lvalue_subscript_parser().repeated().collect::<Vec<_>>())
1214 .map(|(name, subscripts)| {
1215 let mut segments = vec![VarSegment::Field(name)];
1216 segments.extend(subscripts);
1217 VarPath { segments }
1218 })
1219 .labelled("lvalue path")
1220}
1221
1222fn assignment_parser<'tokens, I>(
1228) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1229where
1230 I: ValueInput<'tokens, Token = Token, Span = Span>,
1231{
1232 let local_assignment = just(Token::Local)
1234 .ignore_then(lvalue_path_parser())
1235 .then_ignore(just(Token::Eq))
1236 .then(value_expr_parser())
1237 .map(|(path, value)| Assignment {
1238 path,
1239 value,
1240 local: true,
1241 });
1242
1243 let bash_assignment = lvalue_path_parser()
1246 .then_ignore(just(Token::Eq))
1247 .then(value_expr_parser())
1248 .map(|(path, value)| Assignment {
1249 path,
1250 value,
1251 local: false,
1252 });
1253
1254 choice((local_assignment, bash_assignment))
1255 .labelled("assignment")
1256 .boxed()
1257}
1258
1259fn posix_function_parser<'tokens, I, S>(
1263 stmt: S,
1264) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1265where
1266 I: ValueInput<'tokens, Token = Token, Span = Span>,
1267 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1268{
1269 ident_parser()
1270 .then_ignore(just(Token::LParen))
1271 .then_ignore(just(Token::RParen))
1272 .then_ignore(just(Token::LBrace))
1273 .then_ignore(just(Token::Newline).repeated())
1274 .then(
1275 stmt.repeated()
1276 .collect::<Vec<_>>()
1277 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1278 )
1279 .then_ignore(just(Token::Newline).repeated())
1280 .then_ignore(just(Token::RBrace))
1281 .map(|(name, body)| ToolDef { name, params: vec![], body })
1282 .labelled("POSIX function")
1283 .boxed()
1284}
1285
1286fn bash_function_parser<'tokens, I, S>(
1290 stmt: S,
1291) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1292where
1293 I: ValueInput<'tokens, Token = Token, Span = Span>,
1294 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1295{
1296 just(Token::Function)
1297 .ignore_then(ident_parser())
1298 .then_ignore(just(Token::LBrace))
1299 .then_ignore(just(Token::Newline).repeated())
1300 .then(
1301 stmt.repeated()
1302 .collect::<Vec<_>>()
1303 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1304 )
1305 .then_ignore(just(Token::Newline).repeated())
1306 .then_ignore(just(Token::RBrace))
1307 .map(|(name, body)| ToolDef { name, params: vec![], body })
1308 .labelled("bash function")
1309 .boxed()
1310}
1311
1312fn if_parser<'tokens, I, S>(
1319 stmt: S,
1320) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1321where
1322 I: ValueInput<'tokens, Token = Token, Span = Span>,
1323 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1324{
1325 let branch = condition_parser()
1327 .then_ignore(just(Token::Semi).or_not())
1328 .then_ignore(just(Token::Newline).repeated())
1329 .then_ignore(just(Token::Then))
1330 .then_ignore(just(Token::Newline).repeated())
1331 .then(
1332 stmt.clone()
1333 .repeated()
1334 .collect::<Vec<_>>()
1335 .map(|stmts: Vec<Stmt>| {
1336 stmts
1337 .into_iter()
1338 .filter(|s| !matches!(s, Stmt::Empty))
1339 .collect::<Vec<_>>()
1340 }),
1341 );
1342
1343 let elif_branch = just(Token::Elif)
1345 .ignore_then(condition_parser())
1346 .then_ignore(just(Token::Semi).or_not())
1347 .then_ignore(just(Token::Newline).repeated())
1348 .then_ignore(just(Token::Then))
1349 .then_ignore(just(Token::Newline).repeated())
1350 .then(
1351 stmt.clone()
1352 .repeated()
1353 .collect::<Vec<_>>()
1354 .map(|stmts: Vec<Stmt>| {
1355 stmts
1356 .into_iter()
1357 .filter(|s| !matches!(s, Stmt::Empty))
1358 .collect::<Vec<_>>()
1359 }),
1360 );
1361
1362 let else_branch = just(Token::Else)
1364 .ignore_then(just(Token::Newline).repeated())
1365 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1366 .map(|stmts: Vec<Stmt>| {
1367 stmts
1368 .into_iter()
1369 .filter(|s| !matches!(s, Stmt::Empty))
1370 .collect::<Vec<_>>()
1371 });
1372
1373 just(Token::If)
1374 .ignore_then(branch)
1375 .then(elif_branch.repeated().collect::<Vec<_>>())
1376 .then(else_branch.or_not())
1377 .then_ignore(just(Token::Fi))
1378 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1379 build_if_chain(condition, then_branch, elif_branches, else_branch)
1381 })
1382 .labelled("if statement")
1383 .boxed()
1384}
1385
1386fn build_if_chain(
1393 condition: Expr,
1394 then_branch: Vec<Stmt>,
1395 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1396 else_branch: Option<Vec<Stmt>>,
1397) -> IfStmt {
1398 if elif_branches.is_empty() {
1399 IfStmt {
1401 condition: Box::new(condition),
1402 then_branch,
1403 else_branch,
1404 }
1405 } else {
1406 let (elif_cond, elif_then) = elif_branches.remove(0);
1408 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1409 IfStmt {
1410 condition: Box::new(condition),
1411 then_branch,
1412 else_branch: Some(vec![Stmt::If(nested_if)]),
1413 }
1414 }
1415}
1416
1417fn for_parser<'tokens, I, S>(
1419 stmt: S,
1420) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1421where
1422 I: ValueInput<'tokens, Token = Token, Span = Span>,
1423 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1424{
1425 just(Token::For)
1426 .ignore_then(ident_parser())
1427 .then_ignore(just(Token::In))
1428 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1429 .then_ignore(just(Token::Semi).or_not())
1430 .then_ignore(just(Token::Newline).repeated())
1431 .then_ignore(just(Token::Do))
1432 .then_ignore(just(Token::Newline).repeated())
1433 .then(
1434 stmt.repeated()
1435 .collect::<Vec<_>>()
1436 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1437 )
1438 .then_ignore(just(Token::Done))
1439 .map(|((variable, items), body)| ForLoop {
1440 variable,
1441 items,
1442 body,
1443 })
1444 .labelled("for loop")
1445 .boxed()
1446}
1447
1448fn while_parser<'tokens, I, S>(
1450 stmt: S,
1451) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1452where
1453 I: ValueInput<'tokens, Token = Token, Span = Span>,
1454 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1455{
1456 just(Token::While)
1457 .ignore_then(condition_parser())
1458 .then_ignore(just(Token::Semi).or_not())
1459 .then_ignore(just(Token::Newline).repeated())
1460 .then_ignore(just(Token::Do))
1461 .then_ignore(just(Token::Newline).repeated())
1462 .then(
1463 stmt.repeated()
1464 .collect::<Vec<_>>()
1465 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1466 )
1467 .then_ignore(just(Token::Done))
1468 .map(|(condition, body)| WhileLoop {
1469 condition: Box::new(condition),
1470 body,
1471 })
1472 .labelled("while loop")
1473 .boxed()
1474}
1475
1476fn case_parser<'tokens, I, S>(
1483 stmt: S,
1484) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1485where
1486 I: ValueInput<'tokens, Token = Token, Span = Span>,
1487 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1488{
1489 let pattern_part = choice((
1492 select! { Token::GlobWord(s) => s },
1493 select! { Token::Ident(s) => s },
1494 select! { Token::NumberIdent(s) => s },
1495 select! { Token::DashNumWord(s) => s },
1496 select! { Token::AtWord(s) => s },
1497 select! { Token::DottedIdent(s) => s },
1498 select! { Token::String(s) => s },
1499 select! { Token::SingleString(s) => s },
1500 select! { Token::Int(n) => n.to_string() },
1501 select! { Token::Star => "*".to_string() },
1502 select! { Token::Question => "?".to_string() },
1503 select! { Token::Dot => ".".to_string() },
1504 select! { Token::DotDot => "..".to_string() },
1505 select! { Token::Tilde => "~".to_string() },
1506 select! { Token::TildePath(s) => s },
1507 select! { Token::RelativePath(s) => s },
1508 select! { Token::DotSlashPath(s) => s },
1509 select! { Token::Path(p) => p },
1510 select! { Token::VarRef(v) => v },
1511 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1512 choice((
1520 select! { Token::DoubleDashBare(s) => s },
1521 select! { Token::PlusBare(s) => s },
1522 select! { Token::MinusBare(s) => s },
1523 select! { Token::MinusAlone => "-".to_string() },
1524 select! { Token::DoubleDash => "--".to_string() },
1525 select! { Token::ShortFlag(s) => format!("-{}", s) },
1526 select! { Token::LongFlag(s) => format!("--{}", s) },
1527 select! { Token::PlusFlag(s) => format!("+{}", s) },
1528 )),
1529 just(Token::LBracket)
1531 .ignore_then(
1532 choice((
1533 select! { Token::Ident(s) => s },
1534 select! { Token::Int(n) => n.to_string() },
1535 just(Token::Colon).to(":".to_string()),
1536 just(Token::Bang).to("!".to_string()),
1538 select! { Token::ShortFlag(s) => format!("-{}", s) },
1540 ))
1541 .repeated()
1542 .at_least(1)
1543 .collect::<Vec<String>>()
1544 )
1545 .then_ignore(just(Token::RBracket))
1546 .map(|parts| format!("[{}]", parts.join(""))),
1547 just(Token::LBrace)
1549 .ignore_then(
1550 choice((
1551 select! { Token::Ident(s) => s },
1552 select! { Token::Int(n) => n.to_string() },
1553 ))
1554 .separated_by(just(Token::Comma))
1555 .at_least(1)
1556 .collect::<Vec<String>>()
1557 )
1558 .then_ignore(just(Token::RBrace))
1559 .map(|parts| format!("{{{}}}", parts.join(","))),
1560 ));
1561
1562 let pattern = pattern_part
1565 .repeated()
1566 .at_least(1)
1567 .collect::<Vec<String>>()
1568 .map(|parts| parts.join(""))
1569 .labelled("case pattern");
1570
1571 let patterns = pattern
1573 .separated_by(just(Token::Pipe))
1574 .at_least(1)
1575 .collect::<Vec<String>>()
1576 .labelled("case patterns");
1577
1578 let branch = just(Token::LParen)
1580 .or_not()
1581 .ignore_then(just(Token::Newline).repeated())
1582 .ignore_then(patterns)
1583 .then_ignore(just(Token::RParen))
1584 .then_ignore(just(Token::Newline).repeated())
1585 .then(
1586 stmt.clone()
1587 .repeated()
1588 .collect::<Vec<_>>()
1589 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1590 )
1591 .then_ignore(just(Token::DoubleSemi))
1592 .then_ignore(just(Token::Newline).repeated())
1593 .map(|(patterns, body)| CaseBranch { patterns, body })
1594 .labelled("case branch");
1595
1596 just(Token::Case)
1597 .ignore_then(expr_parser())
1598 .then_ignore(just(Token::In))
1599 .then_ignore(just(Token::Newline).repeated())
1600 .then(branch.repeated().collect::<Vec<_>>())
1601 .then_ignore(just(Token::Esac))
1602 .map(|(expr, branches)| CaseStmt { expr, branches })
1603 .labelled("case statement")
1604 .boxed()
1605}
1606
1607fn pipeline_parser<'tokens, I>(
1609) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1610where
1611 I: ValueInput<'tokens, Token = Token, Span = Span>,
1612{
1613 command_parser()
1614 .separated_by(just(Token::Pipe))
1615 .at_least(1)
1616 .collect::<Vec<_>>()
1617 .then(just(Token::Amp).or_not())
1618 .map(|(commands, bg)| Pipeline {
1619 commands,
1620 background: bg.is_some(),
1621 })
1622 .labelled("pipeline")
1623 .boxed()
1624}
1625
1626fn command_parser<'tokens, I>(
1629) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1630where
1631 I: ValueInput<'tokens, Token = Token, Span = Span>,
1632{
1633 let command_name = choice((
1635 ident_parser(),
1636 path_parser(),
1637 select! { Token::DotSlashPath(s) => s },
1638 just(Token::True).to("true".to_string()),
1639 just(Token::False).to("false".to_string()),
1640 just(Token::Dot).to(".".to_string()),
1641 ));
1642
1643 command_name
1653 .then(args_list_parser())
1654 .then(redirect_parser(primary_expr_parser()).repeated().collect::<Vec<_>>())
1655 .map(|((name, args), redirects)| Command {
1656 name,
1657 args,
1658 redirects,
1659 })
1660 .labelled("command")
1661 .boxed()
1662}
1663
1664fn pipeline_into_stmt(p: Pipeline) -> Stmt {
1669 if p.commands.len() == 1 && !p.background && p.commands[0].redirects.is_empty() {
1670 match p.commands.into_iter().next() {
1671 Some(cmd) => Stmt::Command(cmd),
1672 None => Stmt::Empty, }
1674 } else {
1675 Stmt::Pipeline(p)
1676 }
1677}
1678
1679fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
1683 cmd.redirects
1684 .iter()
1685 .filter(|r| {
1686 matches!(
1687 r.kind,
1688 RedirectKind::Stdin | RedirectKind::HereDoc | RedirectKind::HereString
1689 )
1690 })
1691 .count()
1692 > 1
1693}
1694
1695fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
1699 stmts.iter().any(stmt_has_ambiguous_stdin)
1700}
1701
1702fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
1703 match stmt {
1704 Stmt::Command(c) => command_has_ambiguous_stdin(c),
1705 Stmt::Pipeline(p) => p.commands.iter().any(command_has_ambiguous_stdin),
1706 Stmt::If(i) => {
1707 first_ambiguous_stdin(&i.then_branch)
1708 || i.else_branch
1709 .as_deref()
1710 .is_some_and(first_ambiguous_stdin)
1711 }
1712 Stmt::For(f) => first_ambiguous_stdin(&f.body),
1713 Stmt::While(w) => first_ambiguous_stdin(&w.body),
1714 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
1715 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
1716 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
1717 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
1718 }
1719 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
1720 Stmt::Assignment(_)
1721 | Stmt::Break(_)
1722 | Stmt::Continue(_)
1723 | Stmt::Return(_)
1724 | Stmt::Exit(_)
1725 | Stmt::Test(_)
1726 | Stmt::Empty => false,
1727 }
1728}
1729
1730fn is_comma_literal_arg(arg: &Arg) -> bool {
1733 matches!(arg, Arg::Positional(Expr::Literal(Value::String(s))) if s == ",")
1734}
1735
1736fn is_glue_candidate(arg: &Arg) -> bool {
1757 matches!(arg, Arg::Positional(_) | Arg::LongFlag(_))
1758}
1759
1760fn reject_glued_args<'src>(
1770 args: Vec<(Arg, Span)>,
1771) -> Result<Vec<Arg>, Rich<'src, Token, Span>> {
1772 for pair in args.windows(2) {
1773 let (prev, prev_span) = &pair[0];
1774 let (next, next_span) = &pair[1];
1775 if is_glue_candidate(prev) && is_glue_candidate(next) && prev_span.end == next_span.start {
1776 let msg = if is_comma_literal_arg(prev) || is_comma_literal_arg(next) {
1781 "an unquoted comma splits this into separate words — kaish reserves \
1782 `,` (brace expansion, lists); quote a comma-bearing argument to keep \
1783 it one word, e.g. cut -f \"1,3\", sort -k \"2,2n\", or echo \"a,b\""
1784 } else {
1785 "adjacent words with no space between them are not joined into one \
1786 argument (kaish does no token pasting); quote the whole word, e.g. \
1787 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\""
1788 };
1789 return Err(Rich::custom(*next_span, msg));
1790 }
1791 }
1792 Ok(args.into_iter().map(|(arg, _)| arg).collect())
1793}
1794
1795fn args_list_parser<'tokens, I>(
1799) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1800where
1801 I: ValueInput<'tokens, Token = Token, Span = Span>,
1802{
1803 let pre_dash = arg_before_double_dash_parser()
1812 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1813 .repeated()
1814 .collect::<Vec<(Arg, Span)>>()
1815 .try_map(|args, _span| reject_glued_args(args));
1816
1817 let double_dash = select! {
1819 Token::DoubleDash => Arg::DoubleDash,
1820 };
1821
1822 let post_dash_arg = choice((
1824 select! {
1826 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
1827 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
1828 },
1829 word_assign_arg_parser(),
1836 test_operator_arg_parser(),
1838 primary_expr_parser().map(Arg::Positional),
1840 ));
1841
1842 let post_dash = post_dash_arg
1846 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1847 .repeated()
1848 .collect::<Vec<(Arg, Span)>>()
1849 .try_map(|args, _span| reject_glued_args(args));
1850
1851 pre_dash
1853 .then(double_dash.then(post_dash).or_not())
1854 .map(|(mut args, maybe_dd)| {
1855 if let Some((dd, post)) = maybe_dd {
1856 args.push(dd);
1857 args.extend(post);
1858 }
1859 args
1860 })
1861}
1862
1863fn keyword_word<'tokens, I>(
1872) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1873where
1874 I: ValueInput<'tokens, Token = Token, Span = Span>,
1875{
1876 select! {
1877 Token::Set => "set",
1878 Token::Local => "local",
1879 Token::If => "if",
1880 Token::Then => "then",
1881 Token::Else => "else",
1882 Token::Elif => "elif",
1883 Token::Fi => "fi",
1884 Token::For => "for",
1885 Token::While => "while",
1886 Token::In => "in",
1887 Token::Do => "do",
1888 Token::Done => "done",
1889 Token::Case => "case",
1890 Token::Esac => "esac",
1891 Token::Function => "function",
1892 Token::Break => "break",
1893 Token::Continue => "continue",
1894 Token::Return => "return",
1895 Token::Exit => "exit",
1896 }
1897 .map(|s| s.to_string())
1898}
1899
1900fn word_assign_arg_parser<'tokens, I>(
1910) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1911where
1912 I: ValueInput<'tokens, Token = Token, Span = Span>,
1913{
1914 choice((
1915 select! { Token::Ident(s) => s },
1916 keyword_word(),
1917 ))
1918 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1919 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1920 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1921 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1922 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1924 Err(Rich::custom(
1925 span,
1926 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
1927 ))
1928 } else {
1929 Ok(Arg::WordAssign { key, value })
1930 }
1931 })
1932}
1933
1934fn test_operator_arg_parser<'tokens, I>(
1951) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1952where
1953 I: ValueInput<'tokens, Token = Token, Span = Span>,
1954{
1955 select! {
1956 Token::Eq => "=",
1957 Token::EqEq => "==",
1958 Token::NotEq => "!=",
1959 Token::Bang => "!",
1960 }
1961 .map(|s| Arg::Positional(Expr::Literal(Value::String(s.to_string()))))
1962}
1963
1964fn arg_before_double_dash_parser<'tokens, I>(
1966) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1967where
1968 I: ValueInput<'tokens, Token = Token, Span = Span>,
1969{
1970 let long_flag_with_value = select! {
1972 Token::LongFlag(name) => name,
1973 }
1974 .then_ignore(just(Token::Eq))
1975 .then(primary_expr_parser())
1976 .map(|(key, value)| Arg::Named { key, value });
1977
1978 let long_flag = select! {
1980 Token::LongFlag(name) => Arg::LongFlag(name),
1981 };
1982
1983 let short_flag = select! {
1985 Token::ShortFlag(name) => Arg::ShortFlag(name),
1986 };
1987
1988 let named = word_assign_arg_parser();
1990
1991 let positional = primary_expr_parser().map(Arg::Positional);
1993
1994 let test_operator = test_operator_arg_parser();
1998
1999 choice((
2002 long_flag_with_value,
2003 long_flag,
2004 short_flag,
2005 named,
2006 test_operator,
2007 positional,
2008 ))
2009 .boxed()
2010}
2011
2012fn redirect_parser<'tokens, I, T>(
2022 target: T,
2023) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2024where
2025 I: ValueInput<'tokens, Token = Token, Span = Span>,
2026 T: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2027{
2028 let target = target
2041 .clone()
2042 .map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) })
2043 .then(target.clone().map_with(|_, e| e.span()).rewind().or_not())
2044 .try_map(|((expr, span), glued), _| match glued {
2045 Some(next_span) if next_span.start == span.end => Err(Rich::custom(
2046 next_span,
2047 "adjacent words with no space between them are not joined into the redirect \
2048 target (kaish does no token pasting); quote the whole target, e.g. \
2049 \"/tmp/$(echo x).txt\"",
2050 )),
2051 _ => Ok(expr),
2052 })
2053 .boxed();
2054
2055 let regular_redirect = select! {
2057 Token::GtGt => RedirectKind::StdoutAppend,
2058 Token::Gt => RedirectKind::StdoutOverwrite,
2059 Token::Lt => RedirectKind::Stdin,
2060 Token::Stderr => RedirectKind::Stderr,
2061 Token::Both => RedirectKind::Both,
2062 }
2063 .then(target.clone())
2064 .map(|(kind, target)| Redirect { kind, target });
2065
2066 let heredoc_redirect = just(Token::HereDocStart)
2074 .ignore_then(select! { Token::HereDoc(data) => data })
2075 .map(|data: HereDocData| {
2076 let target = if data.literal {
2077 let body = if data.strip_tabs {
2078 crate::interpreter::strip_leading_tabs(&data.content)
2079 } else {
2080 data.content
2081 };
2082 Expr::Literal(Value::String(body))
2083 } else {
2084 let parts = parse_interpolated_string_spanned(
2085 &data.content,
2086 data.body_start_offset,
2087 );
2088 if parts.len() == 1 && !data.strip_tabs {
2092 if let StringPart::Literal(text) = &parts[0].part {
2093 return Redirect {
2094 kind: RedirectKind::HereDoc,
2095 target: Expr::Literal(Value::String(text.clone())),
2096 };
2097 }
2098 }
2099 Expr::HereDocBody {
2100 parts,
2101 strip_tabs: data.strip_tabs,
2102 }
2103 };
2104 Redirect {
2105 kind: RedirectKind::HereDoc,
2106 target,
2107 }
2108 });
2109
2110 let herestring_redirect = just(Token::HereString)
2114 .ignore_then(target.clone())
2115 .map(|target| Redirect {
2116 kind: RedirectKind::HereString,
2117 target,
2118 });
2119
2120 let merge_stderr_redirect = just(Token::StderrToStdout)
2122 .map(|_| Redirect {
2123 kind: RedirectKind::MergeStderr,
2124 target: Expr::Literal(Value::Null),
2126 });
2127
2128 let merge_stdout_redirect = choice((
2130 just(Token::StdoutToStderr),
2131 just(Token::StdoutToStderr2),
2132 ))
2133 .map(|_| Redirect {
2134 kind: RedirectKind::MergeStdout,
2135 target: Expr::Literal(Value::Null),
2137 });
2138
2139 choice((
2140 heredoc_redirect,
2141 herestring_redirect,
2142 merge_stderr_redirect,
2143 merge_stdout_redirect,
2144 regular_redirect,
2145 ))
2146 .labelled("redirect")
2147 .boxed()
2148}
2149
2150fn test_expr_stmt_parser<'tokens, I>(
2162) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2163where
2164 I: ValueInput<'tokens, Token = Token, Span = Span>,
2165{
2166 let file_test_op = select! {
2168 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
2169 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
2170 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
2171 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
2172 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
2173 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
2174 };
2175
2176 let string_test_op = select! {
2180 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
2181 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
2182 Token::ShortFlag(s) if s == "list" => StringTestOp::IsList,
2183 Token::ShortFlag(s) if s == "record" => StringTestOp::IsRecord,
2184 };
2185
2186 let cmp_op = choice((
2189 just(Token::EqEq).to(TestCmpOp::Eq),
2190 just(Token::Eq).to(TestCmpOp::Eq),
2191 just(Token::NotEq).to(TestCmpOp::NotEq),
2192 just(Token::Match).to(TestCmpOp::Match),
2193 just(Token::NotMatch).to(TestCmpOp::NotMatch),
2194 just(Token::Gt).to(TestCmpOp::Gt),
2195 just(Token::Lt).to(TestCmpOp::Lt),
2196 just(Token::GtEq).to(TestCmpOp::GtEq),
2197 just(Token::LtEq).to(TestCmpOp::LtEq),
2198 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
2199 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
2200 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
2201 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
2202 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
2203 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
2204 ));
2205
2206 let file_test = file_test_op
2208 .then(primary_expr_parser())
2209 .map(|(op, path)| TestExpr::FileTest {
2210 op,
2211 path: Box::new(path),
2212 });
2213
2214 let string_test = string_test_op
2216 .then(primary_expr_parser())
2217 .map(|(op, value)| TestExpr::StringTest {
2218 op,
2219 value: Box::new(value),
2220 });
2221
2222 let comparison = primary_expr_parser()
2224 .then(cmp_op)
2225 .then(primary_expr_parser())
2226 .map(|((left, op), right)| TestExpr::Comparison {
2227 left: Box::new(left),
2228 op,
2229 right: Box::new(right),
2230 });
2231
2232 let not_in = primary_expr_parser()
2239 .then_ignore(select! { Token::Ident(s) if s == "not" => () })
2240 .then_ignore(just(Token::In))
2241 .then(value_primary_parser())
2242 .map(|(left, right)| TestExpr::NotIn {
2243 left: Box::new(left),
2244 right: Box::new(right),
2245 });
2246
2247 let in_ = primary_expr_parser()
2248 .then_ignore(just(Token::In))
2249 .then(value_primary_parser())
2250 .map(|(left, right)| TestExpr::In {
2251 left: Box::new(left),
2252 right: Box::new(right),
2253 });
2254
2255 let primary_test = choice((file_test, string_test, not_in, in_, comparison));
2257
2258 let unary = recursive(|unary| {
2272 let not_expr = just(Token::Bang)
2273 .ignore_then(unary)
2274 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
2275 choice((not_expr, primary_test.clone()))
2276 });
2277
2278 let and_expr = unary.clone().foldl(
2280 just(Token::And).ignore_then(unary).repeated(),
2281 |left, right| TestExpr::And {
2282 left: Box::new(left),
2283 right: Box::new(right),
2284 },
2285 );
2286
2287 let compound_test = and_expr.clone().foldl(
2289 just(Token::Or).ignore_then(and_expr).repeated(),
2290 |left, right| TestExpr::Or {
2291 left: Box::new(left),
2292 right: Box::new(right),
2293 },
2294 );
2295
2296 just(Token::LBracket)
2299 .then(just(Token::LBracket))
2300 .ignore_then(compound_test)
2301 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
2302 .labelled("test expression")
2303 .boxed()
2304}
2305
2306fn condition_parser<'tokens, I>(
2321) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2322where
2323 I: ValueInput<'tokens, Token = Token, Span = Span>,
2324{
2325 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
2327
2328 let command_condition = command_parser().map(Expr::Command);
2331
2332 let base = choice((test_expr_condition, command_condition));
2334
2335 let and_expr = base.clone().foldl(
2338 just(Token::And).ignore_then(base).repeated(),
2339 |left, right| Expr::BinaryOp {
2340 left: Box::new(left),
2341 op: BinaryOp::And,
2342 right: Box::new(right),
2343 },
2344 );
2345
2346 and_expr
2348 .clone()
2349 .foldl(
2350 just(Token::Or).ignore_then(and_expr).repeated(),
2351 |left, right| Expr::BinaryOp {
2352 left: Box::new(left),
2353 op: BinaryOp::Or,
2354 right: Box::new(right),
2355 },
2356 )
2357 .labelled("condition")
2358 .boxed()
2359}
2360
2361fn expr_parser<'tokens, I>(
2368) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2369where
2370 I: ValueInput<'tokens, Token = Token, Span = Span>,
2371{
2372 primary_expr_parser()
2374}
2375
2376fn value_expr_parser<'tokens, I>(
2381) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2382where
2383 I: ValueInput<'tokens, Token = Token, Span = Span>,
2384{
2385 value_literal_parser()
2386}
2387
2388fn value_primary_parser<'tokens, I>(
2394) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2395where
2396 I: ValueInput<'tokens, Token = Token, Span = Span>,
2397{
2398 value_literal_parser()
2399}
2400
2401fn value_literal_parser<'tokens, I>(
2413) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2414where
2415 I: ValueInput<'tokens, Token = Token, Span = Span>,
2416{
2417 recursive(|value| {
2418 choice((
2419 list_literal_parser(value.clone()),
2420 record_literal_parser(value.clone()),
2421 primary_expr_parser(),
2422 ))
2423 })
2424 .boxed()
2425}
2426
2427fn list_literal_parser<'tokens, I, V>(
2435 value: V,
2436) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2437where
2438 I: ValueInput<'tokens, Token = Token, Span = Span>,
2439 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2440{
2441 let spread_elem = just(Token::DotDotDot)
2442 .ignore_then(value.clone())
2443 .map(ListElem::Spread);
2444 let item_elem = value.map(ListElem::Item);
2445 let elem = choice((spread_elem, item_elem));
2446
2447 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2448
2449 just(Token::LBracket)
2450 .ignore_then(just(Token::Newline).repeated())
2451 .ignore_then(elem.then_ignore(sep).repeated().collect::<Vec<_>>())
2452 .then_ignore(just(Token::RBracket))
2453 .map(Expr::ListLiteral)
2454 .labelled("list literal")
2455}
2456
2457fn record_literal_parser<'tokens, I, V>(
2465 value: V,
2466) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2467where
2468 I: ValueInput<'tokens, Token = Token, Span = Span>,
2469 V: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2470{
2471 let bare_key = select! { Token::Ident(s) => RecordKey::Bare(s) };
2472 let double_key = select! { Token::String(s) => s }.try_map(|s, span| {
2478 let parts = parse_interpolated_string(&s)
2479 .map_err(|e| Rich::custom(span, format!("record key: {e}")))?;
2480 Ok(match parts.as_slice() {
2481 [] => RecordKey::Quoted(String::new()),
2482 [StringPart::Literal(lit)] => RecordKey::Quoted(lit.clone()),
2483 _ => RecordKey::Interpolated(parts),
2484 })
2485 });
2486 let single_key = select! { Token::SingleString(s) => RecordKey::Quoted(s) };
2487 let key = choice((double_key, single_key, bare_key)).labelled("record key");
2488
2489 let stray_bareword_after_value = select! { Token::Ident(s) => s }
2501 .then(just(Token::Colon).or_not())
2502 .rewind()
2503 .or_not()
2504 .try_map(|maybe, span| match maybe {
2505 Some((word, None)) => Err(Rich::custom(
2506 span,
2507 format!(
2508 "record value: unexpected word \"{word}\" after the value — a multi-word \
2509 value must be quoted, e.g. {{key: \"hello world\"}}"
2510 ),
2511 )),
2512 _ => Ok(()),
2513 });
2514
2515 let entry = key
2516 .then_ignore(just(Token::Colon))
2517 .then(value)
2518 .then_ignore(stray_bareword_after_value)
2519 .map(|(key, value)| RecordEntry { key, value });
2520
2521 let sep = choice((just(Token::Comma).to(()), just(Token::Newline).to(()))).repeated();
2522
2523 just(Token::LBrace)
2524 .ignore_then(just(Token::Newline).repeated())
2525 .ignore_then(entry.then_ignore(sep).repeated().collect::<Vec<_>>())
2526 .then_ignore(just(Token::RBrace))
2527 .map(Expr::RecordLiteral)
2528 .labelled("record literal")
2529}
2530
2531fn primary_expr_parser<'tokens, I>(
2535) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2536where
2537 I: ValueInput<'tokens, Token = Token, Span = Span>,
2538{
2539 let positional = select! {
2541 Token::Positional(n) => Expr::Positional(n),
2542 Token::AllArgs => Expr::AllArgs,
2543 Token::ArgCount => Expr::ArgCount,
2544 Token::VarLength(name) => Expr::VarLength(parse_varpath(&format!("${{{name}}}"))),
2545 Token::LastExitCode => Expr::LastExitCode,
2546 Token::CurrentPid => Expr::CurrentPid,
2547 };
2548
2549 let arithmetic = select! {
2551 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
2552 };
2553
2554 let keyword_as_bareword = select! {
2557 Token::Done => "done",
2558 Token::Fi => "fi",
2559 Token::Then => "then",
2560 Token::Else => "else",
2561 Token::Elif => "elif",
2562 Token::In => "in",
2563 Token::Do => "do",
2564 Token::Esac => "esac",
2565 Token::Set => "set",
2570 }
2571 .map(|s| Expr::Literal(Value::String(s.to_string())));
2572
2573 let plus_minus_bare = select! {
2577 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
2578 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
2579 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
2580 Token::DoubleDashBare(s) => Expr::Literal(Value::String(s)),
2581 };
2582
2583 let glob_pattern = select! {
2585 Token::GlobWord(s) => Expr::GlobPattern(s),
2586 Token::Star => Expr::GlobPattern("*".to_string()),
2587 Token::Question => Expr::GlobPattern("?".to_string()),
2588 };
2589
2590 recursive(|expr| {
2591 choice((
2592 positional,
2593 arithmetic,
2594 cmd_subst_parser(expr.clone()),
2595 var_expr_parser(),
2596 interpolated_string_parser(),
2597 literal_parser().map(Expr::Literal),
2598 glob_pattern,
2600 ident_parser().map(|s| Expr::Literal(Value::String(s))),
2602 path_parser().map(|s| Expr::Literal(Value::String(s))),
2604 select! {
2607 Token::Dot => Expr::Literal(Value::String(".".into())),
2613 Token::DotDot => Expr::Literal(Value::String("..".into())),
2614 Token::Comma => Expr::Literal(Value::String(",".into())),
2621 Token::Colon => Expr::Literal(Value::String(":".into())),
2628 Token::Tilde => Expr::Literal(Value::String("~".into())),
2629 Token::TildePath(s) => Expr::Literal(Value::String(s)),
2630 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
2631 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
2632 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
2634 Token::DashNumWord(s) => Expr::Literal(Value::String(s)),
2637 Token::AtWord(s) => Expr::Literal(Value::String(s)),
2639 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
2644 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
2647 },
2648 plus_minus_bare,
2649 keyword_as_bareword,
2651 ))
2652 .labelled("expression")
2653 })
2654 .boxed()
2655}
2656
2657fn var_expr_parser<'tokens, I>(
2660) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2661where
2662 I: ValueInput<'tokens, Token = Token, Span = Span>,
2663{
2664 select! {
2665 Token::VarRef(raw) => parse_var_expr(&raw),
2666 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
2667 }
2668 .labelled("variable reference")
2669}
2670
2671fn cmd_subst_parser<'tokens, I, E>(
2675 expr: E,
2676) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2677where
2678 I: ValueInput<'tokens, Token = Token, Span = Span>,
2679 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
2680{
2681 let long_flag_with_value = select! {
2684 Token::LongFlag(name) => name,
2685 }
2686 .then_ignore(just(Token::Eq))
2687 .then(expr.clone())
2688 .map(|(key, value)| Arg::Named { key, value });
2689
2690 let long_flag = select! {
2692 Token::LongFlag(name) => Arg::LongFlag(name),
2693 };
2694
2695 let short_flag = select! {
2697 Token::ShortFlag(name) => Arg::ShortFlag(name),
2698 };
2699
2700 let named = choice((ident_parser(), keyword_word()))
2703 .then_ignore(just(Token::Eq))
2704 .then(expr.clone())
2705 .map(|(key, value)| Arg::WordAssign { key, value });
2706
2707 let positional = expr.clone().map(Arg::Positional);
2709
2710 let arg = choice((
2711 long_flag_with_value,
2712 long_flag,
2713 short_flag,
2714 named,
2715 positional,
2716 ));
2717
2718 let command_name = choice((
2720 ident_parser(),
2721 just(Token::True).to("true".to_string()),
2722 just(Token::False).to("false".to_string()),
2723 ));
2724
2725 let command = command_name
2732 .then(arg.repeated().collect::<Vec<_>>())
2733 .then(
2734 redirect_parser(expr.clone())
2735 .repeated()
2736 .collect::<Vec<_>>(),
2737 )
2738 .map(|((name, args), redirects)| Command {
2739 name,
2740 args,
2741 redirects,
2742 });
2743
2744 let pipeline = command
2746 .separated_by(just(Token::Pipe))
2747 .at_least(1)
2748 .collect::<Vec<_>>()
2749 .map(|commands| Pipeline {
2750 commands,
2751 background: false,
2752 });
2753
2754 let pipeline_stmt = pipeline.map(pipeline_into_stmt);
2757
2758 let chained = pipeline_stmt.clone().foldl(
2766 choice((
2767 just(Token::And).to(true), just(Token::Or).to(false), ))
2770 .then(pipeline_stmt.clone())
2771 .repeated(),
2772 |left, (is_and, right): (bool, Stmt)| {
2773 if is_and {
2774 Stmt::AndChain {
2775 left: Box::new(left),
2776 right: Box::new(right),
2777 }
2778 } else {
2779 Stmt::OrChain {
2780 left: Box::new(left),
2781 right: Box::new(right),
2782 }
2783 }
2784 },
2785 );
2786
2787 let separator = choice((just(Token::Newline), just(Token::Semi)));
2792 let body = separator
2793 .clone()
2794 .repeated()
2795 .ignore_then(
2796 chained
2797 .separated_by(separator.clone().repeated().at_least(1))
2798 .allow_trailing()
2799 .collect::<Vec<_>>(),
2800 )
2801 .then_ignore(separator.repeated());
2802
2803 just(Token::CmdSubstStart)
2804 .ignore_then(body)
2805 .then_ignore(just(Token::RParen))
2806 .map(Expr::CommandSubst)
2807 .labelled("command substitution")
2808}
2809
2810fn interpolated_string_parser<'tokens, I>(
2812) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2813where
2814 I: ValueInput<'tokens, Token = Token, Span = Span>,
2815{
2816 let double_quoted = select! {
2818 Token::String(s) => s,
2819 }
2820 .try_map(|s, span| {
2821 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
2823 let parts = parse_interpolated_string(&s)
2826 .map_err(|msg| Rich::custom(span, msg))?;
2827 if parts.len() == 1
2828 && let StringPart::Literal(text) = &parts[0] {
2829 return Ok(Expr::Literal(Value::String(text.clone())));
2830 }
2831 Ok(Expr::Interpolated(parts))
2832 } else {
2833 Ok(Expr::Literal(Value::String(s)))
2834 }
2835 });
2836
2837 let single_quoted = select! {
2839 Token::SingleString(s) => Expr::Literal(Value::String(s)),
2840 };
2841
2842 choice((single_quoted, double_quoted)).labelled("string")
2843}
2844
2845fn literal_parser<'tokens, I>(
2847) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2848where
2849 I: ValueInput<'tokens, Token = Token, Span = Span>,
2850{
2851 choice((
2852 select! {
2853 Token::True => Value::Bool(true),
2854 Token::False => Value::Bool(false),
2855 },
2856 select! {
2857 Token::Int(n) => Value::Int(n),
2858 Token::Float(f) => Value::Float(f),
2859 },
2860 ))
2861 .labelled("literal")
2862 .boxed()
2863}
2864
2865fn ident_parser<'tokens, I>(
2867) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2868where
2869 I: ValueInput<'tokens, Token = Token, Span = Span>,
2870{
2871 select! {
2872 Token::Ident(s) => s,
2873 }
2874 .labelled("identifier")
2875}
2876
2877fn path_parser<'tokens, I>(
2879) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2880where
2881 I: ValueInput<'tokens, Token = Token, Span = Span>,
2882{
2883 select! {
2884 Token::Path(s) => s,
2885 }
2886 .labelled("path")
2887}
2888
2889#[cfg(test)]
2890#[allow(clippy::approx_constant)]
2891mod tests {
2892 use super::*;
2893
2894 fn subst_cmd(expr: &Expr) -> &Command {
2896 match expr {
2897 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2898 [Stmt::Command(cmd)] => cmd,
2899 other => panic!("expected a single command in $(), got {other:?}"),
2900 },
2901 other => panic!("expected command subst, got {other:?}"),
2902 }
2903 }
2904
2905 fn subst_pipeline(expr: &Expr) -> &Pipeline {
2907 match expr {
2908 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2909 [Stmt::Pipeline(p)] => p,
2910 other => panic!("expected a single pipeline in $(), got {other:?}"),
2911 },
2912 other => panic!("expected command subst, got {other:?}"),
2913 }
2914 }
2915
2916 #[test]
2917 fn parse_empty() {
2918 let result = parse("");
2919 assert!(result.is_ok());
2920 assert_eq!(result.expect("ok").statements.len(), 0);
2921 }
2922
2923 #[test]
2924 fn parse_newlines_only() {
2925 let result = parse("\n\n\n");
2926 assert!(result.is_ok());
2927 }
2928
2929 #[test]
2930 fn parse_simple_command() {
2931 let result = parse("echo");
2932 assert!(result.is_ok());
2933 let program = result.expect("ok");
2934 assert_eq!(program.statements.len(), 1);
2935 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2936 }
2937
2938 #[test]
2939 fn parse_command_with_string_arg() {
2940 let result = parse(r#"echo "hello""#);
2941 assert!(result.is_ok());
2942 let program = result.expect("ok");
2943 match &program.statements[0] {
2944 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
2945 _ => panic!("expected Command"),
2946 }
2947 }
2948
2949 #[test]
2950 fn parse_assignment() {
2951 let result = parse("X=5");
2952 assert!(result.is_ok());
2953 let program = result.expect("ok");
2954 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2955 }
2956
2957 #[test]
2958 fn parse_pipeline() {
2959 let result = parse("a | b | c");
2960 assert!(result.is_ok());
2961 let program = result.expect("ok");
2962 match &program.statements[0] {
2963 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2964 _ => panic!("expected Pipeline"),
2965 }
2966 }
2967
2968 #[test]
2969 fn parse_background_job() {
2970 let result = parse("cmd &");
2971 assert!(result.is_ok());
2972 let program = result.expect("ok");
2973 match &program.statements[0] {
2974 Stmt::Pipeline(p) => assert!(p.background),
2975 _ => panic!("expected Pipeline with background"),
2976 }
2977 }
2978
2979 #[test]
2980 fn parse_if_simple() {
2981 let result = parse("if true; then echo; fi");
2982 assert!(result.is_ok());
2983 let program = result.expect("ok");
2984 assert!(matches!(&program.statements[0], Stmt::If(_)));
2985 }
2986
2987 #[test]
2988 fn parse_if_else() {
2989 let result = parse("if true; then echo; else echo; fi");
2990 assert!(result.is_ok());
2991 let program = result.expect("ok");
2992 match &program.statements[0] {
2993 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
2994 _ => panic!("expected If"),
2995 }
2996 }
2997
2998 #[test]
2999 fn parse_elif_simple() {
3000 let result = parse("if true; then echo a; elif false; then echo b; fi");
3001 assert!(result.is_ok(), "parse failed: {:?}", result);
3002 let program = result.expect("ok");
3003 match &program.statements[0] {
3004 Stmt::If(if_stmt) => {
3005 assert!(if_stmt.else_branch.is_some());
3007 let else_branch = if_stmt.else_branch.as_ref().unwrap();
3008 assert_eq!(else_branch.len(), 1);
3009 assert!(matches!(&else_branch[0], Stmt::If(_)));
3010 }
3011 _ => panic!("expected If"),
3012 }
3013 }
3014
3015 #[test]
3016 fn parse_elif_with_else() {
3017 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
3018 assert!(result.is_ok(), "parse failed: {:?}", result);
3019 let program = result.expect("ok");
3020 match &program.statements[0] {
3021 Stmt::If(outer_if) => {
3022 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
3024 assert_eq!(else_branch.len(), 1);
3025 match &else_branch[0] {
3026 Stmt::If(inner_if) => {
3027 assert!(inner_if.else_branch.is_some());
3029 }
3030 _ => panic!("expected nested If from elif"),
3031 }
3032 }
3033 _ => panic!("expected If"),
3034 }
3035 }
3036
3037 #[test]
3038 fn parse_multiple_elif() {
3039 let result = parse(
3041 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
3042 );
3043 assert!(result.is_ok(), "parse failed: {:?}", result);
3044 }
3045
3046 #[test]
3047 fn parse_for_loop() {
3048 let result = parse("for X in items; do echo; done");
3049 assert!(result.is_ok());
3050 let program = result.expect("ok");
3051 assert!(matches!(&program.statements[0], Stmt::For(_)));
3052 }
3053
3054 #[test]
3055 fn parse_brackets_not_array_literal() {
3056 let result = parse("cmd [1");
3058 let _ = result;
3061 }
3062
3063 #[test]
3064 fn parse_named_arg() {
3065 let result = parse("cmd foo=5");
3069 assert!(result.is_ok());
3070 let program = result.expect("ok");
3071 match &program.statements[0] {
3072 Stmt::Command(cmd) => {
3073 assert_eq!(cmd.args.len(), 1);
3074 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
3075 }
3076 _ => panic!("expected Command"),
3077 }
3078 }
3079
3080 #[test]
3081 fn parse_short_flag() {
3082 let result = parse("ls -l");
3083 assert!(result.is_ok());
3084 let program = result.expect("ok");
3085 match &program.statements[0] {
3086 Stmt::Command(cmd) => {
3087 assert_eq!(cmd.name, "ls");
3088 assert_eq!(cmd.args.len(), 1);
3089 match &cmd.args[0] {
3090 Arg::ShortFlag(name) => assert_eq!(name, "l"),
3091 _ => panic!("expected ShortFlag"),
3092 }
3093 }
3094 _ => panic!("expected Command"),
3095 }
3096 }
3097
3098 #[test]
3099 fn parse_long_flag() {
3100 let result = parse("git push --force");
3101 assert!(result.is_ok());
3102 let program = result.expect("ok");
3103 match &program.statements[0] {
3104 Stmt::Command(cmd) => {
3105 assert_eq!(cmd.name, "git");
3106 assert_eq!(cmd.args.len(), 2);
3107 match &cmd.args[0] {
3108 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
3109 _ => panic!("expected Positional push"),
3110 }
3111 match &cmd.args[1] {
3112 Arg::LongFlag(name) => assert_eq!(name, "force"),
3113 _ => panic!("expected LongFlag"),
3114 }
3115 }
3116 _ => panic!("expected Command"),
3117 }
3118 }
3119
3120 #[test]
3121 fn parse_long_flag_with_value() {
3122 let result = parse(r#"git commit --message="hello""#);
3123 assert!(result.is_ok());
3124 let program = result.expect("ok");
3125 match &program.statements[0] {
3126 Stmt::Command(cmd) => {
3127 assert_eq!(cmd.name, "git");
3128 assert_eq!(cmd.args.len(), 2);
3129 match &cmd.args[1] {
3130 Arg::Named { key, value } => {
3131 assert_eq!(key, "message");
3132 match value {
3133 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
3134 _ => panic!("expected String value"),
3135 }
3136 }
3137 _ => panic!("expected Named from --flag=value"),
3138 }
3139 }
3140 _ => panic!("expected Command"),
3141 }
3142 }
3143
3144 #[test]
3145 fn parse_mixed_flags_and_args() {
3146 let result = parse(r#"git commit -m "message" --amend"#);
3147 assert!(result.is_ok());
3148 let program = result.expect("ok");
3149 match &program.statements[0] {
3150 Stmt::Command(cmd) => {
3151 assert_eq!(cmd.name, "git");
3152 assert_eq!(cmd.args.len(), 4);
3153 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
3155 match &cmd.args[1] {
3157 Arg::ShortFlag(name) => assert_eq!(name, "m"),
3158 _ => panic!("expected ShortFlag -m"),
3159 }
3160 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
3162 match &cmd.args[3] {
3164 Arg::LongFlag(name) => assert_eq!(name, "amend"),
3165 _ => panic!("expected LongFlag --amend"),
3166 }
3167 }
3168 _ => panic!("expected Command"),
3169 }
3170 }
3171
3172 #[test]
3173 fn parse_redirect_stdout() {
3174 let result = parse("cmd > file");
3175 assert!(result.is_ok());
3176 let program = result.expect("ok");
3177 match &program.statements[0] {
3179 Stmt::Pipeline(p) => {
3180 assert_eq!(p.commands.len(), 1);
3181 let cmd = &p.commands[0];
3182 assert_eq!(cmd.redirects.len(), 1);
3183 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
3184 }
3185 _ => panic!("expected Pipeline"),
3186 }
3187 }
3188
3189 #[test]
3190 fn parse_var_ref() {
3191 let result = parse("echo ${VAR}");
3192 assert!(result.is_ok());
3193 let program = result.expect("ok");
3194 match &program.statements[0] {
3195 Stmt::Command(cmd) => {
3196 assert_eq!(cmd.args.len(), 1);
3197 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
3198 }
3199 _ => panic!("expected Command"),
3200 }
3201 }
3202
3203 #[test]
3204 fn parse_multiple_statements() {
3205 let result = parse("a\nb\nc");
3206 assert!(result.is_ok());
3207 let program = result.expect("ok");
3208 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3209 assert_eq!(non_empty.len(), 3);
3210 }
3211
3212 #[test]
3213 fn parse_semicolon_separated() {
3214 let result = parse("a; b; c");
3215 assert!(result.is_ok());
3216 let program = result.expect("ok");
3217 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
3218 assert_eq!(non_empty.len(), 3);
3219 }
3220
3221 #[test]
3222 fn parse_complex_pipeline() {
3223 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
3224 assert!(result.is_ok());
3225 let program = result.expect("ok");
3226 match &program.statements[0] {
3227 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
3228 _ => panic!("expected Pipeline"),
3229 }
3230 }
3231
3232 #[test]
3233 fn parse_json_as_string_arg() {
3234 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
3236 assert!(result.is_ok());
3237 }
3238
3239 #[test]
3240 fn parse_mixed_args() {
3241 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
3242 assert!(result.is_ok());
3243 let program = result.expect("ok");
3244 match &program.statements[0] {
3245 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
3246 _ => panic!("expected Command"),
3247 }
3248 }
3249
3250 #[test]
3251 fn error_unterminated_string() {
3252 let result = parse(r#"echo "hello"#);
3253 assert!(result.is_err());
3254 }
3255
3256 #[test]
3257 fn error_unterminated_var_ref() {
3258 let result = parse("echo ${VAR");
3259 assert!(result.is_err());
3260 }
3261
3262 #[test]
3263 fn error_missing_fi() {
3264 let result = parse("if true; then echo");
3265 assert!(result.is_err());
3266 }
3267
3268 #[test]
3269 fn error_missing_done() {
3270 let result = parse("for X in items; do echo");
3271 assert!(result.is_err());
3272 }
3273
3274 #[test]
3275 fn parse_lvalue_single_index() {
3276 let result = parse("xs[0]=9").unwrap();
3277 match &result.statements[0] {
3278 Stmt::Assignment(a) => {
3279 assert_eq!(a.name(), "xs");
3280 assert_eq!(
3281 a.path.segments,
3282 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
3283 );
3284 assert!(!a.local);
3285 }
3286 other => panic!("expected assignment, got {:?}", other),
3287 }
3288 }
3289
3290 #[test]
3291 fn parse_lvalue_negative_index() {
3292 let result = parse("xs[-1]=7").unwrap();
3293 match &result.statements[0] {
3294 Stmt::Assignment(a) => assert_eq!(
3295 a.path.segments,
3296 vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)]
3297 ),
3298 other => panic!("expected assignment, got {:?}", other),
3299 }
3300 }
3301
3302 #[test]
3303 fn parse_lvalue_bareword_key() {
3304 let result = parse("user[email]=x").unwrap();
3305 match &result.statements[0] {
3306 Stmt::Assignment(a) => assert_eq!(
3307 a.path.segments,
3308 vec![
3309 VarSegment::Field("user".into()),
3310 VarSegment::Key("email".into())
3311 ]
3312 ),
3313 other => panic!("expected assignment, got {:?}", other),
3314 }
3315 }
3316
3317 #[test]
3318 fn parse_lvalue_chained_keys() {
3319 let result = parse("s[web][port]=9000").unwrap();
3320 match &result.statements[0] {
3321 Stmt::Assignment(a) => assert_eq!(
3322 a.path.segments,
3323 vec![
3324 VarSegment::Field("s".into()),
3325 VarSegment::Key("web".into()),
3326 VarSegment::Key("port".into())
3327 ]
3328 ),
3329 other => panic!("expected assignment, got {:?}", other),
3330 }
3331 }
3332
3333 #[test]
3334 fn parse_lvalue_dynamic_key() {
3335 let result = parse("r[$k]=v").unwrap();
3336 match &result.statements[0] {
3337 Stmt::Assignment(a) => assert_eq!(
3338 a.path.segments,
3339 vec![
3340 VarSegment::Field("r".into()),
3341 VarSegment::Dynamic("k".into())
3342 ]
3343 ),
3344 other => panic!("expected assignment, got {:?}", other),
3345 }
3346 }
3347
3348 #[test]
3349 fn parse_local_lvalue_spaced() {
3350 let result = parse("local xs[0] = 9").unwrap();
3351 match &result.statements[0] {
3352 Stmt::Assignment(a) => {
3353 assert!(a.local);
3354 assert_eq!(
3355 a.path.segments,
3356 vec![VarSegment::Field("xs".into()), VarSegment::Index(0)]
3357 );
3358 }
3359 other => panic!("expected assignment, got {:?}", other),
3360 }
3361 }
3362
3363 #[test]
3364 fn env_prefix_subscripted_target_is_not_captured_as_env_scoped() {
3365 let result = parse("user={}\nuser[email]=x echo hi").unwrap();
3375 for stmt in &result.statements {
3376 assert!(
3377 !matches!(stmt, Stmt::EnvScoped { .. }),
3378 "a subscripted assignment must never be captured into EnvScoped: {stmt:?}"
3379 );
3380 }
3381 assert!(matches!(&result.statements[1], Stmt::Assignment(a) if a.name() == "user"));
3383 assert!(matches!(&result.statements[2], Stmt::Command(c) if c.name == "echo"));
3384 }
3385
3386 #[test]
3387 fn parse_nested_cmd_subst() {
3388 let result = parse("X=$(echo $(date))").unwrap();
3390 match &result.statements[0] {
3391 Stmt::Assignment(a) => {
3392 assert_eq!(a.name(), "X");
3393 let outer = subst_cmd(&a.value);
3394 assert_eq!(outer.name, "echo");
3395 match &outer.args[0] {
3397 Arg::Positional(inner_expr) => {
3398 assert_eq!(subst_cmd(inner_expr).name, "date");
3399 }
3400 other => panic!("expected nested cmd subst arg, got {:?}", other),
3401 }
3402 }
3403 other => panic!("expected assignment, got {:?}", other),
3404 }
3405 }
3406
3407 #[test]
3408 fn parse_deeply_nested_cmd_subst() {
3409 let result = parse("X=$(a $(b $(c)))").unwrap();
3411 match &result.statements[0] {
3412 Stmt::Assignment(a) => {
3413 let level1 = subst_cmd(&a.value);
3414 assert_eq!(level1.name, "a");
3415 match &level1.args[0] {
3416 Arg::Positional(level2_expr) => {
3417 let level2 = subst_cmd(level2_expr);
3418 assert_eq!(level2.name, "b");
3419 match &level2.args[0] {
3420 Arg::Positional(level3_expr) => {
3421 assert_eq!(subst_cmd(level3_expr).name, "c");
3422 }
3423 other => panic!("expected level3 cmd subst, got {:?}", other),
3424 }
3425 }
3426 other => panic!("expected level2 cmd subst, got {:?}", other),
3427 }
3428 }
3429 other => panic!("expected assignment, got {:?}", other),
3430 }
3431 }
3432
3433 #[test]
3438 fn value_int_preserved() {
3439 let result = parse("X=42").unwrap();
3440 match &result.statements[0] {
3441 Stmt::Assignment(a) => {
3442 assert_eq!(a.name(), "X");
3443 match &a.value {
3444 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
3445 other => panic!("expected int literal, got {:?}", other),
3446 }
3447 }
3448 other => panic!("expected assignment, got {:?}", other),
3449 }
3450 }
3451
3452 #[test]
3453 fn value_negative_int_preserved() {
3454 let result = parse("X=-99").unwrap();
3455 match &result.statements[0] {
3456 Stmt::Assignment(a) => match &a.value {
3457 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
3458 other => panic!("expected int, got {:?}", other),
3459 },
3460 other => panic!("expected assignment, got {:?}", other),
3461 }
3462 }
3463
3464 #[test]
3465 fn value_float_preserved() {
3466 let result = parse("PI=3.14").unwrap();
3467 match &result.statements[0] {
3468 Stmt::Assignment(a) => match &a.value {
3469 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
3470 other => panic!("expected float, got {:?}", other),
3471 },
3472 other => panic!("expected assignment, got {:?}", other),
3473 }
3474 }
3475
3476 #[test]
3477 fn value_string_preserved() {
3478 let result = parse(r#"echo "hello world""#).unwrap();
3479 match &result.statements[0] {
3480 Stmt::Command(cmd) => {
3481 assert_eq!(cmd.name, "echo");
3482 match &cmd.args[0] {
3483 Arg::Positional(Expr::Literal(Value::String(s))) => {
3484 assert_eq!(s, "hello world");
3485 }
3486 other => panic!("expected string arg, got {:?}", other),
3487 }
3488 }
3489 other => panic!("expected command, got {:?}", other),
3490 }
3491 }
3492
3493 #[test]
3494 fn value_string_with_escapes_preserved() {
3495 let result = parse(r#"echo "line1\nline2""#).unwrap();
3496 match &result.statements[0] {
3497 Stmt::Command(cmd) => match &cmd.args[0] {
3498 Arg::Positional(Expr::Literal(Value::String(s))) => {
3499 assert_eq!(s, "line1\nline2");
3500 }
3501 other => panic!("expected string, got {:?}", other),
3502 },
3503 other => panic!("expected command, got {:?}", other),
3504 }
3505 }
3506
3507 #[test]
3508 fn value_command_name_preserved() {
3509 let result = parse("my-command").unwrap();
3510 match &result.statements[0] {
3511 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
3512 other => panic!("expected command, got {:?}", other),
3513 }
3514 }
3515
3516 #[test]
3517 fn value_assignment_name_preserved() {
3518 let result = parse("MY_VAR=1").unwrap();
3519 match &result.statements[0] {
3520 Stmt::Assignment(a) => assert_eq!(a.name(), "MY_VAR"),
3521 other => panic!("expected assignment, got {:?}", other),
3522 }
3523 }
3524
3525 #[test]
3526 fn value_for_variable_preserved() {
3527 let result = parse("for ITEM in items; do echo; done").unwrap();
3528 match &result.statements[0] {
3529 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
3530 other => panic!("expected for, got {:?}", other),
3531 }
3532 }
3533
3534 #[test]
3535 fn value_varref_name_preserved() {
3536 let result = parse("echo ${MESSAGE}").unwrap();
3537 match &result.statements[0] {
3538 Stmt::Command(cmd) => match &cmd.args[0] {
3539 Arg::Positional(Expr::VarRef(path)) => {
3540 assert_eq!(path.segments.len(), 1);
3541 let VarSegment::Field(name) = &path.segments[0] else {
3542 panic!("expected root field, got {:?}", path.segments[0]);
3543 };
3544 assert_eq!(name, "MESSAGE");
3545 }
3546 other => panic!("expected varref, got {:?}", other),
3547 },
3548 other => panic!("expected command, got {:?}", other),
3549 }
3550 }
3551
3552 #[test]
3553 fn value_varref_field_access_preserved() {
3554 let result = parse("echo ${RESULT.data}").unwrap();
3555 match &result.statements[0] {
3556 Stmt::Command(cmd) => match &cmd.args[0] {
3557 Arg::Positional(Expr::VarRef(path)) => {
3558 assert_eq!(path.segments.len(), 2);
3562 let VarSegment::Field(a) = &path.segments[0] else {
3563 panic!("expected field, got {:?}", path.segments[0]);
3564 };
3565 let VarSegment::Field(b) = &path.segments[1] else {
3566 panic!("expected field, got {:?}", path.segments[1]);
3567 };
3568 assert_eq!(a, "RESULT");
3569 assert_eq!(b, "data");
3570 }
3571 other => panic!("expected varref, got {:?}", other),
3572 },
3573 other => panic!("expected command, got {:?}", other),
3574 }
3575 }
3576
3577 #[test]
3578 fn value_varref_index_parsed() {
3579 let result = parse("echo ${ITEMS[0]}").unwrap();
3582 match &result.statements[0] {
3583 Stmt::Command(cmd) => match &cmd.args[0] {
3584 Arg::Positional(Expr::VarRef(path)) => {
3585 assert_eq!(path.segments.len(), 2);
3586 let VarSegment::Field(name) = &path.segments[0] else {
3587 panic!("expected root field, got {:?}", path.segments[0]);
3588 };
3589 assert_eq!(name, "ITEMS");
3590 assert_eq!(path.segments[1], VarSegment::Index(0));
3591 }
3592 other => panic!("expected varref, got {:?}", other),
3593 },
3594 other => panic!("expected command, got {:?}", other),
3595 }
3596 }
3597
3598 #[test]
3599 fn value_named_arg_preserved() {
3600 let result = parse("cmd count=42").unwrap();
3604 match &result.statements[0] {
3605 Stmt::Command(cmd) => {
3606 assert_eq!(cmd.name, "cmd");
3607 match &cmd.args[0] {
3608 Arg::WordAssign { key, value } => {
3609 assert_eq!(key, "count");
3610 match value {
3611 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
3612 other => panic!("expected int, got {:?}", other),
3613 }
3614 }
3615 other => panic!("expected WordAssign arg, got {:?}", other),
3616 }
3617 }
3618 other => panic!("expected command, got {:?}", other),
3619 }
3620 }
3621
3622 #[test]
3623 fn value_function_def_name_preserved() {
3624 let result = parse("greet() { echo }").unwrap();
3625 match &result.statements[0] {
3626 Stmt::ToolDef(t) => {
3627 assert_eq!(t.name, "greet");
3628 assert!(t.params.is_empty());
3629 }
3630 other => panic!("expected function def, got {:?}", other),
3631 }
3632 }
3633
3634 #[test]
3639 fn parse_comparison_equals() {
3640 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
3642 match &result.statements[0] {
3643 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3644 Expr::Test(test) => match test.as_ref() {
3645 TestExpr::Comparison { left, op, right } => {
3646 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3647 assert_eq!(*op, TestCmpOp::Eq);
3648 match right.as_ref() {
3649 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
3650 other => panic!("expected int, got {:?}", other),
3651 }
3652 }
3653 other => panic!("expected comparison, got {:?}", other),
3654 },
3655 other => panic!("expected test expr, got {:?}", other),
3656 },
3657 other => panic!("expected if, got {:?}", other),
3658 }
3659 }
3660
3661 #[test]
3662 fn parse_comparison_not_equals() {
3663 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
3664 match &result.statements[0] {
3665 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3666 Expr::Test(test) => match test.as_ref() {
3667 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
3668 other => panic!("expected comparison, got {:?}", other),
3669 },
3670 other => panic!("expected test expr, got {:?}", other),
3671 },
3672 other => panic!("expected if, got {:?}", other),
3673 }
3674 }
3675
3676 #[test]
3677 fn parse_comparison_less_than() {
3678 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
3679 match &result.statements[0] {
3680 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3681 Expr::Test(test) => match test.as_ref() {
3682 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
3683 other => panic!("expected comparison, got {:?}", other),
3684 },
3685 other => panic!("expected test expr, got {:?}", other),
3686 },
3687 other => panic!("expected if, got {:?}", other),
3688 }
3689 }
3690
3691 #[test]
3692 fn parse_comparison_greater_than() {
3693 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
3694 match &result.statements[0] {
3695 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3696 Expr::Test(test) => match test.as_ref() {
3697 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
3698 other => panic!("expected comparison, got {:?}", other),
3699 },
3700 other => panic!("expected test expr, got {:?}", other),
3701 },
3702 other => panic!("expected if, got {:?}", other),
3703 }
3704 }
3705
3706 #[test]
3707 fn parse_comparison_less_equal() {
3708 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
3709 match &result.statements[0] {
3710 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3711 Expr::Test(test) => match test.as_ref() {
3712 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
3713 other => panic!("expected comparison, got {:?}", other),
3714 },
3715 other => panic!("expected test expr, got {:?}", other),
3716 },
3717 other => panic!("expected if, got {:?}", other),
3718 }
3719 }
3720
3721 #[test]
3722 fn parse_comparison_greater_equal() {
3723 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
3724 match &result.statements[0] {
3725 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3726 Expr::Test(test) => match test.as_ref() {
3727 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
3728 other => panic!("expected comparison, got {:?}", other),
3729 },
3730 other => panic!("expected test expr, got {:?}", other),
3731 },
3732 other => panic!("expected if, got {:?}", other),
3733 }
3734 }
3735
3736 #[test]
3737 fn parse_regex_match() {
3738 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
3739 match &result.statements[0] {
3740 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3741 Expr::Test(test) => match test.as_ref() {
3742 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
3743 other => panic!("expected comparison, got {:?}", other),
3744 },
3745 other => panic!("expected test expr, got {:?}", other),
3746 },
3747 other => panic!("expected if, got {:?}", other),
3748 }
3749 }
3750
3751 #[test]
3752 fn parse_regex_not_match() {
3753 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
3754 match &result.statements[0] {
3755 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3756 Expr::Test(test) => match test.as_ref() {
3757 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
3758 other => panic!("expected comparison, got {:?}", other),
3759 },
3760 other => panic!("expected test expr, got {:?}", other),
3761 },
3762 other => panic!("expected if, got {:?}", other),
3763 }
3764 }
3765
3766 #[test]
3767 fn parse_string_interpolation() {
3768 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
3769 match &result.statements[0] {
3770 Stmt::Command(cmd) => match &cmd.args[0] {
3771 Arg::Positional(Expr::Interpolated(parts)) => {
3772 assert_eq!(parts.len(), 3);
3773 match &parts[0] {
3774 StringPart::Literal(s) => assert_eq!(s, "Hello "),
3775 other => panic!("expected literal, got {:?}", other),
3776 }
3777 match &parts[1] {
3778 StringPart::Var(path) => {
3779 assert_eq!(path.segments.len(), 1);
3780 let VarSegment::Field(name) = &path.segments[0] else {
3781 panic!("expected root field, got {:?}", path.segments[0]);
3782 };
3783 assert_eq!(name, "NAME");
3784 }
3785 other => panic!("expected var, got {:?}", other),
3786 }
3787 match &parts[2] {
3788 StringPart::Literal(s) => assert_eq!(s, "!"),
3789 other => panic!("expected literal, got {:?}", other),
3790 }
3791 }
3792 other => panic!("expected interpolated, got {:?}", other),
3793 },
3794 other => panic!("expected command, got {:?}", other),
3795 }
3796 }
3797
3798 #[test]
3799 fn parse_string_interpolation_multiple_vars() {
3800 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
3801 match &result.statements[0] {
3802 Stmt::Command(cmd) => match &cmd.args[0] {
3803 Arg::Positional(Expr::Interpolated(parts)) => {
3804 assert_eq!(parts.len(), 3);
3806 assert!(matches!(&parts[0], StringPart::Var(_)));
3807 assert!(matches!(&parts[1], StringPart::Literal(_)));
3808 assert!(matches!(&parts[2], StringPart::Var(_)));
3809 }
3810 other => panic!("expected interpolated, got {:?}", other),
3811 },
3812 other => panic!("expected command, got {:?}", other),
3813 }
3814 }
3815
3816 #[test]
3817 fn parse_empty_function_body() {
3818 let result = parse("empty() { }").unwrap();
3819 match &result.statements[0] {
3820 Stmt::ToolDef(t) => {
3821 assert_eq!(t.name, "empty");
3822 assert!(t.params.is_empty());
3823 assert!(t.body.is_empty());
3824 }
3825 other => panic!("expected function def, got {:?}", other),
3826 }
3827 }
3828
3829 #[test]
3830 fn parse_bash_style_function() {
3831 let result = parse("function greet { echo hello }").unwrap();
3832 match &result.statements[0] {
3833 Stmt::ToolDef(t) => {
3834 assert_eq!(t.name, "greet");
3835 assert!(t.params.is_empty());
3836 assert_eq!(t.body.len(), 1);
3837 }
3838 other => panic!("expected function def, got {:?}", other),
3839 }
3840 }
3841
3842 #[test]
3843 fn parse_comparison_string_values() {
3844 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
3845 match &result.statements[0] {
3846 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3847 Expr::Test(test) => match test.as_ref() {
3848 TestExpr::Comparison { left, op, right } => {
3849 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3850 assert_eq!(*op, TestCmpOp::Eq);
3851 match right.as_ref() {
3852 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
3853 other => panic!("expected string, got {:?}", other),
3854 }
3855 }
3856 other => panic!("expected comparison, got {:?}", other),
3857 },
3858 other => panic!("expected test expr, got {:?}", other),
3859 },
3860 other => panic!("expected if, got {:?}", other),
3861 }
3862 }
3863
3864 #[test]
3869 fn parse_cmd_subst_simple() {
3870 let result = parse("X=$(echo)").unwrap();
3871 match &result.statements[0] {
3872 Stmt::Assignment(a) => {
3873 assert_eq!(a.name(), "X");
3874 assert_eq!(subst_cmd(&a.value).name, "echo");
3875 }
3876 other => panic!("expected assignment, got {:?}", other),
3877 }
3878 }
3879
3880 #[test]
3881 fn parse_cmd_subst_with_args() {
3882 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
3883 match &result.statements[0] {
3884 Stmt::Assignment(a) => {
3885 let cmd = subst_cmd(&a.value);
3886 assert_eq!(cmd.name, "fetch");
3887 assert_eq!(cmd.args.len(), 1);
3888 match &cmd.args[0] {
3889 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
3890 other => panic!("expected WordAssign arg, got {:?}", other),
3891 }
3892 }
3893 other => panic!("expected assignment, got {:?}", other),
3894 }
3895 }
3896
3897 #[test]
3898 fn parse_cmd_subst_pipeline() {
3899 let result = parse("X=$(cat file | grep pattern)").unwrap();
3900 match &result.statements[0] {
3901 Stmt::Assignment(a) => {
3902 let pipeline = subst_pipeline(&a.value);
3903 assert_eq!(pipeline.commands.len(), 2);
3904 assert_eq!(pipeline.commands[0].name, "cat");
3905 assert_eq!(pipeline.commands[1].name, "grep");
3906 }
3907 other => panic!("expected assignment, got {:?}", other),
3908 }
3909 }
3910
3911 #[test]
3912 fn parse_cmd_subst_with_redirect() {
3913 let result = parse("X=$(echo hi > out.txt)").unwrap();
3918 match &result.statements[0] {
3919 Stmt::Assignment(a) => {
3920 let pipeline = subst_pipeline(&a.value);
3921 assert_eq!(pipeline.commands.len(), 1);
3922 let cmd = &pipeline.commands[0];
3923 assert_eq!(cmd.name, "echo");
3924 assert_eq!(cmd.redirects.len(), 1);
3925 assert!(matches!(
3926 cmd.redirects[0].kind,
3927 RedirectKind::StdoutOverwrite
3928 ));
3929 }
3930 other => panic!("expected assignment, got {:?}", other),
3931 }
3932 }
3933
3934 #[test]
3935 fn parse_cmd_subst_redirect_target_with_nested_subst() {
3936 let result = parse("X=$(echo hi > $(echo f))").unwrap();
3942 match &result.statements[0] {
3943 Stmt::Assignment(a) => {
3944 let pipeline = subst_pipeline(&a.value);
3945 assert_eq!(pipeline.commands.len(), 1);
3946 let cmd = &pipeline.commands[0];
3947 assert_eq!(cmd.name, "echo");
3948 assert_eq!(cmd.redirects.len(), 1);
3949 assert!(
3950 matches!(cmd.redirects[0].target, Expr::CommandSubst(_)),
3951 "redirect target should be a nested command substitution, got {:?}",
3952 cmd.redirects[0].target
3953 );
3954 }
3955 other => panic!("expected assignment, got {:?}", other),
3956 }
3957 }
3958
3959 #[test]
3960 fn parse_cmd_subst_chain_with_redirect() {
3961 let result = parse("X=$(echo a && echo b > out.txt)").unwrap();
3965 let stmts = match &result.statements[0] {
3966 Stmt::Assignment(a) => match &a.value {
3967 Expr::CommandSubst(s) => s,
3968 other => panic!("expected command subst, got {:?}", other),
3969 },
3970 other => panic!("expected assignment, got {:?}", other),
3971 };
3972 match stmts.as_slice() {
3973 [Stmt::AndChain { left, right }] => {
3974 assert!(
3976 matches!(**left, Stmt::Command(_)),
3977 "left of && should be a bare command, got {:?}",
3978 left
3979 );
3980 match &**right {
3982 Stmt::Pipeline(p) => {
3983 assert_eq!(p.commands.len(), 1);
3984 assert_eq!(p.commands[0].name, "echo");
3985 assert_eq!(p.commands[0].redirects.len(), 1);
3986 }
3987 other => panic!("right should be a redirect-bearing pipeline, got {:?}", other),
3988 }
3989 }
3990 other => panic!("expected a single AndChain, got {:?}", other),
3991 }
3992 }
3993
3994 #[test]
3995 fn parse_cmd_subst_in_condition() {
3996 let result = parse("if kaish-validate; then echo; fi").unwrap();
3998 match &result.statements[0] {
3999 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4000 Expr::Command(cmd) => {
4001 assert_eq!(cmd.name, "kaish-validate");
4002 }
4003 other => panic!("expected command, got {:?}", other),
4004 },
4005 other => panic!("expected if, got {:?}", other),
4006 }
4007 }
4008
4009 #[test]
4014 fn parse_env_prefix_single() {
4015 let result = parse("FOO=bar echo hi").unwrap();
4016 match &result.statements[0] {
4017 Stmt::EnvScoped { assignments, body } => {
4018 assert_eq!(assignments.len(), 1);
4019 assert_eq!(assignments[0].name(), "FOO");
4020 assert!(!assignments[0].local);
4021 match body.as_ref() {
4022 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
4023 other => panic!("expected command body, got {other:?}"),
4024 }
4025 }
4026 other => panic!("expected env-scoped, got {other:?}"),
4027 }
4028 }
4029
4030 #[test]
4031 fn parse_env_prefix_multiple() {
4032 let result = parse("A=1 B=2 run").unwrap();
4033 match &result.statements[0] {
4034 Stmt::EnvScoped { assignments, body } => {
4035 assert_eq!(assignments.len(), 2);
4036 assert_eq!(assignments[0].name(), "A");
4037 assert_eq!(assignments[1].name(), "B");
4038 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
4039 }
4040 other => panic!("expected env-scoped, got {other:?}"),
4041 }
4042 }
4043
4044 #[test]
4045 fn parse_bare_assignment_is_not_env_scoped() {
4046 let result = parse("FOO=bar").unwrap();
4048 assert!(
4049 matches!(&result.statements[0], Stmt::Assignment(a) if a.name() == "FOO"),
4050 "got {:?}",
4051 result.statements[0]
4052 );
4053 }
4054
4055 #[test]
4056 fn parse_assignment_then_and_chain_does_not_over_capture() {
4057 let result = parse("FOO=bar && echo hi").unwrap();
4060 match &result.statements[0] {
4061 Stmt::AndChain { left, right } => {
4062 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name() == "FOO"));
4063 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
4064 }
4065 other => panic!("expected and-chain, got {other:?}"),
4066 }
4067 }
4068
4069 #[test]
4070 fn parse_env_prefix_pipeline_body() {
4071 let result = parse("FOO=bar cat | grep x").unwrap();
4072 match &result.statements[0] {
4073 Stmt::EnvScoped { assignments, body } => {
4074 assert_eq!(assignments[0].name(), "FOO");
4075 match body.as_ref() {
4076 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 2),
4077 other => panic!("expected pipeline body, got {other:?}"),
4078 }
4079 }
4080 other => panic!("expected env-scoped, got {other:?}"),
4081 }
4082 }
4083
4084 fn parse_err_message(source: &str) -> String {
4089 parse(source)
4090 .expect_err("expected a parse error")
4091 .iter()
4092 .map(|e| e.message.clone())
4093 .collect::<Vec<_>>()
4094 .join(" ")
4095 }
4096
4097 #[test]
4098 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
4099 let msg = parse_err_message("echo /tmp/$(echo x).txt");
4102 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
4103 }
4104
4105 #[test]
4106 fn argv_splat_var_glued_to_path_is_rejected() {
4107 assert!(parse("echo $dir/out.txt").is_err());
4108 }
4109
4110 #[test]
4111 fn argv_splat_three_way_glue_is_rejected() {
4112 assert!(parse("echo foo$(echo bar)baz").is_err());
4113 }
4114
4115 #[test]
4116 fn argv_splat_quoted_word_is_accepted() {
4117 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
4119 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
4120 }
4121
4122 #[test]
4123 fn argv_single_token_words_are_not_splat() {
4124 assert!(parse("echo file.txt").is_ok(), "file.txt");
4126 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
4127 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
4128 }
4129
4130 #[test]
4131 fn argv_spaced_words_are_not_splat() {
4132 assert!(parse("echo a b c").is_ok());
4133 assert!(parse("echo /tmp/x $(echo y)").is_ok());
4134 }
4135
4136 #[test]
4137 fn parse_cmd_subst_in_command_arg() {
4138 let result = parse("echo $(whoami)").unwrap();
4139 match &result.statements[0] {
4140 Stmt::Command(cmd) => {
4141 assert_eq!(cmd.name, "echo");
4142 match &cmd.args[0] {
4143 Arg::Positional(expr) => {
4144 assert_eq!(subst_cmd(expr).name, "whoami");
4145 }
4146 other => panic!("expected command subst, got {:?}", other),
4147 }
4148 }
4149 other => panic!("expected command, got {:?}", other),
4150 }
4151 }
4152
4153 #[test]
4158 fn parse_condition_and() {
4159 let result = parse("if check-a && check-b; then echo; fi").unwrap();
4161 match &result.statements[0] {
4162 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4163 Expr::BinaryOp { left, op, right } => {
4164 assert_eq!(*op, BinaryOp::And);
4165 assert!(matches!(left.as_ref(), Expr::Command(_)));
4166 assert!(matches!(right.as_ref(), Expr::Command(_)));
4167 }
4168 other => panic!("expected binary op, got {:?}", other),
4169 },
4170 other => panic!("expected if, got {:?}", other),
4171 }
4172 }
4173
4174 #[test]
4175 fn parse_condition_or() {
4176 let result = parse("if try-a || try-b; then echo; fi").unwrap();
4177 match &result.statements[0] {
4178 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4179 Expr::BinaryOp { left, op, right } => {
4180 assert_eq!(*op, BinaryOp::Or);
4181 assert!(matches!(left.as_ref(), Expr::Command(_)));
4182 assert!(matches!(right.as_ref(), Expr::Command(_)));
4183 }
4184 other => panic!("expected binary op, got {:?}", other),
4185 },
4186 other => panic!("expected if, got {:?}", other),
4187 }
4188 }
4189
4190 #[test]
4191 fn parse_condition_and_or_precedence() {
4192 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
4194 match &result.statements[0] {
4195 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4196 Expr::BinaryOp { left, op, right } => {
4197 assert_eq!(*op, BinaryOp::Or);
4199 match left.as_ref() {
4201 Expr::BinaryOp { op: inner_op, .. } => {
4202 assert_eq!(*inner_op, BinaryOp::And);
4203 }
4204 other => panic!("expected binary op (&&), got {:?}", other),
4205 }
4206 assert!(matches!(right.as_ref(), Expr::Command(_)));
4208 }
4209 other => panic!("expected binary op, got {:?}", other),
4210 },
4211 other => panic!("expected if, got {:?}", other),
4212 }
4213 }
4214
4215 #[test]
4216 fn parse_condition_multiple_and() {
4217 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
4218 match &result.statements[0] {
4219 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4220 Expr::BinaryOp { left, op, .. } => {
4221 assert_eq!(*op, BinaryOp::And);
4222 match left.as_ref() {
4224 Expr::BinaryOp { op: inner_op, .. } => {
4225 assert_eq!(*inner_op, BinaryOp::And);
4226 }
4227 other => panic!("expected binary op, got {:?}", other),
4228 }
4229 }
4230 other => panic!("expected binary op, got {:?}", other),
4231 },
4232 other => panic!("expected if, got {:?}", other),
4233 }
4234 }
4235
4236 #[test]
4237 fn parse_condition_mixed_comparison_and_logical() {
4238 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
4240 match &result.statements[0] {
4241 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
4242 Expr::BinaryOp { left, op, right } => {
4243 assert_eq!(*op, BinaryOp::And);
4244 match left.as_ref() {
4246 Expr::Test(test) => match test.as_ref() {
4247 TestExpr::Comparison { op: left_op, .. } => {
4248 assert_eq!(*left_op, TestCmpOp::Eq);
4249 }
4250 other => panic!("expected comparison, got {:?}", other),
4251 },
4252 other => panic!("expected test, got {:?}", other),
4253 }
4254 match right.as_ref() {
4256 Expr::Test(test) => match test.as_ref() {
4257 TestExpr::Comparison { op: right_op, .. } => {
4258 assert_eq!(*right_op, TestCmpOp::NumGt);
4259 }
4260 other => panic!("expected comparison, got {:?}", other),
4261 },
4262 other => panic!("expected test, got {:?}", other),
4263 }
4264 }
4265 other => panic!("expected binary op, got {:?}", other),
4266 },
4267 other => panic!("expected if, got {:?}", other),
4268 }
4269 }
4270
4271 #[test]
4277 fn script_level1_linear() {
4278 let script = r#"
4279NAME="kaish"
4280VERSION=1
4281TIMEOUT=30
4282ITEMS="alpha beta gamma"
4283
4284echo "Starting ${NAME} v${VERSION}"
4285cat "README.md" | grep pattern="install" | head count=5
4286fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
4287echo "Items: ${ITEMS}"
4288"#;
4289 let result = parse(script).unwrap();
4290 let stmts: Vec<_> = result.statements.iter()
4291 .filter(|s| !matches!(s, Stmt::Empty))
4292 .collect();
4293
4294 assert_eq!(stmts.len(), 8);
4295 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(_))); }
4304
4305 #[test]
4307 fn script_level2_branching() {
4308 let script = r#"
4309RESULT=$(kaish-validate "input.json")
4310
4311if [[ ${RESULT.ok} == true ]]; then
4312 echo "Validation passed"
4313 process "input.json" > "output.json"
4314else
4315 echo "Validation failed: ${RESULT.err}"
4316fi
4317
4318if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
4319 echo "Count in valid range"
4320fi
4321
4322if check-network || check-cache; then
4323 fetch url=${URL}
4324fi
4325"#;
4326 let result = parse(script).unwrap();
4327 let stmts: Vec<_> = result.statements.iter()
4328 .filter(|s| !matches!(s, Stmt::Empty))
4329 .collect();
4330
4331 assert_eq!(stmts.len(), 4);
4332
4333 match stmts[0] {
4335 Stmt::Assignment(a) => {
4336 assert_eq!(a.name(), "RESULT");
4337 assert!(matches!(&a.value, Expr::CommandSubst(_)));
4338 }
4339 other => panic!("expected assignment, got {:?}", other),
4340 }
4341
4342 match stmts[1] {
4344 Stmt::If(if_stmt) => {
4345 assert_eq!(if_stmt.then_branch.len(), 2);
4346 assert!(if_stmt.else_branch.is_some());
4347 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
4348 }
4349 other => panic!("expected if, got {:?}", other),
4350 }
4351
4352 match stmts[2] {
4354 Stmt::If(if_stmt) => {
4355 match if_stmt.condition.as_ref() {
4356 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
4357 other => panic!("expected && condition, got {:?}", other),
4358 }
4359 }
4360 other => panic!("expected if, got {:?}", other),
4361 }
4362
4363 match stmts[3] {
4365 Stmt::If(if_stmt) => {
4366 match if_stmt.condition.as_ref() {
4367 Expr::BinaryOp { op, left, right } => {
4368 assert_eq!(*op, BinaryOp::Or);
4369 assert!(matches!(left.as_ref(), Expr::Command(_)));
4370 assert!(matches!(right.as_ref(), Expr::Command(_)));
4371 }
4372 other => panic!("expected || condition, got {:?}", other),
4373 }
4374 }
4375 other => panic!("expected if, got {:?}", other),
4376 }
4377 }
4378
4379 #[test]
4381 fn script_level3_loops_and_functions() {
4382 let script = r#"
4383greet() {
4384 echo "Hello, $1!"
4385}
4386
4387fetch_all() {
4388 for URL in $@; do
4389 fetch url=${URL}
4390 done
4391}
4392
4393USERS="alice bob charlie"
4394
4395for USER in ${USERS}; do
4396 greet ${USER}
4397 if [[ ${USER} == "bob" ]]; then
4398 echo "Found Bob!"
4399 fi
4400done
4401
4402long-running-task &
4403"#;
4404 let result = parse(script).unwrap();
4405 let stmts: Vec<_> = result.statements.iter()
4406 .filter(|s| !matches!(s, Stmt::Empty))
4407 .collect();
4408
4409 assert_eq!(stmts.len(), 5);
4410
4411 match stmts[0] {
4413 Stmt::ToolDef(t) => {
4414 assert_eq!(t.name, "greet");
4415 assert!(t.params.is_empty());
4416 }
4417 other => panic!("expected function def, got {:?}", other),
4418 }
4419
4420 match stmts[1] {
4422 Stmt::ToolDef(t) => {
4423 assert_eq!(t.name, "fetch_all");
4424 assert_eq!(t.body.len(), 1);
4425 assert!(matches!(&t.body[0], Stmt::For(_)));
4426 }
4427 other => panic!("expected function def, got {:?}", other),
4428 }
4429
4430 assert!(matches!(stmts[2], Stmt::Assignment(_)));
4432
4433 match stmts[3] {
4435 Stmt::For(f) => {
4436 assert_eq!(f.variable, "USER");
4437 assert_eq!(f.body.len(), 2);
4438 assert!(matches!(&f.body[0], Stmt::Command(_)));
4439 assert!(matches!(&f.body[1], Stmt::If(_)));
4440 }
4441 other => panic!("expected for loop, got {:?}", other),
4442 }
4443
4444 match stmts[4] {
4446 Stmt::Pipeline(p) => {
4447 assert!(p.background);
4448 assert_eq!(p.commands[0].name, "long-running-task");
4449 }
4450 other => panic!("expected pipeline (background), got {:?}", other),
4451 }
4452 }
4453
4454 #[test]
4456 fn script_level4_complex_nesting() {
4457 let script = r#"
4458RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
4459
4460if ping host=${HOST} && [[ ${RESULT} == true ]]; then
4461 for SERVER in "prod-1 prod-2"; do
4462 deploy target=${SERVER} port=8080
4463 if [[ $? -ne 0 ]]; then
4464 notify channel="ops" message="Deploy failed"
4465 fi
4466 done
4467fi
4468"#;
4469 let result = parse(script).unwrap();
4470 let stmts: Vec<_> = result.statements.iter()
4471 .filter(|s| !matches!(s, Stmt::Empty))
4472 .collect();
4473
4474 assert_eq!(stmts.len(), 2);
4475
4476 match stmts[0] {
4478 Stmt::Assignment(a) => {
4479 assert_eq!(a.name(), "RESULT");
4480 assert_eq!(subst_pipeline(&a.value).commands.len(), 3);
4481 }
4482 other => panic!("expected assignment, got {:?}", other),
4483 }
4484
4485 match stmts[1] {
4487 Stmt::If(if_stmt) => {
4488 match if_stmt.condition.as_ref() {
4489 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
4490 other => panic!("expected && condition, got {:?}", other),
4491 }
4492 assert_eq!(if_stmt.then_branch.len(), 1);
4493 match &if_stmt.then_branch[0] {
4494 Stmt::For(f) => {
4495 assert_eq!(f.body.len(), 2);
4496 assert!(matches!(&f.body[1], Stmt::If(_)));
4497 }
4498 other => panic!("expected for in if body, got {:?}", other),
4499 }
4500 }
4501 other => panic!("expected if, got {:?}", other),
4502 }
4503 }
4504
4505 #[test]
4507 fn script_level5_edge_cases() {
4508 let script = r#"
4509echo ""
4510echo "quotes: \"nested\" here"
4511echo "escapes: \n\t\r\\"
4512echo "unicode: \u2764"
4513
4514X=-99999
4515Y=3.14159265358979
4516Z=-0.001
4517
4518cmd a=1 b="two" c=true d=false e=null
4519
4520if true; then
4521 if false; then
4522 echo "inner"
4523 else
4524 echo "else"
4525 fi
4526fi
4527
4528for I in "a b c"; do
4529 echo ${I}
4530done
4531
4532no_params() {
4533 echo "no params"
4534}
4535
4536function all_args {
4537 echo "args: $@"
4538}
4539
4540a | b | c | d | e &
4541cmd 2> "errors.log"
4542cmd &> "all.log"
4543cmd >> "append.log"
4544cmd < "input.txt"
4545"#;
4546 let result = parse(script).unwrap();
4547 let stmts: Vec<_> = result.statements.iter()
4548 .filter(|s| !matches!(s, Stmt::Empty))
4549 .collect();
4550
4551 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
4553
4554 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
4556 assert!(bg_stmt.is_some(), "expected background pipeline");
4557
4558 match bg_stmt.unwrap() {
4559 Stmt::Pipeline(p) => {
4560 assert_eq!(p.commands.len(), 5);
4561 assert!(p.background);
4562 }
4563 _ => unreachable!(),
4564 }
4565 }
4566
4567 #[test]
4572 fn parse_keyword_as_variable_rejected() {
4573 let result = parse(r#"if="value""#);
4576 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
4577
4578 let result = parse("while=true");
4579 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
4580
4581 let result = parse(r#"then="next""#);
4582 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
4583 }
4584
4585 #[test]
4586 fn parse_set_command_with_flag() {
4587 let result = parse("set -e");
4588 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
4589 let program = result.unwrap();
4590 match &program.statements[0] {
4591 Stmt::Command(cmd) => {
4592 assert_eq!(cmd.name, "set");
4593 assert_eq!(cmd.args.len(), 1);
4594 match &cmd.args[0] {
4595 Arg::ShortFlag(f) => assert_eq!(f, "e"),
4596 other => panic!("expected ShortFlag, got {:?}", other),
4597 }
4598 }
4599 other => panic!("expected Command, got {:?}", other),
4600 }
4601 }
4602
4603 #[test]
4604 fn parse_set_command_no_args() {
4605 let result = parse("set");
4606 assert!(result.is_ok(), "failed to parse set: {:?}", result);
4607 let program = result.unwrap();
4608 match &program.statements[0] {
4609 Stmt::Command(cmd) => {
4610 assert_eq!(cmd.name, "set");
4611 assert_eq!(cmd.args.len(), 0);
4612 }
4613 other => panic!("expected Command, got {:?}", other),
4614 }
4615 }
4616
4617 #[test]
4618 fn parse_set_assignment_vs_command() {
4619 let result = parse("X=5");
4621 assert!(result.is_ok());
4622 let program = result.unwrap();
4623 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
4624
4625 let result = parse("set -e");
4627 assert!(result.is_ok());
4628 let program = result.unwrap();
4629 assert!(matches!(&program.statements[0], Stmt::Command(_)));
4630 }
4631
4632 #[test]
4633 fn parse_true_as_command() {
4634 let result = parse("true");
4635 assert!(result.is_ok());
4636 let program = result.unwrap();
4637 match &program.statements[0] {
4638 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
4639 other => panic!("expected Command(true), got {:?}", other),
4640 }
4641 }
4642
4643 #[test]
4644 fn parse_false_as_command() {
4645 let result = parse("false");
4646 assert!(result.is_ok());
4647 let program = result.unwrap();
4648 match &program.statements[0] {
4649 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
4650 other => panic!("expected Command(false), got {:?}", other),
4651 }
4652 }
4653
4654 #[test]
4655 fn parse_dot_as_source_alias() {
4656 let result = parse(". script.kai");
4657 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
4658 let program = result.unwrap();
4659 match &program.statements[0] {
4660 Stmt::Command(cmd) => {
4661 assert_eq!(cmd.name, ".");
4662 assert_eq!(cmd.args.len(), 1);
4663 }
4664 other => panic!("expected Command(.), got {:?}", other),
4665 }
4666 }
4667
4668 #[test]
4669 fn parse_source_command() {
4670 let result = parse("source utils.kai");
4671 assert!(result.is_ok(), "failed to parse source: {:?}", result);
4672 let program = result.unwrap();
4673 match &program.statements[0] {
4674 Stmt::Command(cmd) => {
4675 assert_eq!(cmd.name, "source");
4676 assert_eq!(cmd.args.len(), 1);
4677 }
4678 other => panic!("expected Command(source), got {:?}", other),
4679 }
4680 }
4681
4682 #[test]
4683 fn parse_test_expr_file_test() {
4684 let result = parse(r#"[[ -f "/path/file" ]]"#);
4686 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
4687 }
4688
4689 #[test]
4690 fn parse_test_expr_comparison() {
4691 let result = parse(r#"[[ $X == "value" ]]"#);
4692 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
4693 }
4694
4695 #[test]
4696 fn parse_test_expr_single_eq() {
4697 let result = parse(r#"[[ $X = "value" ]]"#);
4699 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
4700 let program = result.unwrap();
4701 match &program.statements[0] {
4702 Stmt::Test(TestExpr::Comparison { op, .. }) => {
4703 assert_eq!(op, &TestCmpOp::Eq);
4704 }
4705 other => panic!("expected Test(Comparison), got {:?}", other),
4706 }
4707 }
4708
4709 #[test]
4710 fn parse_while_loop() {
4711 let result = parse("while true; do echo; done");
4712 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
4713 let program = result.unwrap();
4714 assert!(matches!(&program.statements[0], Stmt::While(_)));
4715 }
4716
4717 #[test]
4718 fn parse_break_with_level() {
4719 let result = parse("break 2");
4720 assert!(result.is_ok());
4721 let program = result.unwrap();
4722 match &program.statements[0] {
4723 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
4724 other => panic!("expected Break(2), got {:?}", other),
4725 }
4726 }
4727
4728 #[test]
4729 fn parse_continue_with_level() {
4730 let result = parse("continue 3");
4731 assert!(result.is_ok());
4732 let program = result.unwrap();
4733 match &program.statements[0] {
4734 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
4735 other => panic!("expected Continue(3), got {:?}", other),
4736 }
4737 }
4738
4739 #[test]
4740 fn parse_exit_with_code() {
4741 let result = parse("exit 1");
4742 assert!(result.is_ok());
4743 let program = result.unwrap();
4744 match &program.statements[0] {
4745 Stmt::Exit(Some(expr)) => {
4746 match expr.as_ref() {
4747 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
4748 other => panic!("expected Int(1), got {:?}", other),
4749 }
4750 }
4751 other => panic!("expected Exit(1), got {:?}", other),
4752 }
4753 }
4754
4755 #[test]
4762 fn spanned_literal_only_records_byte_range() {
4763 let parts = parse_interpolated_string_spanned("hello world", 100);
4764 assert_eq!(parts.len(), 1);
4765 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
4766 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
4767 assert_eq!(parts[0].len, 11);
4768 }
4769
4770 #[test]
4771 fn spanned_braced_var_at_zero() {
4772 let parts = parse_interpolated_string_spanned("${X}", 50);
4773 assert_eq!(parts.len(), 1);
4774 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4775 assert_eq!(parts[0].offset, 50);
4776 assert_eq!(parts[0].len, 4); }
4778
4779 #[test]
4780 fn spanned_simple_var_then_literal() {
4781 let parts = parse_interpolated_string_spanned("$X end", 10);
4782 assert_eq!(parts.len(), 2);
4783 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4784 assert_eq!(parts[0].offset, 10);
4785 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
4787 assert_eq!(parts[1].offset, 12);
4788 assert_eq!(parts[1].len, 4);
4789 }
4790
4791 #[test]
4792 fn spanned_mixed_literal_var_literal() {
4793 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0);
4794 assert_eq!(parts.len(), 3);
4795 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
4797 assert_eq!(parts[0].offset, 0);
4798 assert_eq!(parts[0].len, 3);
4799 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4801 assert_eq!(parts[1].offset, 3);
4802 assert_eq!(parts[1].len, 4);
4803 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
4805 assert_eq!(parts[2].offset, 7);
4806 assert_eq!(parts[2].len, 4);
4807 }
4808
4809 #[test]
4810 fn spanned_positional_param() {
4811 let parts = parse_interpolated_string_spanned("$1 done", 0);
4812 assert_eq!(parts.len(), 2);
4813 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
4814 assert_eq!(parts[0].offset, 0);
4815 assert_eq!(parts[0].len, 2); }
4817
4818 #[test]
4819 fn spanned_special_dollar_dollar() {
4820 let parts = parse_interpolated_string_spanned("$$", 5);
4821 assert_eq!(parts.len(), 1);
4822 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
4823 assert_eq!(parts[0].offset, 5);
4824 assert_eq!(parts[0].len, 2);
4825 }
4826
4827 #[test]
4828 fn spanned_arithmetic_marker_recognised() {
4829 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0);
4833 assert_eq!(parts.len(), 1);
4834 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
4835 }
4836
4837 #[test]
4838 fn spanned_default_separator_yields_var_with_default() {
4839 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0);
4840 assert_eq!(parts.len(), 1);
4841 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
4842 assert_eq!(parts[0].offset, 0);
4843 assert_eq!(parts[0].len, 14); }
4845
4846 #[test]
4847 fn spanned_no_dollar_runs_one_literal() {
4848 let parts = parse_interpolated_string_spanned("plain text only", 7);
4849 assert_eq!(parts.len(), 1);
4850 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
4851 assert_eq!(parts[0].offset, 7);
4852 assert_eq!(parts[0].len, 15);
4853 }
4854
4855 #[test]
4856 fn spanned_matches_unspanned_part_count() {
4857 let cases = [
4860 "hello",
4861 "$X",
4862 "${X}",
4863 "${X:-d}",
4864 "hi $A and $B",
4865 "$0 $1 $2",
4866 "$$ $? $#",
4867 ];
4868 for s in &cases {
4869 let unspanned = parse_interpolated_string(s).expect("test input parses");
4870 let spanned = parse_interpolated_string_spanned(s, 0);
4871 assert_eq!(
4872 unspanned.len(),
4873 spanned.len(),
4874 "part count differs for {:?}",
4875 s
4876 );
4877 }
4878 }
4879
4880 #[test]
4881 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
4882 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0);
4887 assert_eq!(parts.len(), 2);
4888
4889 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
4890 assert_eq!(parts[0].offset, 0);
4891 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
4892
4893 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4894 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
4895 assert_eq!(parts[1].len, 4);
4896 }
4897
4898 #[test]
4899 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
4900 let parts = parse_interpolated_string_spanned("hello 世界 world", 0);
4903 assert_eq!(parts.len(), 1);
4904 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
4905 assert_eq!(parts[0].offset, 0);
4906 assert_eq!(parts[0].len, 18);
4907 }
4908
4909 #[test]
4910 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
4911 let parts = parse_interpolated_string_spanned("\\$", 0);
4914 assert_eq!(parts.len(), 1);
4915 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
4916 assert_eq!(parts[0].offset, 0);
4917 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
4918 }
4919
4920 #[test]
4921 fn spanned_escape_backslash_collapses_pair_to_one() {
4922 let parts = parse_interpolated_string_spanned("\\\\", 0);
4923 assert_eq!(parts.len(), 1);
4924 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
4925 assert_eq!(parts[0].len, 2);
4926 }
4927
4928 #[test]
4929 fn spanned_standalone_cr_continuation_realigns_span_start() {
4930 let parts = parse_interpolated_string_spanned("\\\rCD${x}", 0);
4938 assert_eq!(parts.len(), 2);
4939 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "CD"));
4940 assert_eq!(parts[0].offset, 2, "literal run must start after the consumed \\+CR");
4941 assert_eq!(parts[0].len, 2);
4942 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4943 assert_eq!(parts[1].offset, 4);
4944 assert_eq!(parts[1].len, 4); }
4946
4947 #[test]
4948 fn spanned_standalone_cr_continuation_mid_run_keeps_span_start() {
4949 let parts = parse_interpolated_string_spanned("AB\\\rCD${x}", 0);
4954 assert_eq!(parts.len(), 2);
4955 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "ABCD"));
4956 assert_eq!(parts[0].offset, 0);
4957 assert_eq!(parts[0].len, 6); assert!(matches!(&parts[1].part, StringPart::Var(_)));
4959 assert_eq!(parts[1].offset, 6);
4960 assert_eq!(parts[1].len, 4); }
4962
4963 fn assignment_value(source: &str) -> Expr {
4967 let program = parse(source).unwrap_or_else(|e| panic!("parse {source:?}: {e:?}"));
4968 match program.statements.as_slice() {
4969 [Stmt::Assignment(a)] => a.value.clone(),
4970 other => panic!("expected a single assignment, got {other:?}"),
4971 }
4972 }
4973
4974 #[test]
4975 fn list_literal_three_elements() {
4976 let expr = assignment_value("xs=[a b c]");
4977 match expr {
4978 Expr::ListLiteral(elems) => {
4979 assert_eq!(elems.len(), 3);
4980 assert!(elems.iter().all(|e| matches!(e, ListElem::Item(_))));
4981 }
4982 other => panic!("expected ListLiteral, got {other:?}"),
4983 }
4984 }
4985
4986 #[test]
4987 fn list_literal_empty() {
4988 let expr = assignment_value("xs=[]");
4989 assert!(matches!(expr, Expr::ListLiteral(elems) if elems.is_empty()));
4990 }
4991
4992 #[test]
4993 fn list_literal_single_glued_dog() {
4994 let expr = assignment_value("xs=[dog]");
4998 match expr {
4999 Expr::ListLiteral(elems) => assert_eq!(elems.len(), 1),
5000 other => panic!("expected ListLiteral, got {other:?}"),
5001 }
5002 }
5003
5004 #[test]
5005 fn list_literal_single_int() {
5006 let expr = assignment_value("xs=[1]");
5007 match expr {
5008 Expr::ListLiteral(elems) => match elems.as_slice() {
5009 [ListElem::Item(Expr::Literal(Value::Int(1)))] => {}
5010 other => panic!("expected one Int(1) item, got {other:?}"),
5011 },
5012 other => panic!("expected ListLiteral, got {other:?}"),
5013 }
5014 }
5015
5016 #[test]
5017 fn record_literal_unspaced_colon_equals_spaced() {
5018 let spaced = assignment_value("x={port: 8080}");
5019 let unspaced = assignment_value("x={port:8080}");
5020 assert_eq!(spaced, unspaced, "{{port:8080}} must parse identically to {{port: 8080}}");
5021 match spaced {
5022 Expr::RecordLiteral(entries) => match entries.as_slice() {
5023 [RecordEntry { key: RecordKey::Bare(k), value: Expr::Literal(Value::Int(8080)) }] => {
5024 assert_eq!(k, "port");
5025 }
5026 other => panic!("expected one port:8080 entry, got {other:?}"),
5027 },
5028 other => panic!("expected RecordLiteral, got {other:?}"),
5029 }
5030 }
5031
5032 #[test]
5033 fn record_literal_name_role() {
5034 let expr = assignment_value("u={name: amy, role: maintainer}");
5035 match expr {
5036 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2),
5037 other => panic!("expected RecordLiteral, got {other:?}"),
5038 }
5039 }
5040
5041 #[test]
5042 fn record_literal_multiline_trailing_comma() {
5043 let source = "services={\n web: {port: 8080, replicas: 3, healthy: true},\n api: {port: 9000, replicas: 2, healthy: false},\n}";
5044 let expr = assignment_value(source);
5045 match expr {
5046 Expr::RecordLiteral(entries) => assert_eq!(entries.len(), 2, "web + api entries"),
5047 other => panic!("expected RecordLiteral, got {other:?}"),
5048 }
5049 }
5050
5051 #[test]
5052 fn record_literal_quoted_key() {
5053 let expr = assignment_value(r#"r={"content-type": x}"#);
5054 match expr {
5055 Expr::RecordLiteral(entries) => match entries.as_slice() {
5056 [RecordEntry { key: RecordKey::Quoted(k), .. }] => assert_eq!(k, "content-type"),
5057 other => panic!("expected one quoted-key entry, got {other:?}"),
5058 },
5059 other => panic!("expected RecordLiteral, got {other:?}"),
5060 }
5061 }
5062
5063 #[test]
5064 fn nested_list_and_record_in_record() {
5065 let expr = assignment_value("x={tags: [a b], meta: {active: true}}");
5066 match expr {
5067 Expr::RecordLiteral(entries) => {
5068 assert_eq!(entries.len(), 2);
5069 assert!(matches!(entries[0].value, Expr::ListLiteral(_)));
5070 assert!(matches!(entries[1].value, Expr::RecordLiteral(_)));
5071 }
5072 other => panic!("expected RecordLiteral, got {other:?}"),
5073 }
5074 }
5075
5076 #[test]
5077 fn spread_and_item_elements() {
5078 let expr = assignment_value("new=[...$xs date]");
5079 match expr {
5080 Expr::ListLiteral(elems) => match elems.as_slice() {
5081 [ListElem::Spread(Expr::VarRef(_)), ListElem::Item(Expr::Literal(Value::String(s)))] => {
5082 assert_eq!(s, "date");
5083 }
5084 other => panic!("expected [Spread($xs), Item(date)], got {other:?}"),
5085 },
5086 other => panic!("expected ListLiteral, got {other:?}"),
5087 }
5088 }
5089
5090 #[test]
5091 fn spread_of_two_variables() {
5092 let expr = assignment_value("c=[...$a ...$b]");
5093 match expr {
5094 Expr::ListLiteral(elems) => {
5095 assert_eq!(elems.len(), 2);
5096 assert!(elems.iter().all(|e| matches!(e, ListElem::Spread(_))));
5097 }
5098 other => panic!("expected ListLiteral, got {other:?}"),
5099 }
5100 }
5101
5102 #[test]
5103 fn in_rhs_accepts_a_list_literal() {
5104 let program = parse("if [[ $a not in [dog] ]]; then echo hit; fi")
5105 .unwrap_or_else(|e| panic!("parse: {e:?}"));
5106 assert_eq!(program.statements.len(), 1);
5107 }
5108
5109 #[test]
5110 fn multiword_bareword_record_value_is_a_parse_error() {
5111 assert!(parse("x={msg: hello world}").is_err());
5114 }
5115
5116 #[test]
5119 fn argv_bracket_glob_stays_a_glob_pattern() {
5120 let program = parse("ls [dog]").unwrap_or_else(|e| panic!("parse: {e:?}"));
5124 assert_eq!(program.statements.len(), 1);
5125 }
5126
5127 #[test]
5128 fn brace_expansion_at_argv_position_is_unaffected() {
5129 let program = parse("cmd *.{rs,go}").unwrap_or_else(|e| panic!("parse: {e:?}"));
5134 assert_eq!(program.statements.len(), 1);
5135 }
5136
5137 #[test]
5138 fn for_head_item_is_not_a_literal() {
5139 let program = parse("for x in [a]; do echo $x; done")
5142 .unwrap_or_else(|e| panic!("parse: {e:?}"));
5143 match program.statements.as_slice() {
5144 [Stmt::For(for_loop)] => {
5145 assert_eq!(for_loop.items.len(), 1);
5146 assert!(
5147 !matches!(for_loop.items[0], Expr::ListLiteral(_)),
5148 "for-head item must not be a ListLiteral: {:?}",
5149 for_loop.items[0]
5150 );
5151 }
5152 other => panic!("expected a single For statement, got {other:?}"),
5153 }
5154 }
5155}