1use crate::ast::{
7 Arg, Assignment, BinaryOp, CaseBranch, CaseStmt, Command, Expr, FileTestOp, ForLoop, IfStmt,
8 Pipeline, Program, Redirect, RedirectKind, SpannedPart, Stmt, StringPart, StringTestOp,
9 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 name = raw[2..colon_idx].to_string();
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 { name, default };
49 }
50
51 Expr::VarRef(parse_varpath(raw))
53}
54
55fn unquote_default_word(word: &str) -> String {
65 let mut out = String::with_capacity(word.len());
66 let mut in_single = false;
67 let mut in_double = false;
68 for ch in word.chars() {
69 match ch {
70 '\'' if !in_double => in_single = !in_single,
73 '"' if !in_single => in_double = !in_double,
74 '$' if in_single => out.push_str("__KAISH_ESCAPED_DOLLAR__"),
76 _ => out.push(ch),
77 }
78 }
79 out
80}
81
82fn find_default_separator(raw: &str) -> Option<usize> {
84 let bytes = raw.as_bytes();
85 let mut depth = 0;
86 let mut i = 0;
87
88 while i < bytes.len() {
89 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
90 depth += 1;
91 i += 2;
92 continue;
93 }
94 if bytes[i] == b'}' && depth > 0 {
95 depth -= 1;
96 i += 1;
97 continue;
98 }
99 if depth == 1 && i + 1 < bytes.len() && bytes[i] == b':' && bytes[i + 1] == b'-' {
101 return Some(i);
102 }
103 i += 1;
104 }
105 None
106}
107
108fn find_default_separator_in_content(content: &str) -> Option<usize> {
110 let bytes = content.as_bytes();
111 let mut depth = 0;
112 let mut i = 0;
113
114 while i < bytes.len() {
115 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
116 depth += 1;
117 i += 2;
118 continue;
119 }
120 if bytes[i] == b'}' && depth > 0 {
121 depth -= 1;
122 i += 1;
123 continue;
124 }
125 if depth == 0 && i + 1 < bytes.len() && bytes[i] == b':' && bytes[i + 1] == b'-' {
127 return Some(i);
128 }
129 i += 1;
130 }
131 None
132}
133
134fn parse_varpath(raw: &str) -> VarPath {
138 let segments_strs = lexer::parse_var_ref(raw).unwrap_or_default();
139 let segments = segments_strs
140 .into_iter()
141 .filter(|s| !s.starts_with('[')) .map(VarSegment::Field)
143 .collect();
144 VarPath { segments }
145}
146
147fn strip_empty_stmts(statements: Vec<Stmt>) -> Vec<Stmt> {
150 statements
151 .into_iter()
152 .filter(|s| !matches!(s, Stmt::Empty))
153 .collect()
154}
155
156fn parse_interpolated_string_spanned(s: &str, base_offset: usize) -> Vec<SpannedPart> {
175 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
176
177 let chars_vec: Vec<char> = s.chars().collect();
178 let mut i = 0;
179 let mut pos: usize = 0;
180
181 let mut parts: Vec<SpannedPart> = Vec::new();
182 let mut current_text = String::new();
183 let mut current_text_start: usize = pos;
184
185 let push_literal =
186 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
187 if !current_text.is_empty() {
188 parts.push(SpannedPart {
189 part: StringPart::Literal(std::mem::take(current_text)),
190 offset: base_offset + *start,
191 len: end - *start,
192 });
193 *start = end;
194 }
195 };
196
197 while i < chars_vec.len() {
198 let ch = chars_vec[i];
199
200 if ch == '\x00' {
201 let start = pos;
203 i += 1;
204 pos += 1;
205 let mut marker = String::new();
206 while let Some(&c) = chars_vec.get(i) {
207 if c == '\x00' {
208 i += 1;
209 pos += 1;
210 break;
211 }
212 marker.push(c);
213 i += 1;
214 pos += c.len_utf8();
215 }
216 if marker == "DOLLAR" {
217 if current_text.is_empty() {
218 current_text_start = start;
219 }
220 current_text.push('$');
221 }
222 } else if ch == '\\' {
223 let next = chars_vec.get(i + 1).copied();
229 match next {
230 Some('$') => {
231 if current_text.is_empty() {
232 current_text_start = pos;
233 }
234 current_text.push('$');
235 i += 2;
236 pos += 2;
237 }
238 Some('\\') => {
239 if current_text.is_empty() {
240 current_text_start = pos;
241 }
242 current_text.push('\\');
243 i += 2;
244 pos += 2;
245 }
246 Some('\n') => {
247 i += 2;
250 pos += 2;
251 if current_text.is_empty() {
252 current_text_start = pos;
253 }
254 }
255 Some('\r') => {
256 i += 2;
258 pos += 2;
259 if chars_vec.get(i) == Some(&'\n') {
260 i += 1;
261 pos += 1;
262 }
263 if current_text.is_empty() {
264 current_text_start = pos;
265 }
266 }
267 _ => {
268 if current_text.is_empty() {
272 current_text_start = pos;
273 }
274 current_text.push('\\');
275 i += 1;
276 pos += 1;
277 }
278 }
279 } else if ch == '$' {
280 let part_start = pos;
282 let next = chars_vec.get(i + 1).copied();
283
284 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
285 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
287 i += 2; pos += 2;
289 let mut cmd_content = String::new();
290 let mut depth = 1;
291 while let Some(&c) = chars_vec.get(i) {
292 i += 1;
293 pos += c.len_utf8();
294 if c == '(' {
295 depth += 1;
296 cmd_content.push(c);
297 } else if c == ')' {
298 depth -= 1;
299 if depth == 0 {
300 break;
301 }
302 cmd_content.push(c);
303 } else {
304 cmd_content.push(c);
305 }
306 }
307 let inserted = if let Ok(program) = parse(&cmd_content) {
308 let stmts = strip_empty_stmts(program.statements);
311 if stmts.is_empty() {
312 false
313 } else {
314 parts.push(SpannedPart {
315 part: StringPart::CommandSubst(stmts),
316 offset: base_offset + part_start,
317 len: pos - part_start,
318 });
319 true
320 }
321 } else {
322 false
323 };
324 if inserted {
325 current_text_start = pos;
328 } else {
329 if current_text.is_empty() {
334 current_text_start = part_start;
335 }
336 current_text.push_str("$(");
337 current_text.push_str(&cmd_content);
338 current_text.push(')');
339 }
340 } else if next == Some('{') {
341 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
342 i += 2; pos += 2;
344 let mut var_content = String::new();
345 let mut depth = 1;
346 while let Some(&c) = chars_vec.get(i) {
347 i += 1;
348 pos += c.len_utf8();
349 if c == '{' && var_content.ends_with('$') {
350 depth += 1;
351 var_content.push(c);
352 } else if c == '}' {
353 depth -= 1;
354 if depth == 0 {
355 break;
356 }
357 var_content.push(c);
358 } else {
359 var_content.push(c);
360 }
361 }
362 let part = if let Some(name) = var_content.strip_prefix('#') {
363 StringPart::VarLength(name.to_string())
364 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
365 let expr = var_content
366 .strip_prefix("__ARITH:")
367 .and_then(|s| s.strip_suffix("__"))
368 .unwrap_or("");
369 StringPart::Arithmetic(expr.to_string())
370 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
371 let name = var_content[..colon_idx].to_string();
372 let default_str = &var_content[colon_idx + 2..];
373 let default_word = unquote_default_word(default_str);
378 let default = parse_interpolated_string(&default_word)
379 .unwrap_or_else(|_| vec![StringPart::Literal(default_word.clone())]);
380 StringPart::VarWithDefault { name, default }
381 } else {
382 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
383 };
384 parts.push(SpannedPart {
385 part,
386 offset: base_offset + part_start,
387 len: pos - part_start,
388 });
389 current_text_start = pos;
390 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
391 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
392 i += 1; pos += 1;
394 if let Some(&digit) = chars_vec.get(i) {
395 let n = digit.to_digit(10).unwrap_or(0) as usize;
396 i += 1;
397 pos += digit.len_utf8();
398 parts.push(SpannedPart {
399 part: StringPart::Positional(n),
400 offset: base_offset + part_start,
401 len: pos - part_start,
402 });
403 }
404 current_text_start = pos;
405 } else if next == Some('@') {
406 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
407 i += 2; pos += 2;
409 parts.push(SpannedPart {
410 part: StringPart::AllArgs,
411 offset: base_offset + part_start,
412 len: pos - part_start,
413 });
414 current_text_start = pos;
415 } else if next == Some('#') {
416 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
417 i += 2; pos += 2;
419 parts.push(SpannedPart {
420 part: StringPart::ArgCount,
421 offset: base_offset + part_start,
422 len: pos - part_start,
423 });
424 current_text_start = pos;
425 } else if next == Some('?') {
426 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
427 i += 2; pos += 2;
429 parts.push(SpannedPart {
430 part: StringPart::LastExitCode,
431 offset: base_offset + part_start,
432 len: pos - part_start,
433 });
434 current_text_start = pos;
435 } else if next == Some('$') {
436 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
437 i += 2; pos += 2;
439 parts.push(SpannedPart {
440 part: StringPart::CurrentPid,
441 offset: base_offset + part_start,
442 len: pos - part_start,
443 });
444 current_text_start = pos;
445 } else if next.map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
446 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
447 i += 1; pos += 1;
449 let mut var_name = String::new();
450 while let Some(&c) = chars_vec.get(i) {
451 if c.is_ascii_alphanumeric() || c == '_' {
452 var_name.push(c);
453 i += 1;
454 pos += c.len_utf8();
455 } else {
456 break;
457 }
458 }
459 parts.push(SpannedPart {
460 part: StringPart::Var(VarPath::simple(var_name)),
461 offset: base_offset + part_start,
462 len: pos - part_start,
463 });
464 current_text_start = pos;
465 } else {
466 if current_text.is_empty() {
468 current_text_start = pos;
469 }
470 current_text.push(ch);
471 i += 1;
472 pos += 1;
473 }
474 } else {
475 if current_text.is_empty() {
476 current_text_start = pos;
477 }
478 current_text.push(ch);
479 i += 1;
480 pos += ch.len_utf8();
481 }
482 }
483
484 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
485
486 parts
487}
488
489fn parse_interpolated_string(s: &str) -> Result<Vec<StringPart>, String> {
490 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
493
494 let mut parts = Vec::new();
495 let mut current_text = String::new();
496 let mut chars = s.chars().peekable();
497
498 while let Some(ch) = chars.next() {
499 if ch == '\x00' {
500 let mut marker = String::new();
502 while let Some(&c) = chars.peek() {
503 if c == '\x00' {
504 chars.next(); break;
506 }
507 if let Some(c) = chars.next() {
508 marker.push(c);
509 }
510 }
511 if marker == "DOLLAR" {
512 current_text.push('$');
513 }
514 } else if ch == '$' {
515 if chars.peek() == Some(&'(') {
517 if !current_text.is_empty() {
519 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
520 }
521
522 chars.next();
524
525 let mut cmd_content = String::new();
527 let mut paren_depth = 1;
528 for c in chars.by_ref() {
529 if c == '(' {
530 paren_depth += 1;
531 cmd_content.push(c);
532 } else if c == ')' {
533 paren_depth -= 1;
534 if paren_depth == 0 {
535 break;
536 }
537 cmd_content.push(c);
538 } else {
539 cmd_content.push(c);
540 }
541 }
542
543 match parse(&cmd_content) {
546 Ok(program) => {
547 let stmts = strip_empty_stmts(program.statements);
548 if stmts.is_empty() {
549 current_text.push_str("$(");
552 current_text.push_str(&cmd_content);
553 current_text.push(')');
554 } else {
555 parts.push(StringPart::CommandSubst(stmts));
556 }
557 }
558 Err(_) => {
559 return Err(format!(
563 "syntax error in command substitution: $({cmd_content})"
564 ));
565 }
566 }
567 } else if chars.peek() == Some(&'{') {
568 if !current_text.is_empty() {
570 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
571 }
572
573 chars.next();
575
576 let mut var_content = String::new();
578 let mut depth = 1;
579 for c in chars.by_ref() {
580 if c == '{' && var_content.ends_with('$') {
581 depth += 1;
582 var_content.push(c);
583 } else if c == '}' {
584 depth -= 1;
585 if depth == 0 {
586 break;
587 }
588 var_content.push(c);
589 } else {
590 var_content.push(c);
591 }
592 }
593
594 let part = if let Some(name) = var_content.strip_prefix('#') {
596 StringPart::VarLength(name.to_string())
598 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
599 let expr = var_content
601 .strip_prefix("__ARITH:")
602 .and_then(|s| s.strip_suffix("__"))
603 .unwrap_or("");
604 StringPart::Arithmetic(expr.to_string())
605 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
606 let name = var_content[..colon_idx].to_string();
608 let default_str = &var_content[colon_idx + 2..];
609 let default = parse_interpolated_string(&unquote_default_word(default_str))?;
610 StringPart::VarWithDefault { name, default }
611 } else {
612 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
614 };
615 parts.push(part);
616 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
617 if !current_text.is_empty() {
619 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
620 }
621 if let Some(digit) = chars.next() {
622 let n = digit.to_digit(10).unwrap_or(0) as usize;
623 parts.push(StringPart::Positional(n));
624 }
625 } else if chars.peek() == Some(&'@') {
626 if !current_text.is_empty() {
628 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
629 }
630 chars.next(); parts.push(StringPart::AllArgs);
632 } else if chars.peek() == Some(&'#') {
633 if !current_text.is_empty() {
635 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
636 }
637 chars.next(); parts.push(StringPart::ArgCount);
639 } else if chars.peek() == Some(&'?') {
640 if !current_text.is_empty() {
642 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
643 }
644 chars.next(); parts.push(StringPart::LastExitCode);
646 } else if chars.peek() == Some(&'$') {
647 if !current_text.is_empty() {
649 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
650 }
651 chars.next(); parts.push(StringPart::CurrentPid);
653 } else if chars.peek().map(|c| c.is_ascii_alphabetic() || *c == '_').unwrap_or(false) {
654 if !current_text.is_empty() {
656 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
657 }
658
659 let mut var_name = String::new();
661 while let Some(&c) = chars.peek() {
662 if c.is_ascii_alphanumeric() || c == '_' {
663 if let Some(c) = chars.next() {
664 var_name.push(c);
665 }
666 } else {
667 break;
668 }
669 }
670
671 parts.push(StringPart::Var(VarPath::simple(var_name)));
672 } else {
673 current_text.push(ch);
675 }
676 } else {
677 current_text.push(ch);
678 }
679 }
680
681 if !current_text.is_empty() {
682 parts.push(StringPart::Literal(current_text));
683 }
684
685 Ok(parts)
686}
687
688#[derive(Debug, Clone)]
690pub struct ParseError {
691 pub span: Span,
692 pub message: String,
693}
694
695impl ParseError {
696 pub fn format(&self, source: &str) -> String {
701 let start = self.span.start;
702 let mut line = 1usize;
703 let mut col = 1usize;
704 for (i, ch) in source.char_indices() {
705 if i >= start {
706 break;
707 }
708 if ch == '\n' {
709 line += 1;
710 col = 1;
711 } else {
712 col += 1;
713 }
714 }
715 let line_content = {
716 let line_start = source[..start.min(source.len())]
717 .rfind('\n')
718 .map_or(0, |i| i + 1);
719 let line_end = source[start.min(source.len())..]
720 .find('\n')
721 .map_or(source.len(), |i| start + i);
722 source.get(line_start..line_end).unwrap_or("")
723 };
724 if line_content.is_empty() {
725 format!("{}:{} [parse]: {}", line, col, self.message)
726 } else {
727 format!(
728 "{}:{} [parse]: {}\n | {}",
729 line, col, self.message, line_content
730 )
731 }
732 }
733}
734
735impl std::fmt::Display for ParseError {
736 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737 write!(f, "{} at {:?}", self.message, self.span)
738 }
739}
740
741impl std::error::Error for ParseError {}
742
743pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
745 let tokens = lexer::tokenize(source).map_err(|errs| {
747 errs.into_iter()
748 .map(|e| ParseError {
749 span: (e.span.start..e.span.end).into(),
750 message: format!("lexer error: {}", e.token),
751 })
752 .collect::<Vec<_>>()
753 })?;
754
755 let tokens: Vec<(Token, Span)> = tokens
757 .into_iter()
758 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
759 .collect();
760
761 let end_span: Span = (source.len()..source.len()).into();
763
764 let parser = program_parser();
766 let result = parser.parse(tokens.as_slice().map(end_span, |(t, s)| (t, s)));
767
768 let program = result.into_result().map_err(|errs| {
769 errs.into_iter()
770 .map(|e| ParseError {
771 span: *e.span(),
772 message: e.to_string(),
773 })
774 .collect::<Vec<_>>()
775 })?;
776
777 if first_ambiguous_stdin(&program.statements) {
782 return Err(vec![ParseError {
783 span: (0..0).into(),
787 message: "multiple stdin redirects on one command are ambiguous; \
788 use exactly one of `<`, `<<`, or `<<<`"
789 .to_string(),
790 }]);
791 }
792
793 Ok(program)
794}
795
796pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
798 let program = parse(source)?;
799 program
800 .statements
801 .into_iter()
802 .find(|s| !matches!(s, Stmt::Empty))
803 .ok_or_else(|| {
804 vec![ParseError {
805 span: (0..source.len()).into(),
806 message: "empty input".to_string(),
807 }]
808 })
809}
810
811fn program_parser<'tokens, 'src: 'tokens, I>(
817) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
818where
819 I: ValueInput<'tokens, Token = Token, Span = Span>,
820{
821 statement_parser()
822 .repeated()
823 .collect::<Vec<_>>()
824 .map(|statements| Program { statements })
825}
826
827fn statement_parser<'tokens, I>(
830) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
831where
832 I: ValueInput<'tokens, Token = Token, Span = Span>,
833{
834 recursive(|stmt| {
835 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
836
837 let break_stmt = just(Token::Break)
839 .ignore_then(
840 select! { Token::Int(n) => n as usize }.or_not()
841 )
842 .map(Stmt::Break);
843
844 let continue_stmt = just(Token::Continue)
846 .ignore_then(
847 select! { Token::Int(n) => n as usize }.or_not()
848 )
849 .map(Stmt::Continue);
850
851 let return_stmt = just(Token::Return)
853 .ignore_then(primary_expr_parser().or_not())
854 .map(|e| Stmt::Return(e.map(Box::new)));
855
856 let exit_stmt = just(Token::Exit)
858 .ignore_then(primary_expr_parser().or_not())
859 .map(|e| Stmt::Exit(e.map(Box::new)));
860
861 let set_flag_arg = choice((
870 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
871 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
872 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
874 ));
875
876 let option_value_str = select! {
880 Token::NumberIdent(s) => s,
881 Token::Int(n) => n.to_string(),
882 Token::Ident(s) => s,
883 };
884
885 let set_option_assign = ident_parser()
889 .then_ignore(just(Token::Eq))
890 .then(option_value_str)
891 .map(|(name, value)| {
892 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
893 });
894
895 let set_quoted_arg = select! {
899 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
900 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
901 };
902
903 let set_with_flags = just(Token::Set)
905 .then(set_flag_arg)
906 .then(
907 choice((
908 set_flag_arg,
909 set_option_assign,
911 set_quoted_arg,
912 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
914 ))
915 .repeated()
916 .collect::<Vec<_>>(),
917 )
918 .map(|((_, first_arg), mut rest_args)| {
919 let mut args = vec![first_arg];
920 args.append(&mut rest_args);
921 Stmt::Command(Command {
922 name: "set".to_string(),
923 args,
924 redirects: vec![],
925 })
926 });
927
928 let set_no_args = just(Token::Set)
931 .then(
932 choice((
933 just(Token::Newline).to(()),
934 just(Token::Semi).to(()),
935 just(Token::And).to(()),
936 just(Token::Or).to(()),
937 end(),
938 ))
939 .rewind(),
940 )
941 .map(|_| Stmt::Command(Command {
942 name: "set".to_string(),
943 args: vec![],
944 redirects: vec![],
945 }));
946
947 let set_command = set_with_flags.or(set_no_args);
951
952 let env_prefix_assign = ident_parser()
959 .then_ignore(just(Token::Eq))
960 .then(expr_parser())
961 .map(|(name, value)| Assignment { name, value, local: false });
962 let env_scoped = env_prefix_assign
963 .repeated()
964 .at_least(1)
965 .collect::<Vec<_>>()
966 .then(pipeline_parser().map(pipeline_into_stmt))
967 .map(|(assignments, body)| Stmt::EnvScoped {
968 assignments,
969 body: Box::new(body),
970 });
971
972 let base_statement = choice((
974 just(Token::Newline).to(Stmt::Empty),
975 set_command,
976 env_scoped,
977 assignment_parser().map(Stmt::Assignment),
978 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), if_parser(stmt.clone()).map(Stmt::If),
982 for_parser(stmt.clone()).map(Stmt::For),
983 while_parser(stmt.clone()).map(Stmt::While),
984 case_parser(stmt.clone()).map(Stmt::Case),
985 break_stmt,
986 continue_stmt,
987 return_stmt,
988 exit_stmt,
989 test_expr_stmt_parser().map(Stmt::Test),
990 pipeline_parser().map(pipeline_into_stmt),
992 ))
993 .boxed();
994
995 base_statement
1001 .clone()
1002 .foldl(
1003 choice((
1004 just(Token::And).to(true), just(Token::Or).to(false), ))
1007 .then(base_statement)
1008 .repeated(),
1009 |left, (is_and, right): (bool, Stmt)| {
1010 if is_and {
1011 Stmt::AndChain {
1012 left: Box::new(left),
1013 right: Box::new(right),
1014 }
1015 } else {
1016 Stmt::OrChain {
1017 left: Box::new(left),
1018 right: Box::new(right),
1019 }
1020 }
1021 },
1022 )
1023 .then_ignore(terminator)
1024 })
1025}
1026
1027fn assignment_parser<'tokens, I>(
1029) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1030where
1031 I: ValueInput<'tokens, Token = Token, Span = Span>,
1032{
1033 let local_assignment = just(Token::Local)
1035 .ignore_then(ident_parser())
1036 .then_ignore(just(Token::Eq))
1037 .then(expr_parser())
1038 .map(|(name, value)| Assignment {
1039 name,
1040 value,
1041 local: true,
1042 });
1043
1044 let bash_assignment = ident_parser()
1047 .then_ignore(just(Token::Eq))
1048 .then(expr_parser())
1049 .map(|(name, value)| Assignment {
1050 name,
1051 value,
1052 local: false,
1053 });
1054
1055 choice((local_assignment, bash_assignment))
1056 .labelled("assignment")
1057 .boxed()
1058}
1059
1060fn posix_function_parser<'tokens, I, S>(
1064 stmt: S,
1065) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1066where
1067 I: ValueInput<'tokens, Token = Token, Span = Span>,
1068 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1069{
1070 ident_parser()
1071 .then_ignore(just(Token::LParen))
1072 .then_ignore(just(Token::RParen))
1073 .then_ignore(just(Token::LBrace))
1074 .then_ignore(just(Token::Newline).repeated())
1075 .then(
1076 stmt.repeated()
1077 .collect::<Vec<_>>()
1078 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1079 )
1080 .then_ignore(just(Token::Newline).repeated())
1081 .then_ignore(just(Token::RBrace))
1082 .map(|(name, body)| ToolDef { name, params: vec![], body })
1083 .labelled("POSIX function")
1084 .boxed()
1085}
1086
1087fn bash_function_parser<'tokens, I, S>(
1091 stmt: S,
1092) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1093where
1094 I: ValueInput<'tokens, Token = Token, Span = Span>,
1095 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1096{
1097 just(Token::Function)
1098 .ignore_then(ident_parser())
1099 .then_ignore(just(Token::LBrace))
1100 .then_ignore(just(Token::Newline).repeated())
1101 .then(
1102 stmt.repeated()
1103 .collect::<Vec<_>>()
1104 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1105 )
1106 .then_ignore(just(Token::Newline).repeated())
1107 .then_ignore(just(Token::RBrace))
1108 .map(|(name, body)| ToolDef { name, params: vec![], body })
1109 .labelled("bash function")
1110 .boxed()
1111}
1112
1113fn if_parser<'tokens, I, S>(
1120 stmt: S,
1121) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1122where
1123 I: ValueInput<'tokens, Token = Token, Span = Span>,
1124 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1125{
1126 let branch = condition_parser()
1128 .then_ignore(just(Token::Semi).or_not())
1129 .then_ignore(just(Token::Newline).repeated())
1130 .then_ignore(just(Token::Then))
1131 .then_ignore(just(Token::Newline).repeated())
1132 .then(
1133 stmt.clone()
1134 .repeated()
1135 .collect::<Vec<_>>()
1136 .map(|stmts: Vec<Stmt>| {
1137 stmts
1138 .into_iter()
1139 .filter(|s| !matches!(s, Stmt::Empty))
1140 .collect::<Vec<_>>()
1141 }),
1142 );
1143
1144 let elif_branch = just(Token::Elif)
1146 .ignore_then(condition_parser())
1147 .then_ignore(just(Token::Semi).or_not())
1148 .then_ignore(just(Token::Newline).repeated())
1149 .then_ignore(just(Token::Then))
1150 .then_ignore(just(Token::Newline).repeated())
1151 .then(
1152 stmt.clone()
1153 .repeated()
1154 .collect::<Vec<_>>()
1155 .map(|stmts: Vec<Stmt>| {
1156 stmts
1157 .into_iter()
1158 .filter(|s| !matches!(s, Stmt::Empty))
1159 .collect::<Vec<_>>()
1160 }),
1161 );
1162
1163 let else_branch = just(Token::Else)
1165 .ignore_then(just(Token::Newline).repeated())
1166 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1167 .map(|stmts: Vec<Stmt>| {
1168 stmts
1169 .into_iter()
1170 .filter(|s| !matches!(s, Stmt::Empty))
1171 .collect::<Vec<_>>()
1172 });
1173
1174 just(Token::If)
1175 .ignore_then(branch)
1176 .then(elif_branch.repeated().collect::<Vec<_>>())
1177 .then(else_branch.or_not())
1178 .then_ignore(just(Token::Fi))
1179 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1180 build_if_chain(condition, then_branch, elif_branches, else_branch)
1182 })
1183 .labelled("if statement")
1184 .boxed()
1185}
1186
1187fn build_if_chain(
1194 condition: Expr,
1195 then_branch: Vec<Stmt>,
1196 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1197 else_branch: Option<Vec<Stmt>>,
1198) -> IfStmt {
1199 if elif_branches.is_empty() {
1200 IfStmt {
1202 condition: Box::new(condition),
1203 then_branch,
1204 else_branch,
1205 }
1206 } else {
1207 let (elif_cond, elif_then) = elif_branches.remove(0);
1209 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1210 IfStmt {
1211 condition: Box::new(condition),
1212 then_branch,
1213 else_branch: Some(vec![Stmt::If(nested_if)]),
1214 }
1215 }
1216}
1217
1218fn for_parser<'tokens, I, S>(
1220 stmt: S,
1221) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1222where
1223 I: ValueInput<'tokens, Token = Token, Span = Span>,
1224 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1225{
1226 just(Token::For)
1227 .ignore_then(ident_parser())
1228 .then_ignore(just(Token::In))
1229 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1230 .then_ignore(just(Token::Semi).or_not())
1231 .then_ignore(just(Token::Newline).repeated())
1232 .then_ignore(just(Token::Do))
1233 .then_ignore(just(Token::Newline).repeated())
1234 .then(
1235 stmt.repeated()
1236 .collect::<Vec<_>>()
1237 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1238 )
1239 .then_ignore(just(Token::Done))
1240 .map(|((variable, items), body)| ForLoop {
1241 variable,
1242 items,
1243 body,
1244 })
1245 .labelled("for loop")
1246 .boxed()
1247}
1248
1249fn while_parser<'tokens, I, S>(
1251 stmt: S,
1252) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1253where
1254 I: ValueInput<'tokens, Token = Token, Span = Span>,
1255 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1256{
1257 just(Token::While)
1258 .ignore_then(condition_parser())
1259 .then_ignore(just(Token::Semi).or_not())
1260 .then_ignore(just(Token::Newline).repeated())
1261 .then_ignore(just(Token::Do))
1262 .then_ignore(just(Token::Newline).repeated())
1263 .then(
1264 stmt.repeated()
1265 .collect::<Vec<_>>()
1266 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1267 )
1268 .then_ignore(just(Token::Done))
1269 .map(|(condition, body)| WhileLoop {
1270 condition: Box::new(condition),
1271 body,
1272 })
1273 .labelled("while loop")
1274 .boxed()
1275}
1276
1277fn case_parser<'tokens, I, S>(
1284 stmt: S,
1285) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1286where
1287 I: ValueInput<'tokens, Token = Token, Span = Span>,
1288 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1289{
1290 let pattern_part = choice((
1293 select! { Token::GlobWord(s) => s },
1294 select! { Token::Ident(s) => s },
1295 select! { Token::NumberIdent(s) => s },
1296 select! { Token::DashNumWord(s) => s },
1297 select! { Token::AtWord(s) => s },
1298 select! { Token::DottedIdent(s) => s },
1299 select! { Token::String(s) => s },
1300 select! { Token::SingleString(s) => s },
1301 select! { Token::Int(n) => n.to_string() },
1302 select! { Token::Star => "*".to_string() },
1303 select! { Token::Question => "?".to_string() },
1304 select! { Token::Dot => ".".to_string() },
1305 select! { Token::DotDot => "..".to_string() },
1306 select! { Token::Tilde => "~".to_string() },
1307 select! { Token::TildePath(s) => s },
1308 select! { Token::RelativePath(s) => s },
1309 select! { Token::DotSlashPath(s) => s },
1310 select! { Token::Path(p) => p },
1311 select! { Token::VarRef(v) => v },
1312 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1313 just(Token::LBracket)
1315 .ignore_then(
1316 choice((
1317 select! { Token::Ident(s) => s },
1318 select! { Token::Int(n) => n.to_string() },
1319 just(Token::Colon).to(":".to_string()),
1320 just(Token::Bang).to("!".to_string()),
1322 select! { Token::ShortFlag(s) => format!("-{}", s) },
1324 ))
1325 .repeated()
1326 .at_least(1)
1327 .collect::<Vec<String>>()
1328 )
1329 .then_ignore(just(Token::RBracket))
1330 .map(|parts| format!("[{}]", parts.join(""))),
1331 just(Token::LBrace)
1333 .ignore_then(
1334 choice((
1335 select! { Token::Ident(s) => s },
1336 select! { Token::Int(n) => n.to_string() },
1337 ))
1338 .separated_by(just(Token::Comma))
1339 .at_least(1)
1340 .collect::<Vec<String>>()
1341 )
1342 .then_ignore(just(Token::RBrace))
1343 .map(|parts| format!("{{{}}}", parts.join(","))),
1344 ));
1345
1346 let pattern = pattern_part
1349 .repeated()
1350 .at_least(1)
1351 .collect::<Vec<String>>()
1352 .map(|parts| parts.join(""))
1353 .labelled("case pattern");
1354
1355 let patterns = pattern
1357 .separated_by(just(Token::Pipe))
1358 .at_least(1)
1359 .collect::<Vec<String>>()
1360 .labelled("case patterns");
1361
1362 let branch = just(Token::LParen)
1364 .or_not()
1365 .ignore_then(just(Token::Newline).repeated())
1366 .ignore_then(patterns)
1367 .then_ignore(just(Token::RParen))
1368 .then_ignore(just(Token::Newline).repeated())
1369 .then(
1370 stmt.clone()
1371 .repeated()
1372 .collect::<Vec<_>>()
1373 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1374 )
1375 .then_ignore(just(Token::DoubleSemi))
1376 .then_ignore(just(Token::Newline).repeated())
1377 .map(|(patterns, body)| CaseBranch { patterns, body })
1378 .labelled("case branch");
1379
1380 just(Token::Case)
1381 .ignore_then(expr_parser())
1382 .then_ignore(just(Token::In))
1383 .then_ignore(just(Token::Newline).repeated())
1384 .then(branch.repeated().collect::<Vec<_>>())
1385 .then_ignore(just(Token::Esac))
1386 .map(|(expr, branches)| CaseStmt { expr, branches })
1387 .labelled("case statement")
1388 .boxed()
1389}
1390
1391fn pipeline_parser<'tokens, I>(
1393) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1394where
1395 I: ValueInput<'tokens, Token = Token, Span = Span>,
1396{
1397 command_parser()
1398 .separated_by(just(Token::Pipe))
1399 .at_least(1)
1400 .collect::<Vec<_>>()
1401 .then(just(Token::Amp).or_not())
1402 .map(|(commands, bg)| Pipeline {
1403 commands,
1404 background: bg.is_some(),
1405 })
1406 .labelled("pipeline")
1407 .boxed()
1408}
1409
1410fn command_parser<'tokens, I>(
1413) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1414where
1415 I: ValueInput<'tokens, Token = Token, Span = Span>,
1416{
1417 let command_name = choice((
1419 ident_parser(),
1420 path_parser(),
1421 select! { Token::DotSlashPath(s) => s },
1422 just(Token::True).to("true".to_string()),
1423 just(Token::False).to("false".to_string()),
1424 just(Token::Dot).to(".".to_string()),
1425 ));
1426
1427 command_name
1437 .then(args_list_parser())
1438 .then(redirect_parser().repeated().collect::<Vec<_>>())
1439 .map(|((name, args), redirects)| Command {
1440 name,
1441 args,
1442 redirects,
1443 })
1444 .labelled("command")
1445 .boxed()
1446}
1447
1448fn pipeline_into_stmt(p: Pipeline) -> Stmt {
1453 if p.commands.len() == 1 && !p.background && p.commands[0].redirects.is_empty() {
1454 match p.commands.into_iter().next() {
1455 Some(cmd) => Stmt::Command(cmd),
1456 None => Stmt::Empty, }
1458 } else {
1459 Stmt::Pipeline(p)
1460 }
1461}
1462
1463fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
1467 cmd.redirects
1468 .iter()
1469 .filter(|r| {
1470 matches!(
1471 r.kind,
1472 RedirectKind::Stdin | RedirectKind::HereDoc | RedirectKind::HereString
1473 )
1474 })
1475 .count()
1476 > 1
1477}
1478
1479fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
1483 stmts.iter().any(stmt_has_ambiguous_stdin)
1484}
1485
1486fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
1487 match stmt {
1488 Stmt::Command(c) => command_has_ambiguous_stdin(c),
1489 Stmt::Pipeline(p) => p.commands.iter().any(command_has_ambiguous_stdin),
1490 Stmt::If(i) => {
1491 first_ambiguous_stdin(&i.then_branch)
1492 || i.else_branch
1493 .as_deref()
1494 .is_some_and(first_ambiguous_stdin)
1495 }
1496 Stmt::For(f) => first_ambiguous_stdin(&f.body),
1497 Stmt::While(w) => first_ambiguous_stdin(&w.body),
1498 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
1499 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
1500 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
1501 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
1502 }
1503 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
1504 Stmt::Assignment(_)
1505 | Stmt::Break(_)
1506 | Stmt::Continue(_)
1507 | Stmt::Return(_)
1508 | Stmt::Exit(_)
1509 | Stmt::Test(_)
1510 | Stmt::Empty => false,
1511 }
1512}
1513
1514fn is_comma_literal_arg(arg: &Arg) -> bool {
1517 matches!(arg, Arg::Positional(Expr::Literal(Value::String(s))) if s == ",")
1518}
1519
1520fn args_list_parser<'tokens, I>(
1524) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1525where
1526 I: ValueInput<'tokens, Token = Token, Span = Span>,
1527{
1528 let pre_dash = arg_before_double_dash_parser()
1535 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1536 .repeated()
1537 .collect::<Vec<(Arg, Span)>>()
1538 .try_map(|args, _span| {
1539 for pair in args.windows(2) {
1540 let (prev, prev_span) = &pair[0];
1541 let (next, next_span) = &pair[1];
1542 if matches!(prev, Arg::Positional(_))
1543 && matches!(next, Arg::Positional(_))
1544 && prev_span.end == next_span.start
1545 {
1546 let msg = if is_comma_literal_arg(prev) || is_comma_literal_arg(next) {
1551 "an unquoted comma splits this into separate words — kaish reserves \
1552 `,` (brace expansion, lists); quote a comma-bearing argument to keep \
1553 it one word, e.g. cut -f \"1,3\", sort -k \"2,2n\", or echo \"a,b\""
1554 } else {
1555 "adjacent words with no space between them are not joined into one \
1556 argument (kaish does no token pasting); quote the whole word, e.g. \
1557 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\""
1558 };
1559 return Err(Rich::custom(*next_span, msg));
1560 }
1561 }
1562 Ok(args.into_iter().map(|(arg, _)| arg).collect::<Vec<Arg>>())
1563 });
1564
1565 let double_dash = select! {
1567 Token::DoubleDash => Arg::DoubleDash,
1568 };
1569
1570 let post_dash_arg = choice((
1572 select! {
1574 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
1575 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
1576 },
1577 primary_expr_parser().map(Arg::Positional),
1579 ));
1580
1581 let post_dash = post_dash_arg.repeated().collect::<Vec<_>>();
1582
1583 pre_dash
1585 .then(double_dash.then(post_dash).or_not())
1586 .map(|(mut args, maybe_dd)| {
1587 if let Some((dd, post)) = maybe_dd {
1588 args.push(dd);
1589 args.extend(post);
1590 }
1591 args
1592 })
1593}
1594
1595fn keyword_word<'tokens, I>(
1604) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1605where
1606 I: ValueInput<'tokens, Token = Token, Span = Span>,
1607{
1608 select! {
1609 Token::Set => "set",
1610 Token::Local => "local",
1611 Token::If => "if",
1612 Token::Then => "then",
1613 Token::Else => "else",
1614 Token::Elif => "elif",
1615 Token::Fi => "fi",
1616 Token::For => "for",
1617 Token::While => "while",
1618 Token::In => "in",
1619 Token::Do => "do",
1620 Token::Done => "done",
1621 Token::Case => "case",
1622 Token::Esac => "esac",
1623 Token::Function => "function",
1624 Token::Break => "break",
1625 Token::Continue => "continue",
1626 Token::Return => "return",
1627 Token::Exit => "exit",
1628 }
1629 .map(|s| s.to_string())
1630}
1631
1632fn arg_before_double_dash_parser<'tokens, I>(
1634) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1635where
1636 I: ValueInput<'tokens, Token = Token, Span = Span>,
1637{
1638 let long_flag_with_value = select! {
1640 Token::LongFlag(name) => name,
1641 }
1642 .then_ignore(just(Token::Eq))
1643 .then(primary_expr_parser())
1644 .map(|(key, value)| Arg::Named { key, value });
1645
1646 let long_flag = select! {
1648 Token::LongFlag(name) => Arg::LongFlag(name),
1649 };
1650
1651 let short_flag = select! {
1653 Token::ShortFlag(name) => Arg::ShortFlag(name),
1654 };
1655
1656 let named = choice((
1662 select! { Token::Ident(s) => s },
1663 keyword_word(),
1664 ))
1665 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1666 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1667 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1668 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1669 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1671 Err(Rich::custom(
1672 span,
1673 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
1674 ))
1675 } else {
1676 Ok(Arg::WordAssign { key, value })
1677 }
1678 });
1679
1680 let positional = primary_expr_parser().map(Arg::Positional);
1682
1683 choice((
1686 long_flag_with_value,
1687 long_flag,
1688 short_flag,
1689 named,
1690 positional,
1691 ))
1692 .boxed()
1693}
1694
1695fn redirect_parser<'tokens, I>(
1697) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1698where
1699 I: ValueInput<'tokens, Token = Token, Span = Span>,
1700{
1701 let regular_redirect = select! {
1703 Token::GtGt => RedirectKind::StdoutAppend,
1704 Token::Gt => RedirectKind::StdoutOverwrite,
1705 Token::Lt => RedirectKind::Stdin,
1706 Token::Stderr => RedirectKind::Stderr,
1707 Token::Both => RedirectKind::Both,
1708 }
1709 .then(primary_expr_parser())
1710 .map(|(kind, target)| Redirect { kind, target });
1711
1712 let heredoc_redirect = just(Token::HereDocStart)
1720 .ignore_then(select! { Token::HereDoc(data) => data })
1721 .map(|data: HereDocData| {
1722 let target = if data.literal {
1723 let body = if data.strip_tabs {
1724 crate::interpreter::strip_leading_tabs(&data.content)
1725 } else {
1726 data.content
1727 };
1728 Expr::Literal(Value::String(body))
1729 } else {
1730 let parts = parse_interpolated_string_spanned(
1731 &data.content,
1732 data.body_start_offset,
1733 );
1734 if parts.len() == 1 && !data.strip_tabs {
1738 if let StringPart::Literal(text) = &parts[0].part {
1739 return Redirect {
1740 kind: RedirectKind::HereDoc,
1741 target: Expr::Literal(Value::String(text.clone())),
1742 };
1743 }
1744 }
1745 Expr::HereDocBody {
1746 parts,
1747 strip_tabs: data.strip_tabs,
1748 }
1749 };
1750 Redirect {
1751 kind: RedirectKind::HereDoc,
1752 target,
1753 }
1754 });
1755
1756 let herestring_redirect = just(Token::HereString)
1760 .ignore_then(primary_expr_parser())
1761 .map(|target| Redirect {
1762 kind: RedirectKind::HereString,
1763 target,
1764 });
1765
1766 let merge_stderr_redirect = just(Token::StderrToStdout)
1768 .map(|_| Redirect {
1769 kind: RedirectKind::MergeStderr,
1770 target: Expr::Literal(Value::Null),
1772 });
1773
1774 let merge_stdout_redirect = choice((
1776 just(Token::StdoutToStderr),
1777 just(Token::StdoutToStderr2),
1778 ))
1779 .map(|_| Redirect {
1780 kind: RedirectKind::MergeStdout,
1781 target: Expr::Literal(Value::Null),
1783 });
1784
1785 choice((
1786 heredoc_redirect,
1787 herestring_redirect,
1788 merge_stderr_redirect,
1789 merge_stdout_redirect,
1790 regular_redirect,
1791 ))
1792 .labelled("redirect")
1793 .boxed()
1794}
1795
1796fn test_expr_stmt_parser<'tokens, I>(
1806) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1807where
1808 I: ValueInput<'tokens, Token = Token, Span = Span>,
1809{
1810 let file_test_op = select! {
1812 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
1813 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
1814 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
1815 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
1816 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
1817 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
1818 };
1819
1820 let string_test_op = select! {
1822 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
1823 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
1824 };
1825
1826 let cmp_op = choice((
1829 just(Token::EqEq).to(TestCmpOp::Eq),
1830 just(Token::Eq).to(TestCmpOp::Eq),
1831 just(Token::NotEq).to(TestCmpOp::NotEq),
1832 just(Token::Match).to(TestCmpOp::Match),
1833 just(Token::NotMatch).to(TestCmpOp::NotMatch),
1834 just(Token::Gt).to(TestCmpOp::Gt),
1835 just(Token::Lt).to(TestCmpOp::Lt),
1836 just(Token::GtEq).to(TestCmpOp::GtEq),
1837 just(Token::LtEq).to(TestCmpOp::LtEq),
1838 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
1839 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
1840 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
1841 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
1842 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
1843 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
1844 ));
1845
1846 let file_test = file_test_op
1848 .then(primary_expr_parser())
1849 .map(|(op, path)| TestExpr::FileTest {
1850 op,
1851 path: Box::new(path),
1852 });
1853
1854 let string_test = string_test_op
1856 .then(primary_expr_parser())
1857 .map(|(op, value)| TestExpr::StringTest {
1858 op,
1859 value: Box::new(value),
1860 });
1861
1862 let comparison = primary_expr_parser()
1864 .then(cmp_op)
1865 .then(primary_expr_parser())
1866 .map(|((left, op), right)| TestExpr::Comparison {
1867 left: Box::new(left),
1868 op,
1869 right: Box::new(right),
1870 });
1871
1872 let primary_test = choice((file_test, string_test, comparison));
1874
1875 let unary = recursive(|unary| {
1889 let not_expr = just(Token::Bang)
1890 .ignore_then(unary)
1891 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
1892 choice((not_expr, primary_test.clone()))
1893 });
1894
1895 let and_expr = unary.clone().foldl(
1897 just(Token::And).ignore_then(unary).repeated(),
1898 |left, right| TestExpr::And {
1899 left: Box::new(left),
1900 right: Box::new(right),
1901 },
1902 );
1903
1904 let compound_test = and_expr.clone().foldl(
1906 just(Token::Or).ignore_then(and_expr).repeated(),
1907 |left, right| TestExpr::Or {
1908 left: Box::new(left),
1909 right: Box::new(right),
1910 },
1911 );
1912
1913 just(Token::LBracket)
1916 .then(just(Token::LBracket))
1917 .ignore_then(compound_test)
1918 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
1919 .labelled("test expression")
1920 .boxed()
1921}
1922
1923fn condition_parser<'tokens, I>(
1938) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1939where
1940 I: ValueInput<'tokens, Token = Token, Span = Span>,
1941{
1942 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
1944
1945 let command_condition = command_parser().map(Expr::Command);
1948
1949 let base = choice((test_expr_condition, command_condition));
1951
1952 let and_expr = base.clone().foldl(
1955 just(Token::And).ignore_then(base).repeated(),
1956 |left, right| Expr::BinaryOp {
1957 left: Box::new(left),
1958 op: BinaryOp::And,
1959 right: Box::new(right),
1960 },
1961 );
1962
1963 and_expr
1965 .clone()
1966 .foldl(
1967 just(Token::Or).ignore_then(and_expr).repeated(),
1968 |left, right| Expr::BinaryOp {
1969 left: Box::new(left),
1970 op: BinaryOp::Or,
1971 right: Box::new(right),
1972 },
1973 )
1974 .labelled("condition")
1975 .boxed()
1976}
1977
1978fn expr_parser<'tokens, I>(
1980) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1981where
1982 I: ValueInput<'tokens, Token = Token, Span = Span>,
1983{
1984 primary_expr_parser()
1986}
1987
1988fn primary_expr_parser<'tokens, I>(
1992) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1993where
1994 I: ValueInput<'tokens, Token = Token, Span = Span>,
1995{
1996 let positional = select! {
1998 Token::Positional(n) => Expr::Positional(n),
1999 Token::AllArgs => Expr::AllArgs,
2000 Token::ArgCount => Expr::ArgCount,
2001 Token::VarLength(name) => Expr::VarLength(name),
2002 Token::LastExitCode => Expr::LastExitCode,
2003 Token::CurrentPid => Expr::CurrentPid,
2004 };
2005
2006 let arithmetic = select! {
2008 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
2009 };
2010
2011 let keyword_as_bareword = select! {
2014 Token::Done => "done",
2015 Token::Fi => "fi",
2016 Token::Then => "then",
2017 Token::Else => "else",
2018 Token::Elif => "elif",
2019 Token::In => "in",
2020 Token::Do => "do",
2021 Token::Esac => "esac",
2022 Token::Set => "set",
2027 }
2028 .map(|s| Expr::Literal(Value::String(s.to_string())));
2029
2030 let plus_minus_bare = select! {
2032 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
2033 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
2034 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
2035 };
2036
2037 let glob_pattern = select! {
2039 Token::GlobWord(s) => Expr::GlobPattern(s),
2040 Token::Star => Expr::GlobPattern("*".to_string()),
2041 Token::Question => Expr::GlobPattern("?".to_string()),
2042 };
2043
2044 recursive(|expr| {
2045 choice((
2046 positional,
2047 arithmetic,
2048 cmd_subst_parser(expr.clone()),
2049 var_expr_parser(),
2050 interpolated_string_parser(),
2051 literal_parser().map(Expr::Literal),
2052 glob_pattern,
2054 ident_parser().map(|s| Expr::Literal(Value::String(s))),
2056 path_parser().map(|s| Expr::Literal(Value::String(s))),
2058 select! {
2061 Token::Dot => Expr::Literal(Value::String(".".into())),
2067 Token::DotDot => Expr::Literal(Value::String("..".into())),
2068 Token::Comma => Expr::Literal(Value::String(",".into())),
2075 Token::Colon => Expr::Literal(Value::String(":".into())),
2082 Token::Tilde => Expr::Literal(Value::String("~".into())),
2083 Token::TildePath(s) => Expr::Literal(Value::String(s)),
2084 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
2085 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
2086 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
2088 Token::DashNumWord(s) => Expr::Literal(Value::String(s)),
2091 Token::AtWord(s) => Expr::Literal(Value::String(s)),
2093 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
2098 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
2101 },
2102 plus_minus_bare,
2103 keyword_as_bareword,
2105 ))
2106 .labelled("expression")
2107 })
2108 .boxed()
2109}
2110
2111fn var_expr_parser<'tokens, I>(
2114) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2115where
2116 I: ValueInput<'tokens, Token = Token, Span = Span>,
2117{
2118 select! {
2119 Token::VarRef(raw) => parse_var_expr(&raw),
2120 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
2121 }
2122 .labelled("variable reference")
2123}
2124
2125fn cmd_subst_parser<'tokens, I, E>(
2129 expr: E,
2130) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2131where
2132 I: ValueInput<'tokens, Token = Token, Span = Span>,
2133 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone,
2134{
2135 let long_flag_with_value = select! {
2138 Token::LongFlag(name) => name,
2139 }
2140 .then_ignore(just(Token::Eq))
2141 .then(expr.clone())
2142 .map(|(key, value)| Arg::Named { key, value });
2143
2144 let long_flag = select! {
2146 Token::LongFlag(name) => Arg::LongFlag(name),
2147 };
2148
2149 let short_flag = select! {
2151 Token::ShortFlag(name) => Arg::ShortFlag(name),
2152 };
2153
2154 let named = choice((ident_parser(), keyword_word()))
2157 .then_ignore(just(Token::Eq))
2158 .then(expr.clone())
2159 .map(|(key, value)| Arg::WordAssign { key, value });
2160
2161 let positional = expr.map(Arg::Positional);
2163
2164 let arg = choice((
2165 long_flag_with_value,
2166 long_flag,
2167 short_flag,
2168 named,
2169 positional,
2170 ));
2171
2172 let command_name = choice((
2174 ident_parser(),
2175 just(Token::True).to("true".to_string()),
2176 just(Token::False).to("false".to_string()),
2177 ));
2178
2179 let command = command_name
2181 .then(arg.repeated().collect::<Vec<_>>())
2182 .map(|(name, args)| Command {
2183 name,
2184 args,
2185 redirects: vec![],
2186 });
2187
2188 let pipeline = command
2190 .separated_by(just(Token::Pipe))
2191 .at_least(1)
2192 .collect::<Vec<_>>()
2193 .map(|commands| Pipeline {
2194 commands,
2195 background: false,
2196 });
2197
2198 let pipeline_stmt = pipeline.map(pipeline_into_stmt);
2201
2202 let chained = pipeline_stmt.clone().foldl(
2210 choice((
2211 just(Token::And).to(true), just(Token::Or).to(false), ))
2214 .then(pipeline_stmt.clone())
2215 .repeated(),
2216 |left, (is_and, right): (bool, Stmt)| {
2217 if is_and {
2218 Stmt::AndChain {
2219 left: Box::new(left),
2220 right: Box::new(right),
2221 }
2222 } else {
2223 Stmt::OrChain {
2224 left: Box::new(left),
2225 right: Box::new(right),
2226 }
2227 }
2228 },
2229 );
2230
2231 let separator = choice((just(Token::Newline), just(Token::Semi)));
2236 let body = separator
2237 .clone()
2238 .repeated()
2239 .ignore_then(
2240 chained
2241 .separated_by(separator.clone().repeated().at_least(1))
2242 .allow_trailing()
2243 .collect::<Vec<_>>(),
2244 )
2245 .then_ignore(separator.repeated());
2246
2247 just(Token::CmdSubstStart)
2248 .ignore_then(body)
2249 .then_ignore(just(Token::RParen))
2250 .map(Expr::CommandSubst)
2251 .labelled("command substitution")
2252}
2253
2254fn interpolated_string_parser<'tokens, I>(
2256) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2257where
2258 I: ValueInput<'tokens, Token = Token, Span = Span>,
2259{
2260 let double_quoted = select! {
2262 Token::String(s) => s,
2263 }
2264 .try_map(|s, span| {
2265 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
2267 let parts = parse_interpolated_string(&s)
2270 .map_err(|msg| Rich::custom(span, msg))?;
2271 if parts.len() == 1
2272 && let StringPart::Literal(text) = &parts[0] {
2273 return Ok(Expr::Literal(Value::String(text.clone())));
2274 }
2275 Ok(Expr::Interpolated(parts))
2276 } else {
2277 Ok(Expr::Literal(Value::String(s)))
2278 }
2279 });
2280
2281 let single_quoted = select! {
2283 Token::SingleString(s) => Expr::Literal(Value::String(s)),
2284 };
2285
2286 choice((single_quoted, double_quoted)).labelled("string")
2287}
2288
2289fn literal_parser<'tokens, I>(
2291) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2292where
2293 I: ValueInput<'tokens, Token = Token, Span = Span>,
2294{
2295 choice((
2296 select! {
2297 Token::True => Value::Bool(true),
2298 Token::False => Value::Bool(false),
2299 },
2300 select! {
2301 Token::Int(n) => Value::Int(n),
2302 Token::Float(f) => Value::Float(f),
2303 },
2304 ))
2305 .labelled("literal")
2306 .boxed()
2307}
2308
2309fn ident_parser<'tokens, I>(
2311) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2312where
2313 I: ValueInput<'tokens, Token = Token, Span = Span>,
2314{
2315 select! {
2316 Token::Ident(s) => s,
2317 }
2318 .labelled("identifier")
2319}
2320
2321fn path_parser<'tokens, I>(
2323) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2324where
2325 I: ValueInput<'tokens, Token = Token, Span = Span>,
2326{
2327 select! {
2328 Token::Path(s) => s,
2329 }
2330 .labelled("path")
2331}
2332
2333#[cfg(test)]
2334#[allow(clippy::approx_constant)]
2335mod tests {
2336 use super::*;
2337
2338 fn subst_cmd(expr: &Expr) -> &Command {
2340 match expr {
2341 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2342 [Stmt::Command(cmd)] => cmd,
2343 other => panic!("expected a single command in $(), got {other:?}"),
2344 },
2345 other => panic!("expected command subst, got {other:?}"),
2346 }
2347 }
2348
2349 fn subst_pipeline(expr: &Expr) -> &Pipeline {
2351 match expr {
2352 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2353 [Stmt::Pipeline(p)] => p,
2354 other => panic!("expected a single pipeline in $(), got {other:?}"),
2355 },
2356 other => panic!("expected command subst, got {other:?}"),
2357 }
2358 }
2359
2360 #[test]
2361 fn parse_empty() {
2362 let result = parse("");
2363 assert!(result.is_ok());
2364 assert_eq!(result.expect("ok").statements.len(), 0);
2365 }
2366
2367 #[test]
2368 fn parse_newlines_only() {
2369 let result = parse("\n\n\n");
2370 assert!(result.is_ok());
2371 }
2372
2373 #[test]
2374 fn parse_simple_command() {
2375 let result = parse("echo");
2376 assert!(result.is_ok());
2377 let program = result.expect("ok");
2378 assert_eq!(program.statements.len(), 1);
2379 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2380 }
2381
2382 #[test]
2383 fn parse_command_with_string_arg() {
2384 let result = parse(r#"echo "hello""#);
2385 assert!(result.is_ok());
2386 let program = result.expect("ok");
2387 match &program.statements[0] {
2388 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
2389 _ => panic!("expected Command"),
2390 }
2391 }
2392
2393 #[test]
2394 fn parse_assignment() {
2395 let result = parse("X=5");
2396 assert!(result.is_ok());
2397 let program = result.expect("ok");
2398 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2399 }
2400
2401 #[test]
2402 fn parse_pipeline() {
2403 let result = parse("a | b | c");
2404 assert!(result.is_ok());
2405 let program = result.expect("ok");
2406 match &program.statements[0] {
2407 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2408 _ => panic!("expected Pipeline"),
2409 }
2410 }
2411
2412 #[test]
2413 fn parse_background_job() {
2414 let result = parse("cmd &");
2415 assert!(result.is_ok());
2416 let program = result.expect("ok");
2417 match &program.statements[0] {
2418 Stmt::Pipeline(p) => assert!(p.background),
2419 _ => panic!("expected Pipeline with background"),
2420 }
2421 }
2422
2423 #[test]
2424 fn parse_if_simple() {
2425 let result = parse("if true; then echo; fi");
2426 assert!(result.is_ok());
2427 let program = result.expect("ok");
2428 assert!(matches!(&program.statements[0], Stmt::If(_)));
2429 }
2430
2431 #[test]
2432 fn parse_if_else() {
2433 let result = parse("if true; then echo; else echo; fi");
2434 assert!(result.is_ok());
2435 let program = result.expect("ok");
2436 match &program.statements[0] {
2437 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
2438 _ => panic!("expected If"),
2439 }
2440 }
2441
2442 #[test]
2443 fn parse_elif_simple() {
2444 let result = parse("if true; then echo a; elif false; then echo b; fi");
2445 assert!(result.is_ok(), "parse failed: {:?}", result);
2446 let program = result.expect("ok");
2447 match &program.statements[0] {
2448 Stmt::If(if_stmt) => {
2449 assert!(if_stmt.else_branch.is_some());
2451 let else_branch = if_stmt.else_branch.as_ref().unwrap();
2452 assert_eq!(else_branch.len(), 1);
2453 assert!(matches!(&else_branch[0], Stmt::If(_)));
2454 }
2455 _ => panic!("expected If"),
2456 }
2457 }
2458
2459 #[test]
2460 fn parse_elif_with_else() {
2461 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
2462 assert!(result.is_ok(), "parse failed: {:?}", result);
2463 let program = result.expect("ok");
2464 match &program.statements[0] {
2465 Stmt::If(outer_if) => {
2466 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
2468 assert_eq!(else_branch.len(), 1);
2469 match &else_branch[0] {
2470 Stmt::If(inner_if) => {
2471 assert!(inner_if.else_branch.is_some());
2473 }
2474 _ => panic!("expected nested If from elif"),
2475 }
2476 }
2477 _ => panic!("expected If"),
2478 }
2479 }
2480
2481 #[test]
2482 fn parse_multiple_elif() {
2483 let result = parse(
2485 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
2486 );
2487 assert!(result.is_ok(), "parse failed: {:?}", result);
2488 }
2489
2490 #[test]
2491 fn parse_for_loop() {
2492 let result = parse("for X in items; do echo; done");
2493 assert!(result.is_ok());
2494 let program = result.expect("ok");
2495 assert!(matches!(&program.statements[0], Stmt::For(_)));
2496 }
2497
2498 #[test]
2499 fn parse_brackets_not_array_literal() {
2500 let result = parse("cmd [1");
2502 let _ = result;
2505 }
2506
2507 #[test]
2508 fn parse_named_arg() {
2509 let result = parse("cmd foo=5");
2513 assert!(result.is_ok());
2514 let program = result.expect("ok");
2515 match &program.statements[0] {
2516 Stmt::Command(cmd) => {
2517 assert_eq!(cmd.args.len(), 1);
2518 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
2519 }
2520 _ => panic!("expected Command"),
2521 }
2522 }
2523
2524 #[test]
2525 fn parse_short_flag() {
2526 let result = parse("ls -l");
2527 assert!(result.is_ok());
2528 let program = result.expect("ok");
2529 match &program.statements[0] {
2530 Stmt::Command(cmd) => {
2531 assert_eq!(cmd.name, "ls");
2532 assert_eq!(cmd.args.len(), 1);
2533 match &cmd.args[0] {
2534 Arg::ShortFlag(name) => assert_eq!(name, "l"),
2535 _ => panic!("expected ShortFlag"),
2536 }
2537 }
2538 _ => panic!("expected Command"),
2539 }
2540 }
2541
2542 #[test]
2543 fn parse_long_flag() {
2544 let result = parse("git push --force");
2545 assert!(result.is_ok());
2546 let program = result.expect("ok");
2547 match &program.statements[0] {
2548 Stmt::Command(cmd) => {
2549 assert_eq!(cmd.name, "git");
2550 assert_eq!(cmd.args.len(), 2);
2551 match &cmd.args[0] {
2552 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
2553 _ => panic!("expected Positional push"),
2554 }
2555 match &cmd.args[1] {
2556 Arg::LongFlag(name) => assert_eq!(name, "force"),
2557 _ => panic!("expected LongFlag"),
2558 }
2559 }
2560 _ => panic!("expected Command"),
2561 }
2562 }
2563
2564 #[test]
2565 fn parse_long_flag_with_value() {
2566 let result = parse(r#"git commit --message="hello""#);
2567 assert!(result.is_ok());
2568 let program = result.expect("ok");
2569 match &program.statements[0] {
2570 Stmt::Command(cmd) => {
2571 assert_eq!(cmd.name, "git");
2572 assert_eq!(cmd.args.len(), 2);
2573 match &cmd.args[1] {
2574 Arg::Named { key, value } => {
2575 assert_eq!(key, "message");
2576 match value {
2577 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
2578 _ => panic!("expected String value"),
2579 }
2580 }
2581 _ => panic!("expected Named from --flag=value"),
2582 }
2583 }
2584 _ => panic!("expected Command"),
2585 }
2586 }
2587
2588 #[test]
2589 fn parse_mixed_flags_and_args() {
2590 let result = parse(r#"git commit -m "message" --amend"#);
2591 assert!(result.is_ok());
2592 let program = result.expect("ok");
2593 match &program.statements[0] {
2594 Stmt::Command(cmd) => {
2595 assert_eq!(cmd.name, "git");
2596 assert_eq!(cmd.args.len(), 4);
2597 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
2599 match &cmd.args[1] {
2601 Arg::ShortFlag(name) => assert_eq!(name, "m"),
2602 _ => panic!("expected ShortFlag -m"),
2603 }
2604 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
2606 match &cmd.args[3] {
2608 Arg::LongFlag(name) => assert_eq!(name, "amend"),
2609 _ => panic!("expected LongFlag --amend"),
2610 }
2611 }
2612 _ => panic!("expected Command"),
2613 }
2614 }
2615
2616 #[test]
2617 fn parse_redirect_stdout() {
2618 let result = parse("cmd > file");
2619 assert!(result.is_ok());
2620 let program = result.expect("ok");
2621 match &program.statements[0] {
2623 Stmt::Pipeline(p) => {
2624 assert_eq!(p.commands.len(), 1);
2625 let cmd = &p.commands[0];
2626 assert_eq!(cmd.redirects.len(), 1);
2627 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
2628 }
2629 _ => panic!("expected Pipeline"),
2630 }
2631 }
2632
2633 #[test]
2634 fn parse_var_ref() {
2635 let result = parse("echo ${VAR}");
2636 assert!(result.is_ok());
2637 let program = result.expect("ok");
2638 match &program.statements[0] {
2639 Stmt::Command(cmd) => {
2640 assert_eq!(cmd.args.len(), 1);
2641 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
2642 }
2643 _ => panic!("expected Command"),
2644 }
2645 }
2646
2647 #[test]
2648 fn parse_multiple_statements() {
2649 let result = parse("a\nb\nc");
2650 assert!(result.is_ok());
2651 let program = result.expect("ok");
2652 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2653 assert_eq!(non_empty.len(), 3);
2654 }
2655
2656 #[test]
2657 fn parse_semicolon_separated() {
2658 let result = parse("a; b; c");
2659 assert!(result.is_ok());
2660 let program = result.expect("ok");
2661 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2662 assert_eq!(non_empty.len(), 3);
2663 }
2664
2665 #[test]
2666 fn parse_complex_pipeline() {
2667 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
2668 assert!(result.is_ok());
2669 let program = result.expect("ok");
2670 match &program.statements[0] {
2671 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2672 _ => panic!("expected Pipeline"),
2673 }
2674 }
2675
2676 #[test]
2677 fn parse_json_as_string_arg() {
2678 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
2680 assert!(result.is_ok());
2681 }
2682
2683 #[test]
2684 fn parse_mixed_args() {
2685 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
2686 assert!(result.is_ok());
2687 let program = result.expect("ok");
2688 match &program.statements[0] {
2689 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
2690 _ => panic!("expected Command"),
2691 }
2692 }
2693
2694 #[test]
2695 fn error_unterminated_string() {
2696 let result = parse(r#"echo "hello"#);
2697 assert!(result.is_err());
2698 }
2699
2700 #[test]
2701 fn error_unterminated_var_ref() {
2702 let result = parse("echo ${VAR");
2703 assert!(result.is_err());
2704 }
2705
2706 #[test]
2707 fn error_missing_fi() {
2708 let result = parse("if true; then echo");
2709 assert!(result.is_err());
2710 }
2711
2712 #[test]
2713 fn error_missing_done() {
2714 let result = parse("for X in items; do echo");
2715 assert!(result.is_err());
2716 }
2717
2718 #[test]
2719 fn parse_nested_cmd_subst() {
2720 let result = parse("X=$(echo $(date))").unwrap();
2722 match &result.statements[0] {
2723 Stmt::Assignment(a) => {
2724 assert_eq!(a.name, "X");
2725 let outer = subst_cmd(&a.value);
2726 assert_eq!(outer.name, "echo");
2727 match &outer.args[0] {
2729 Arg::Positional(inner_expr) => {
2730 assert_eq!(subst_cmd(inner_expr).name, "date");
2731 }
2732 other => panic!("expected nested cmd subst arg, got {:?}", other),
2733 }
2734 }
2735 other => panic!("expected assignment, got {:?}", other),
2736 }
2737 }
2738
2739 #[test]
2740 fn parse_deeply_nested_cmd_subst() {
2741 let result = parse("X=$(a $(b $(c)))").unwrap();
2743 match &result.statements[0] {
2744 Stmt::Assignment(a) => {
2745 let level1 = subst_cmd(&a.value);
2746 assert_eq!(level1.name, "a");
2747 match &level1.args[0] {
2748 Arg::Positional(level2_expr) => {
2749 let level2 = subst_cmd(level2_expr);
2750 assert_eq!(level2.name, "b");
2751 match &level2.args[0] {
2752 Arg::Positional(level3_expr) => {
2753 assert_eq!(subst_cmd(level3_expr).name, "c");
2754 }
2755 other => panic!("expected level3 cmd subst, got {:?}", other),
2756 }
2757 }
2758 other => panic!("expected level2 cmd subst, got {:?}", other),
2759 }
2760 }
2761 other => panic!("expected assignment, got {:?}", other),
2762 }
2763 }
2764
2765 #[test]
2770 fn value_int_preserved() {
2771 let result = parse("X=42").unwrap();
2772 match &result.statements[0] {
2773 Stmt::Assignment(a) => {
2774 assert_eq!(a.name, "X");
2775 match &a.value {
2776 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2777 other => panic!("expected int literal, got {:?}", other),
2778 }
2779 }
2780 other => panic!("expected assignment, got {:?}", other),
2781 }
2782 }
2783
2784 #[test]
2785 fn value_negative_int_preserved() {
2786 let result = parse("X=-99").unwrap();
2787 match &result.statements[0] {
2788 Stmt::Assignment(a) => match &a.value {
2789 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
2790 other => panic!("expected int, got {:?}", other),
2791 },
2792 other => panic!("expected assignment, got {:?}", other),
2793 }
2794 }
2795
2796 #[test]
2797 fn value_float_preserved() {
2798 let result = parse("PI=3.14").unwrap();
2799 match &result.statements[0] {
2800 Stmt::Assignment(a) => match &a.value {
2801 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
2802 other => panic!("expected float, got {:?}", other),
2803 },
2804 other => panic!("expected assignment, got {:?}", other),
2805 }
2806 }
2807
2808 #[test]
2809 fn value_string_preserved() {
2810 let result = parse(r#"echo "hello world""#).unwrap();
2811 match &result.statements[0] {
2812 Stmt::Command(cmd) => {
2813 assert_eq!(cmd.name, "echo");
2814 match &cmd.args[0] {
2815 Arg::Positional(Expr::Literal(Value::String(s))) => {
2816 assert_eq!(s, "hello world");
2817 }
2818 other => panic!("expected string arg, got {:?}", other),
2819 }
2820 }
2821 other => panic!("expected command, got {:?}", other),
2822 }
2823 }
2824
2825 #[test]
2826 fn value_string_with_escapes_preserved() {
2827 let result = parse(r#"echo "line1\nline2""#).unwrap();
2828 match &result.statements[0] {
2829 Stmt::Command(cmd) => match &cmd.args[0] {
2830 Arg::Positional(Expr::Literal(Value::String(s))) => {
2831 assert_eq!(s, "line1\nline2");
2832 }
2833 other => panic!("expected string, got {:?}", other),
2834 },
2835 other => panic!("expected command, got {:?}", other),
2836 }
2837 }
2838
2839 #[test]
2840 fn value_command_name_preserved() {
2841 let result = parse("my-command").unwrap();
2842 match &result.statements[0] {
2843 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
2844 other => panic!("expected command, got {:?}", other),
2845 }
2846 }
2847
2848 #[test]
2849 fn value_assignment_name_preserved() {
2850 let result = parse("MY_VAR=1").unwrap();
2851 match &result.statements[0] {
2852 Stmt::Assignment(a) => assert_eq!(a.name, "MY_VAR"),
2853 other => panic!("expected assignment, got {:?}", other),
2854 }
2855 }
2856
2857 #[test]
2858 fn value_for_variable_preserved() {
2859 let result = parse("for ITEM in items; do echo; done").unwrap();
2860 match &result.statements[0] {
2861 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
2862 other => panic!("expected for, got {:?}", other),
2863 }
2864 }
2865
2866 #[test]
2867 fn value_varref_name_preserved() {
2868 let result = parse("echo ${MESSAGE}").unwrap();
2869 match &result.statements[0] {
2870 Stmt::Command(cmd) => match &cmd.args[0] {
2871 Arg::Positional(Expr::VarRef(path)) => {
2872 assert_eq!(path.segments.len(), 1);
2873 let VarSegment::Field(name) = &path.segments[0];
2874 assert_eq!(name, "MESSAGE");
2875 }
2876 other => panic!("expected varref, got {:?}", other),
2877 },
2878 other => panic!("expected command, got {:?}", other),
2879 }
2880 }
2881
2882 #[test]
2883 fn value_varref_field_access_preserved() {
2884 let result = parse("echo ${RESULT.data}").unwrap();
2885 match &result.statements[0] {
2886 Stmt::Command(cmd) => match &cmd.args[0] {
2887 Arg::Positional(Expr::VarRef(path)) => {
2888 assert_eq!(path.segments.len(), 2);
2889 let VarSegment::Field(a) = &path.segments[0];
2890 let VarSegment::Field(b) = &path.segments[1];
2891 assert_eq!(a, "RESULT");
2892 assert_eq!(b, "data");
2893 }
2894 other => panic!("expected varref, got {:?}", other),
2895 },
2896 other => panic!("expected command, got {:?}", other),
2897 }
2898 }
2899
2900 #[test]
2901 fn value_varref_index_ignored() {
2902 let result = parse("echo ${ITEMS[0]}").unwrap();
2904 match &result.statements[0] {
2905 Stmt::Command(cmd) => match &cmd.args[0] {
2906 Arg::Positional(Expr::VarRef(path)) => {
2907 assert_eq!(path.segments.len(), 1);
2909 let VarSegment::Field(name) = &path.segments[0];
2910 assert_eq!(name, "ITEMS");
2911 }
2912 other => panic!("expected varref, got {:?}", other),
2913 },
2914 other => panic!("expected command, got {:?}", other),
2915 }
2916 }
2917
2918 #[test]
2919 fn value_named_arg_preserved() {
2920 let result = parse("cmd count=42").unwrap();
2924 match &result.statements[0] {
2925 Stmt::Command(cmd) => {
2926 assert_eq!(cmd.name, "cmd");
2927 match &cmd.args[0] {
2928 Arg::WordAssign { key, value } => {
2929 assert_eq!(key, "count");
2930 match value {
2931 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2932 other => panic!("expected int, got {:?}", other),
2933 }
2934 }
2935 other => panic!("expected WordAssign arg, got {:?}", other),
2936 }
2937 }
2938 other => panic!("expected command, got {:?}", other),
2939 }
2940 }
2941
2942 #[test]
2943 fn value_function_def_name_preserved() {
2944 let result = parse("greet() { echo }").unwrap();
2945 match &result.statements[0] {
2946 Stmt::ToolDef(t) => {
2947 assert_eq!(t.name, "greet");
2948 assert!(t.params.is_empty());
2949 }
2950 other => panic!("expected function def, got {:?}", other),
2951 }
2952 }
2953
2954 #[test]
2959 fn parse_comparison_equals() {
2960 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
2962 match &result.statements[0] {
2963 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2964 Expr::Test(test) => match test.as_ref() {
2965 TestExpr::Comparison { left, op, right } => {
2966 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2967 assert_eq!(*op, TestCmpOp::Eq);
2968 match right.as_ref() {
2969 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
2970 other => panic!("expected int, got {:?}", other),
2971 }
2972 }
2973 other => panic!("expected comparison, got {:?}", other),
2974 },
2975 other => panic!("expected test expr, got {:?}", other),
2976 },
2977 other => panic!("expected if, got {:?}", other),
2978 }
2979 }
2980
2981 #[test]
2982 fn parse_comparison_not_equals() {
2983 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
2984 match &result.statements[0] {
2985 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2986 Expr::Test(test) => match test.as_ref() {
2987 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
2988 other => panic!("expected comparison, got {:?}", other),
2989 },
2990 other => panic!("expected test expr, got {:?}", other),
2991 },
2992 other => panic!("expected if, got {:?}", other),
2993 }
2994 }
2995
2996 #[test]
2997 fn parse_comparison_less_than() {
2998 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
2999 match &result.statements[0] {
3000 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3001 Expr::Test(test) => match test.as_ref() {
3002 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
3003 other => panic!("expected comparison, got {:?}", other),
3004 },
3005 other => panic!("expected test expr, got {:?}", other),
3006 },
3007 other => panic!("expected if, got {:?}", other),
3008 }
3009 }
3010
3011 #[test]
3012 fn parse_comparison_greater_than() {
3013 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
3014 match &result.statements[0] {
3015 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3016 Expr::Test(test) => match test.as_ref() {
3017 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
3018 other => panic!("expected comparison, got {:?}", other),
3019 },
3020 other => panic!("expected test expr, got {:?}", other),
3021 },
3022 other => panic!("expected if, got {:?}", other),
3023 }
3024 }
3025
3026 #[test]
3027 fn parse_comparison_less_equal() {
3028 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
3029 match &result.statements[0] {
3030 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3031 Expr::Test(test) => match test.as_ref() {
3032 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
3033 other => panic!("expected comparison, got {:?}", other),
3034 },
3035 other => panic!("expected test expr, got {:?}", other),
3036 },
3037 other => panic!("expected if, got {:?}", other),
3038 }
3039 }
3040
3041 #[test]
3042 fn parse_comparison_greater_equal() {
3043 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
3044 match &result.statements[0] {
3045 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3046 Expr::Test(test) => match test.as_ref() {
3047 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
3048 other => panic!("expected comparison, got {:?}", other),
3049 },
3050 other => panic!("expected test expr, got {:?}", other),
3051 },
3052 other => panic!("expected if, got {:?}", other),
3053 }
3054 }
3055
3056 #[test]
3057 fn parse_regex_match() {
3058 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
3059 match &result.statements[0] {
3060 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3061 Expr::Test(test) => match test.as_ref() {
3062 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
3063 other => panic!("expected comparison, got {:?}", other),
3064 },
3065 other => panic!("expected test expr, got {:?}", other),
3066 },
3067 other => panic!("expected if, got {:?}", other),
3068 }
3069 }
3070
3071 #[test]
3072 fn parse_regex_not_match() {
3073 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
3074 match &result.statements[0] {
3075 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3076 Expr::Test(test) => match test.as_ref() {
3077 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
3078 other => panic!("expected comparison, got {:?}", other),
3079 },
3080 other => panic!("expected test expr, got {:?}", other),
3081 },
3082 other => panic!("expected if, got {:?}", other),
3083 }
3084 }
3085
3086 #[test]
3087 fn parse_string_interpolation() {
3088 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
3089 match &result.statements[0] {
3090 Stmt::Command(cmd) => match &cmd.args[0] {
3091 Arg::Positional(Expr::Interpolated(parts)) => {
3092 assert_eq!(parts.len(), 3);
3093 match &parts[0] {
3094 StringPart::Literal(s) => assert_eq!(s, "Hello "),
3095 other => panic!("expected literal, got {:?}", other),
3096 }
3097 match &parts[1] {
3098 StringPart::Var(path) => {
3099 assert_eq!(path.segments.len(), 1);
3100 let VarSegment::Field(name) = &path.segments[0];
3101 assert_eq!(name, "NAME");
3102 }
3103 other => panic!("expected var, got {:?}", other),
3104 }
3105 match &parts[2] {
3106 StringPart::Literal(s) => assert_eq!(s, "!"),
3107 other => panic!("expected literal, got {:?}", other),
3108 }
3109 }
3110 other => panic!("expected interpolated, got {:?}", other),
3111 },
3112 other => panic!("expected command, got {:?}", other),
3113 }
3114 }
3115
3116 #[test]
3117 fn parse_string_interpolation_multiple_vars() {
3118 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
3119 match &result.statements[0] {
3120 Stmt::Command(cmd) => match &cmd.args[0] {
3121 Arg::Positional(Expr::Interpolated(parts)) => {
3122 assert_eq!(parts.len(), 3);
3124 assert!(matches!(&parts[0], StringPart::Var(_)));
3125 assert!(matches!(&parts[1], StringPart::Literal(_)));
3126 assert!(matches!(&parts[2], StringPart::Var(_)));
3127 }
3128 other => panic!("expected interpolated, got {:?}", other),
3129 },
3130 other => panic!("expected command, got {:?}", other),
3131 }
3132 }
3133
3134 #[test]
3135 fn parse_empty_function_body() {
3136 let result = parse("empty() { }").unwrap();
3137 match &result.statements[0] {
3138 Stmt::ToolDef(t) => {
3139 assert_eq!(t.name, "empty");
3140 assert!(t.params.is_empty());
3141 assert!(t.body.is_empty());
3142 }
3143 other => panic!("expected function def, got {:?}", other),
3144 }
3145 }
3146
3147 #[test]
3148 fn parse_bash_style_function() {
3149 let result = parse("function greet { echo hello }").unwrap();
3150 match &result.statements[0] {
3151 Stmt::ToolDef(t) => {
3152 assert_eq!(t.name, "greet");
3153 assert!(t.params.is_empty());
3154 assert_eq!(t.body.len(), 1);
3155 }
3156 other => panic!("expected function def, got {:?}", other),
3157 }
3158 }
3159
3160 #[test]
3161 fn parse_comparison_string_values() {
3162 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
3163 match &result.statements[0] {
3164 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3165 Expr::Test(test) => match test.as_ref() {
3166 TestExpr::Comparison { left, op, right } => {
3167 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3168 assert_eq!(*op, TestCmpOp::Eq);
3169 match right.as_ref() {
3170 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
3171 other => panic!("expected string, got {:?}", other),
3172 }
3173 }
3174 other => panic!("expected comparison, got {:?}", other),
3175 },
3176 other => panic!("expected test expr, got {:?}", other),
3177 },
3178 other => panic!("expected if, got {:?}", other),
3179 }
3180 }
3181
3182 #[test]
3187 fn parse_cmd_subst_simple() {
3188 let result = parse("X=$(echo)").unwrap();
3189 match &result.statements[0] {
3190 Stmt::Assignment(a) => {
3191 assert_eq!(a.name, "X");
3192 assert_eq!(subst_cmd(&a.value).name, "echo");
3193 }
3194 other => panic!("expected assignment, got {:?}", other),
3195 }
3196 }
3197
3198 #[test]
3199 fn parse_cmd_subst_with_args() {
3200 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
3201 match &result.statements[0] {
3202 Stmt::Assignment(a) => {
3203 let cmd = subst_cmd(&a.value);
3204 assert_eq!(cmd.name, "fetch");
3205 assert_eq!(cmd.args.len(), 1);
3206 match &cmd.args[0] {
3207 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
3208 other => panic!("expected WordAssign arg, got {:?}", other),
3209 }
3210 }
3211 other => panic!("expected assignment, got {:?}", other),
3212 }
3213 }
3214
3215 #[test]
3216 fn parse_cmd_subst_pipeline() {
3217 let result = parse("X=$(cat file | grep pattern)").unwrap();
3218 match &result.statements[0] {
3219 Stmt::Assignment(a) => {
3220 let pipeline = subst_pipeline(&a.value);
3221 assert_eq!(pipeline.commands.len(), 2);
3222 assert_eq!(pipeline.commands[0].name, "cat");
3223 assert_eq!(pipeline.commands[1].name, "grep");
3224 }
3225 other => panic!("expected assignment, got {:?}", other),
3226 }
3227 }
3228
3229 #[test]
3230 fn parse_cmd_subst_in_condition() {
3231 let result = parse("if kaish-validate; then echo; fi").unwrap();
3233 match &result.statements[0] {
3234 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3235 Expr::Command(cmd) => {
3236 assert_eq!(cmd.name, "kaish-validate");
3237 }
3238 other => panic!("expected command, got {:?}", other),
3239 },
3240 other => panic!("expected if, got {:?}", other),
3241 }
3242 }
3243
3244 #[test]
3249 fn parse_env_prefix_single() {
3250 let result = parse("FOO=bar echo hi").unwrap();
3251 match &result.statements[0] {
3252 Stmt::EnvScoped { assignments, body } => {
3253 assert_eq!(assignments.len(), 1);
3254 assert_eq!(assignments[0].name, "FOO");
3255 assert!(!assignments[0].local);
3256 match body.as_ref() {
3257 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
3258 other => panic!("expected command body, got {other:?}"),
3259 }
3260 }
3261 other => panic!("expected env-scoped, got {other:?}"),
3262 }
3263 }
3264
3265 #[test]
3266 fn parse_env_prefix_multiple() {
3267 let result = parse("A=1 B=2 run").unwrap();
3268 match &result.statements[0] {
3269 Stmt::EnvScoped { assignments, body } => {
3270 assert_eq!(assignments.len(), 2);
3271 assert_eq!(assignments[0].name, "A");
3272 assert_eq!(assignments[1].name, "B");
3273 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
3274 }
3275 other => panic!("expected env-scoped, got {other:?}"),
3276 }
3277 }
3278
3279 #[test]
3280 fn parse_bare_assignment_is_not_env_scoped() {
3281 let result = parse("FOO=bar").unwrap();
3283 assert!(
3284 matches!(&result.statements[0], Stmt::Assignment(a) if a.name == "FOO"),
3285 "got {:?}",
3286 result.statements[0]
3287 );
3288 }
3289
3290 #[test]
3291 fn parse_assignment_then_and_chain_does_not_over_capture() {
3292 let result = parse("FOO=bar && echo hi").unwrap();
3295 match &result.statements[0] {
3296 Stmt::AndChain { left, right } => {
3297 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name == "FOO"));
3298 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
3299 }
3300 other => panic!("expected and-chain, got {other:?}"),
3301 }
3302 }
3303
3304 #[test]
3305 fn parse_env_prefix_pipeline_body() {
3306 let result = parse("FOO=bar cat | grep x").unwrap();
3307 match &result.statements[0] {
3308 Stmt::EnvScoped { assignments, body } => {
3309 assert_eq!(assignments[0].name, "FOO");
3310 match body.as_ref() {
3311 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 2),
3312 other => panic!("expected pipeline body, got {other:?}"),
3313 }
3314 }
3315 other => panic!("expected env-scoped, got {other:?}"),
3316 }
3317 }
3318
3319 fn parse_err_message(source: &str) -> String {
3324 parse(source)
3325 .expect_err("expected a parse error")
3326 .iter()
3327 .map(|e| e.message.clone())
3328 .collect::<Vec<_>>()
3329 .join(" ")
3330 }
3331
3332 #[test]
3333 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
3334 let msg = parse_err_message("echo /tmp/$(echo x).txt");
3337 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
3338 }
3339
3340 #[test]
3341 fn argv_splat_var_glued_to_path_is_rejected() {
3342 assert!(parse("echo $dir/out.txt").is_err());
3343 }
3344
3345 #[test]
3346 fn argv_splat_three_way_glue_is_rejected() {
3347 assert!(parse("echo foo$(echo bar)baz").is_err());
3348 }
3349
3350 #[test]
3351 fn argv_splat_quoted_word_is_accepted() {
3352 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
3354 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
3355 }
3356
3357 #[test]
3358 fn argv_single_token_words_are_not_splat() {
3359 assert!(parse("echo file.txt").is_ok(), "file.txt");
3361 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
3362 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
3363 }
3364
3365 #[test]
3366 fn argv_spaced_words_are_not_splat() {
3367 assert!(parse("echo a b c").is_ok());
3368 assert!(parse("echo /tmp/x $(echo y)").is_ok());
3369 }
3370
3371 #[test]
3372 fn parse_cmd_subst_in_command_arg() {
3373 let result = parse("echo $(whoami)").unwrap();
3374 match &result.statements[0] {
3375 Stmt::Command(cmd) => {
3376 assert_eq!(cmd.name, "echo");
3377 match &cmd.args[0] {
3378 Arg::Positional(expr) => {
3379 assert_eq!(subst_cmd(expr).name, "whoami");
3380 }
3381 other => panic!("expected command subst, got {:?}", other),
3382 }
3383 }
3384 other => panic!("expected command, got {:?}", other),
3385 }
3386 }
3387
3388 #[test]
3393 fn parse_condition_and() {
3394 let result = parse("if check-a && check-b; then echo; fi").unwrap();
3396 match &result.statements[0] {
3397 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3398 Expr::BinaryOp { left, op, right } => {
3399 assert_eq!(*op, BinaryOp::And);
3400 assert!(matches!(left.as_ref(), Expr::Command(_)));
3401 assert!(matches!(right.as_ref(), Expr::Command(_)));
3402 }
3403 other => panic!("expected binary op, got {:?}", other),
3404 },
3405 other => panic!("expected if, got {:?}", other),
3406 }
3407 }
3408
3409 #[test]
3410 fn parse_condition_or() {
3411 let result = parse("if try-a || try-b; then echo; fi").unwrap();
3412 match &result.statements[0] {
3413 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3414 Expr::BinaryOp { left, op, right } => {
3415 assert_eq!(*op, BinaryOp::Or);
3416 assert!(matches!(left.as_ref(), Expr::Command(_)));
3417 assert!(matches!(right.as_ref(), Expr::Command(_)));
3418 }
3419 other => panic!("expected binary op, got {:?}", other),
3420 },
3421 other => panic!("expected if, got {:?}", other),
3422 }
3423 }
3424
3425 #[test]
3426 fn parse_condition_and_or_precedence() {
3427 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
3429 match &result.statements[0] {
3430 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3431 Expr::BinaryOp { left, op, right } => {
3432 assert_eq!(*op, BinaryOp::Or);
3434 match left.as_ref() {
3436 Expr::BinaryOp { op: inner_op, .. } => {
3437 assert_eq!(*inner_op, BinaryOp::And);
3438 }
3439 other => panic!("expected binary op (&&), got {:?}", other),
3440 }
3441 assert!(matches!(right.as_ref(), Expr::Command(_)));
3443 }
3444 other => panic!("expected binary op, got {:?}", other),
3445 },
3446 other => panic!("expected if, got {:?}", other),
3447 }
3448 }
3449
3450 #[test]
3451 fn parse_condition_multiple_and() {
3452 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
3453 match &result.statements[0] {
3454 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3455 Expr::BinaryOp { left, op, .. } => {
3456 assert_eq!(*op, BinaryOp::And);
3457 match left.as_ref() {
3459 Expr::BinaryOp { op: inner_op, .. } => {
3460 assert_eq!(*inner_op, BinaryOp::And);
3461 }
3462 other => panic!("expected binary op, got {:?}", other),
3463 }
3464 }
3465 other => panic!("expected binary op, got {:?}", other),
3466 },
3467 other => panic!("expected if, got {:?}", other),
3468 }
3469 }
3470
3471 #[test]
3472 fn parse_condition_mixed_comparison_and_logical() {
3473 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
3475 match &result.statements[0] {
3476 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3477 Expr::BinaryOp { left, op, right } => {
3478 assert_eq!(*op, BinaryOp::And);
3479 match left.as_ref() {
3481 Expr::Test(test) => match test.as_ref() {
3482 TestExpr::Comparison { op: left_op, .. } => {
3483 assert_eq!(*left_op, TestCmpOp::Eq);
3484 }
3485 other => panic!("expected comparison, got {:?}", other),
3486 },
3487 other => panic!("expected test, got {:?}", other),
3488 }
3489 match right.as_ref() {
3491 Expr::Test(test) => match test.as_ref() {
3492 TestExpr::Comparison { op: right_op, .. } => {
3493 assert_eq!(*right_op, TestCmpOp::NumGt);
3494 }
3495 other => panic!("expected comparison, got {:?}", other),
3496 },
3497 other => panic!("expected test, got {:?}", other),
3498 }
3499 }
3500 other => panic!("expected binary op, got {:?}", other),
3501 },
3502 other => panic!("expected if, got {:?}", other),
3503 }
3504 }
3505
3506 #[test]
3512 fn script_level1_linear() {
3513 let script = r#"
3514NAME="kaish"
3515VERSION=1
3516TIMEOUT=30
3517ITEMS="alpha beta gamma"
3518
3519echo "Starting ${NAME} v${VERSION}"
3520cat "README.md" | grep pattern="install" | head count=5
3521fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
3522echo "Items: ${ITEMS}"
3523"#;
3524 let result = parse(script).unwrap();
3525 let stmts: Vec<_> = result.statements.iter()
3526 .filter(|s| !matches!(s, Stmt::Empty))
3527 .collect();
3528
3529 assert_eq!(stmts.len(), 8);
3530 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(_))); }
3539
3540 #[test]
3542 fn script_level2_branching() {
3543 let script = r#"
3544RESULT=$(kaish-validate "input.json")
3545
3546if [[ ${RESULT.ok} == true ]]; then
3547 echo "Validation passed"
3548 process "input.json" > "output.json"
3549else
3550 echo "Validation failed: ${RESULT.err}"
3551fi
3552
3553if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
3554 echo "Count in valid range"
3555fi
3556
3557if check-network || check-cache; then
3558 fetch url=${URL}
3559fi
3560"#;
3561 let result = parse(script).unwrap();
3562 let stmts: Vec<_> = result.statements.iter()
3563 .filter(|s| !matches!(s, Stmt::Empty))
3564 .collect();
3565
3566 assert_eq!(stmts.len(), 4);
3567
3568 match stmts[0] {
3570 Stmt::Assignment(a) => {
3571 assert_eq!(a.name, "RESULT");
3572 assert!(matches!(&a.value, Expr::CommandSubst(_)));
3573 }
3574 other => panic!("expected assignment, got {:?}", other),
3575 }
3576
3577 match stmts[1] {
3579 Stmt::If(if_stmt) => {
3580 assert_eq!(if_stmt.then_branch.len(), 2);
3581 assert!(if_stmt.else_branch.is_some());
3582 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
3583 }
3584 other => panic!("expected if, got {:?}", other),
3585 }
3586
3587 match stmts[2] {
3589 Stmt::If(if_stmt) => {
3590 match if_stmt.condition.as_ref() {
3591 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3592 other => panic!("expected && condition, got {:?}", other),
3593 }
3594 }
3595 other => panic!("expected if, got {:?}", other),
3596 }
3597
3598 match stmts[3] {
3600 Stmt::If(if_stmt) => {
3601 match if_stmt.condition.as_ref() {
3602 Expr::BinaryOp { op, left, right } => {
3603 assert_eq!(*op, BinaryOp::Or);
3604 assert!(matches!(left.as_ref(), Expr::Command(_)));
3605 assert!(matches!(right.as_ref(), Expr::Command(_)));
3606 }
3607 other => panic!("expected || condition, got {:?}", other),
3608 }
3609 }
3610 other => panic!("expected if, got {:?}", other),
3611 }
3612 }
3613
3614 #[test]
3616 fn script_level3_loops_and_functions() {
3617 let script = r#"
3618greet() {
3619 echo "Hello, $1!"
3620}
3621
3622fetch_all() {
3623 for URL in $@; do
3624 fetch url=${URL}
3625 done
3626}
3627
3628USERS="alice bob charlie"
3629
3630for USER in ${USERS}; do
3631 greet ${USER}
3632 if [[ ${USER} == "bob" ]]; then
3633 echo "Found Bob!"
3634 fi
3635done
3636
3637long-running-task &
3638"#;
3639 let result = parse(script).unwrap();
3640 let stmts: Vec<_> = result.statements.iter()
3641 .filter(|s| !matches!(s, Stmt::Empty))
3642 .collect();
3643
3644 assert_eq!(stmts.len(), 5);
3645
3646 match stmts[0] {
3648 Stmt::ToolDef(t) => {
3649 assert_eq!(t.name, "greet");
3650 assert!(t.params.is_empty());
3651 }
3652 other => panic!("expected function def, got {:?}", other),
3653 }
3654
3655 match stmts[1] {
3657 Stmt::ToolDef(t) => {
3658 assert_eq!(t.name, "fetch_all");
3659 assert_eq!(t.body.len(), 1);
3660 assert!(matches!(&t.body[0], Stmt::For(_)));
3661 }
3662 other => panic!("expected function def, got {:?}", other),
3663 }
3664
3665 assert!(matches!(stmts[2], Stmt::Assignment(_)));
3667
3668 match stmts[3] {
3670 Stmt::For(f) => {
3671 assert_eq!(f.variable, "USER");
3672 assert_eq!(f.body.len(), 2);
3673 assert!(matches!(&f.body[0], Stmt::Command(_)));
3674 assert!(matches!(&f.body[1], Stmt::If(_)));
3675 }
3676 other => panic!("expected for loop, got {:?}", other),
3677 }
3678
3679 match stmts[4] {
3681 Stmt::Pipeline(p) => {
3682 assert!(p.background);
3683 assert_eq!(p.commands[0].name, "long-running-task");
3684 }
3685 other => panic!("expected pipeline (background), got {:?}", other),
3686 }
3687 }
3688
3689 #[test]
3691 fn script_level4_complex_nesting() {
3692 let script = r#"
3693RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
3694
3695if ping host=${HOST} && [[ ${RESULT} == true ]]; then
3696 for SERVER in "prod-1 prod-2"; do
3697 deploy target=${SERVER} port=8080
3698 if [[ $? -ne 0 ]]; then
3699 notify channel="ops" message="Deploy failed"
3700 fi
3701 done
3702fi
3703"#;
3704 let result = parse(script).unwrap();
3705 let stmts: Vec<_> = result.statements.iter()
3706 .filter(|s| !matches!(s, Stmt::Empty))
3707 .collect();
3708
3709 assert_eq!(stmts.len(), 2);
3710
3711 match stmts[0] {
3713 Stmt::Assignment(a) => {
3714 assert_eq!(a.name, "RESULT");
3715 assert_eq!(subst_pipeline(&a.value).commands.len(), 3);
3716 }
3717 other => panic!("expected assignment, got {:?}", other),
3718 }
3719
3720 match stmts[1] {
3722 Stmt::If(if_stmt) => {
3723 match if_stmt.condition.as_ref() {
3724 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3725 other => panic!("expected && condition, got {:?}", other),
3726 }
3727 assert_eq!(if_stmt.then_branch.len(), 1);
3728 match &if_stmt.then_branch[0] {
3729 Stmt::For(f) => {
3730 assert_eq!(f.body.len(), 2);
3731 assert!(matches!(&f.body[1], Stmt::If(_)));
3732 }
3733 other => panic!("expected for in if body, got {:?}", other),
3734 }
3735 }
3736 other => panic!("expected if, got {:?}", other),
3737 }
3738 }
3739
3740 #[test]
3742 fn script_level5_edge_cases() {
3743 let script = r#"
3744echo ""
3745echo "quotes: \"nested\" here"
3746echo "escapes: \n\t\r\\"
3747echo "unicode: \u2764"
3748
3749X=-99999
3750Y=3.14159265358979
3751Z=-0.001
3752
3753cmd a=1 b="two" c=true d=false e=null
3754
3755if true; then
3756 if false; then
3757 echo "inner"
3758 else
3759 echo "else"
3760 fi
3761fi
3762
3763for I in "a b c"; do
3764 echo ${I}
3765done
3766
3767no_params() {
3768 echo "no params"
3769}
3770
3771function all_args {
3772 echo "args: $@"
3773}
3774
3775a | b | c | d | e &
3776cmd 2> "errors.log"
3777cmd &> "all.log"
3778cmd >> "append.log"
3779cmd < "input.txt"
3780"#;
3781 let result = parse(script).unwrap();
3782 let stmts: Vec<_> = result.statements.iter()
3783 .filter(|s| !matches!(s, Stmt::Empty))
3784 .collect();
3785
3786 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
3788
3789 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
3791 assert!(bg_stmt.is_some(), "expected background pipeline");
3792
3793 match bg_stmt.unwrap() {
3794 Stmt::Pipeline(p) => {
3795 assert_eq!(p.commands.len(), 5);
3796 assert!(p.background);
3797 }
3798 _ => unreachable!(),
3799 }
3800 }
3801
3802 #[test]
3807 fn parse_keyword_as_variable_rejected() {
3808 let result = parse(r#"if="value""#);
3811 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
3812
3813 let result = parse("while=true");
3814 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
3815
3816 let result = parse(r#"then="next""#);
3817 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
3818 }
3819
3820 #[test]
3821 fn parse_set_command_with_flag() {
3822 let result = parse("set -e");
3823 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
3824 let program = result.unwrap();
3825 match &program.statements[0] {
3826 Stmt::Command(cmd) => {
3827 assert_eq!(cmd.name, "set");
3828 assert_eq!(cmd.args.len(), 1);
3829 match &cmd.args[0] {
3830 Arg::ShortFlag(f) => assert_eq!(f, "e"),
3831 other => panic!("expected ShortFlag, got {:?}", other),
3832 }
3833 }
3834 other => panic!("expected Command, got {:?}", other),
3835 }
3836 }
3837
3838 #[test]
3839 fn parse_set_command_no_args() {
3840 let result = parse("set");
3841 assert!(result.is_ok(), "failed to parse set: {:?}", result);
3842 let program = result.unwrap();
3843 match &program.statements[0] {
3844 Stmt::Command(cmd) => {
3845 assert_eq!(cmd.name, "set");
3846 assert_eq!(cmd.args.len(), 0);
3847 }
3848 other => panic!("expected Command, got {:?}", other),
3849 }
3850 }
3851
3852 #[test]
3853 fn parse_set_assignment_vs_command() {
3854 let result = parse("X=5");
3856 assert!(result.is_ok());
3857 let program = result.unwrap();
3858 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
3859
3860 let result = parse("set -e");
3862 assert!(result.is_ok());
3863 let program = result.unwrap();
3864 assert!(matches!(&program.statements[0], Stmt::Command(_)));
3865 }
3866
3867 #[test]
3868 fn parse_true_as_command() {
3869 let result = parse("true");
3870 assert!(result.is_ok());
3871 let program = result.unwrap();
3872 match &program.statements[0] {
3873 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
3874 other => panic!("expected Command(true), got {:?}", other),
3875 }
3876 }
3877
3878 #[test]
3879 fn parse_false_as_command() {
3880 let result = parse("false");
3881 assert!(result.is_ok());
3882 let program = result.unwrap();
3883 match &program.statements[0] {
3884 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
3885 other => panic!("expected Command(false), got {:?}", other),
3886 }
3887 }
3888
3889 #[test]
3890 fn parse_dot_as_source_alias() {
3891 let result = parse(". script.kai");
3892 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
3893 let program = result.unwrap();
3894 match &program.statements[0] {
3895 Stmt::Command(cmd) => {
3896 assert_eq!(cmd.name, ".");
3897 assert_eq!(cmd.args.len(), 1);
3898 }
3899 other => panic!("expected Command(.), got {:?}", other),
3900 }
3901 }
3902
3903 #[test]
3904 fn parse_source_command() {
3905 let result = parse("source utils.kai");
3906 assert!(result.is_ok(), "failed to parse source: {:?}", result);
3907 let program = result.unwrap();
3908 match &program.statements[0] {
3909 Stmt::Command(cmd) => {
3910 assert_eq!(cmd.name, "source");
3911 assert_eq!(cmd.args.len(), 1);
3912 }
3913 other => panic!("expected Command(source), got {:?}", other),
3914 }
3915 }
3916
3917 #[test]
3918 fn parse_test_expr_file_test() {
3919 let result = parse(r#"[[ -f "/path/file" ]]"#);
3921 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
3922 }
3923
3924 #[test]
3925 fn parse_test_expr_comparison() {
3926 let result = parse(r#"[[ $X == "value" ]]"#);
3927 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
3928 }
3929
3930 #[test]
3931 fn parse_test_expr_single_eq() {
3932 let result = parse(r#"[[ $X = "value" ]]"#);
3934 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
3935 let program = result.unwrap();
3936 match &program.statements[0] {
3937 Stmt::Test(TestExpr::Comparison { op, .. }) => {
3938 assert_eq!(op, &TestCmpOp::Eq);
3939 }
3940 other => panic!("expected Test(Comparison), got {:?}", other),
3941 }
3942 }
3943
3944 #[test]
3945 fn parse_while_loop() {
3946 let result = parse("while true; do echo; done");
3947 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
3948 let program = result.unwrap();
3949 assert!(matches!(&program.statements[0], Stmt::While(_)));
3950 }
3951
3952 #[test]
3953 fn parse_break_with_level() {
3954 let result = parse("break 2");
3955 assert!(result.is_ok());
3956 let program = result.unwrap();
3957 match &program.statements[0] {
3958 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
3959 other => panic!("expected Break(2), got {:?}", other),
3960 }
3961 }
3962
3963 #[test]
3964 fn parse_continue_with_level() {
3965 let result = parse("continue 3");
3966 assert!(result.is_ok());
3967 let program = result.unwrap();
3968 match &program.statements[0] {
3969 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
3970 other => panic!("expected Continue(3), got {:?}", other),
3971 }
3972 }
3973
3974 #[test]
3975 fn parse_exit_with_code() {
3976 let result = parse("exit 1");
3977 assert!(result.is_ok());
3978 let program = result.unwrap();
3979 match &program.statements[0] {
3980 Stmt::Exit(Some(expr)) => {
3981 match expr.as_ref() {
3982 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
3983 other => panic!("expected Int(1), got {:?}", other),
3984 }
3985 }
3986 other => panic!("expected Exit(1), got {:?}", other),
3987 }
3988 }
3989
3990 #[test]
3997 fn spanned_literal_only_records_byte_range() {
3998 let parts = parse_interpolated_string_spanned("hello world", 100);
3999 assert_eq!(parts.len(), 1);
4000 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
4001 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
4002 assert_eq!(parts[0].len, 11);
4003 }
4004
4005 #[test]
4006 fn spanned_braced_var_at_zero() {
4007 let parts = parse_interpolated_string_spanned("${X}", 50);
4008 assert_eq!(parts.len(), 1);
4009 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4010 assert_eq!(parts[0].offset, 50);
4011 assert_eq!(parts[0].len, 4); }
4013
4014 #[test]
4015 fn spanned_simple_var_then_literal() {
4016 let parts = parse_interpolated_string_spanned("$X end", 10);
4017 assert_eq!(parts.len(), 2);
4018 assert!(matches!(&parts[0].part, StringPart::Var(_)));
4019 assert_eq!(parts[0].offset, 10);
4020 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
4022 assert_eq!(parts[1].offset, 12);
4023 assert_eq!(parts[1].len, 4);
4024 }
4025
4026 #[test]
4027 fn spanned_mixed_literal_var_literal() {
4028 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0);
4029 assert_eq!(parts.len(), 3);
4030 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
4032 assert_eq!(parts[0].offset, 0);
4033 assert_eq!(parts[0].len, 3);
4034 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4036 assert_eq!(parts[1].offset, 3);
4037 assert_eq!(parts[1].len, 4);
4038 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
4040 assert_eq!(parts[2].offset, 7);
4041 assert_eq!(parts[2].len, 4);
4042 }
4043
4044 #[test]
4045 fn spanned_positional_param() {
4046 let parts = parse_interpolated_string_spanned("$1 done", 0);
4047 assert_eq!(parts.len(), 2);
4048 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
4049 assert_eq!(parts[0].offset, 0);
4050 assert_eq!(parts[0].len, 2); }
4052
4053 #[test]
4054 fn spanned_special_dollar_dollar() {
4055 let parts = parse_interpolated_string_spanned("$$", 5);
4056 assert_eq!(parts.len(), 1);
4057 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
4058 assert_eq!(parts[0].offset, 5);
4059 assert_eq!(parts[0].len, 2);
4060 }
4061
4062 #[test]
4063 fn spanned_arithmetic_marker_recognised() {
4064 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0);
4068 assert_eq!(parts.len(), 1);
4069 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
4070 }
4071
4072 #[test]
4073 fn spanned_default_separator_yields_var_with_default() {
4074 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0);
4075 assert_eq!(parts.len(), 1);
4076 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
4077 assert_eq!(parts[0].offset, 0);
4078 assert_eq!(parts[0].len, 14); }
4080
4081 #[test]
4082 fn spanned_no_dollar_runs_one_literal() {
4083 let parts = parse_interpolated_string_spanned("plain text only", 7);
4084 assert_eq!(parts.len(), 1);
4085 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
4086 assert_eq!(parts[0].offset, 7);
4087 assert_eq!(parts[0].len, 15);
4088 }
4089
4090 #[test]
4091 fn spanned_matches_unspanned_part_count() {
4092 let cases = [
4095 "hello",
4096 "$X",
4097 "${X}",
4098 "${X:-d}",
4099 "hi $A and $B",
4100 "$0 $1 $2",
4101 "$$ $? $#",
4102 ];
4103 for s in &cases {
4104 let unspanned = parse_interpolated_string(s).expect("test input parses");
4105 let spanned = parse_interpolated_string_spanned(s, 0);
4106 assert_eq!(
4107 unspanned.len(),
4108 spanned.len(),
4109 "part count differs for {:?}",
4110 s
4111 );
4112 }
4113 }
4114
4115 #[test]
4116 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
4117 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0);
4122 assert_eq!(parts.len(), 2);
4123
4124 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
4125 assert_eq!(parts[0].offset, 0);
4126 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
4127
4128 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4129 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
4130 assert_eq!(parts[1].len, 4);
4131 }
4132
4133 #[test]
4134 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
4135 let parts = parse_interpolated_string_spanned("hello 世界 world", 0);
4138 assert_eq!(parts.len(), 1);
4139 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
4140 assert_eq!(parts[0].offset, 0);
4141 assert_eq!(parts[0].len, 18);
4142 }
4143
4144 #[test]
4145 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
4146 let parts = parse_interpolated_string_spanned("\\$", 0);
4149 assert_eq!(parts.len(), 1);
4150 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
4151 assert_eq!(parts[0].offset, 0);
4152 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
4153 }
4154
4155 #[test]
4156 fn spanned_escape_backslash_collapses_pair_to_one() {
4157 let parts = parse_interpolated_string_spanned("\\\\", 0);
4158 assert_eq!(parts.len(), 1);
4159 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
4160 assert_eq!(parts[0].len, 2);
4161 }
4162}