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 = parse_interpolated_string(&unquote_default_word(default_str));
43 return Expr::VarWithDefault { name, default };
44 }
45
46 Expr::VarRef(parse_varpath(raw))
48}
49
50fn unquote_default_word(word: &str) -> String {
60 let mut out = String::with_capacity(word.len());
61 let mut in_single = false;
62 let mut in_double = false;
63 for ch in word.chars() {
64 match ch {
65 '\'' if !in_double => in_single = !in_single,
68 '"' if !in_single => in_double = !in_double,
69 '$' if in_single => out.push_str("__KAISH_ESCAPED_DOLLAR__"),
71 _ => out.push(ch),
72 }
73 }
74 out
75}
76
77fn find_default_separator(raw: &str) -> Option<usize> {
79 let bytes = raw.as_bytes();
80 let mut depth = 0;
81 let mut i = 0;
82
83 while i < bytes.len() {
84 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
85 depth += 1;
86 i += 2;
87 continue;
88 }
89 if bytes[i] == b'}' && depth > 0 {
90 depth -= 1;
91 i += 1;
92 continue;
93 }
94 if depth == 1 && i + 1 < bytes.len() && bytes[i] == b':' && bytes[i + 1] == b'-' {
96 return Some(i);
97 }
98 i += 1;
99 }
100 None
101}
102
103fn find_default_separator_in_content(content: &str) -> Option<usize> {
105 let bytes = content.as_bytes();
106 let mut depth = 0;
107 let mut i = 0;
108
109 while i < bytes.len() {
110 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
111 depth += 1;
112 i += 2;
113 continue;
114 }
115 if bytes[i] == b'}' && depth > 0 {
116 depth -= 1;
117 i += 1;
118 continue;
119 }
120 if depth == 0 && i + 1 < bytes.len() && bytes[i] == b':' && bytes[i + 1] == b'-' {
122 return Some(i);
123 }
124 i += 1;
125 }
126 None
127}
128
129fn parse_varpath(raw: &str) -> VarPath {
133 let segments_strs = lexer::parse_var_ref(raw).unwrap_or_default();
134 let segments = segments_strs
135 .into_iter()
136 .filter(|s| !s.starts_with('[')) .map(VarSegment::Field)
138 .collect();
139 VarPath { segments }
140}
141
142fn strip_empty_stmts(statements: Vec<Stmt>) -> Vec<Stmt> {
145 statements
146 .into_iter()
147 .filter(|s| !matches!(s, Stmt::Empty))
148 .collect()
149}
150
151fn parse_interpolated_string_spanned(s: &str, base_offset: usize) -> Vec<SpannedPart> {
170 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
171
172 let chars_vec: Vec<char> = s.chars().collect();
173 let mut i = 0;
174 let mut pos: usize = 0;
175
176 let mut parts: Vec<SpannedPart> = Vec::new();
177 let mut current_text = String::new();
178 let mut current_text_start: usize = pos;
179
180 let push_literal =
181 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
182 if !current_text.is_empty() {
183 parts.push(SpannedPart {
184 part: StringPart::Literal(std::mem::take(current_text)),
185 offset: base_offset + *start,
186 len: end - *start,
187 });
188 *start = end;
189 }
190 };
191
192 while i < chars_vec.len() {
193 let ch = chars_vec[i];
194
195 if ch == '\x00' {
196 let start = pos;
198 i += 1;
199 pos += 1;
200 let mut marker = String::new();
201 while let Some(&c) = chars_vec.get(i) {
202 if c == '\x00' {
203 i += 1;
204 pos += 1;
205 break;
206 }
207 marker.push(c);
208 i += 1;
209 pos += c.len_utf8();
210 }
211 if marker == "DOLLAR" {
212 if current_text.is_empty() {
213 current_text_start = start;
214 }
215 current_text.push('$');
216 }
217 } else if ch == '\\' {
218 let next = chars_vec.get(i + 1).copied();
224 match next {
225 Some('$') => {
226 if current_text.is_empty() {
227 current_text_start = pos;
228 }
229 current_text.push('$');
230 i += 2;
231 pos += 2;
232 }
233 Some('\\') => {
234 if current_text.is_empty() {
235 current_text_start = pos;
236 }
237 current_text.push('\\');
238 i += 2;
239 pos += 2;
240 }
241 Some('\n') => {
242 i += 2;
245 pos += 2;
246 if current_text.is_empty() {
247 current_text_start = pos;
248 }
249 }
250 Some('\r') => {
251 i += 2;
253 pos += 2;
254 if chars_vec.get(i) == Some(&'\n') {
255 i += 1;
256 pos += 1;
257 }
258 if current_text.is_empty() {
259 current_text_start = pos;
260 }
261 }
262 _ => {
263 if current_text.is_empty() {
267 current_text_start = pos;
268 }
269 current_text.push('\\');
270 i += 1;
271 pos += 1;
272 }
273 }
274 } else if ch == '$' {
275 let part_start = pos;
277 let next = chars_vec.get(i + 1).copied();
278
279 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
280 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
282 i += 2; pos += 2;
284 let mut cmd_content = String::new();
285 let mut depth = 1;
286 while let Some(&c) = chars_vec.get(i) {
287 i += 1;
288 pos += c.len_utf8();
289 if c == '(' {
290 depth += 1;
291 cmd_content.push(c);
292 } else if c == ')' {
293 depth -= 1;
294 if depth == 0 {
295 break;
296 }
297 cmd_content.push(c);
298 } else {
299 cmd_content.push(c);
300 }
301 }
302 let inserted = if let Ok(program) = parse(&cmd_content) {
303 let stmts = strip_empty_stmts(program.statements);
306 if stmts.is_empty() {
307 false
308 } else {
309 parts.push(SpannedPart {
310 part: StringPart::CommandSubst(stmts),
311 offset: base_offset + part_start,
312 len: pos - part_start,
313 });
314 true
315 }
316 } else {
317 false
318 };
319 if inserted {
320 current_text_start = pos;
323 } else {
324 if current_text.is_empty() {
329 current_text_start = part_start;
330 }
331 current_text.push_str("$(");
332 current_text.push_str(&cmd_content);
333 current_text.push(')');
334 }
335 } else if next == Some('{') {
336 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
337 i += 2; pos += 2;
339 let mut var_content = String::new();
340 let mut depth = 1;
341 while let Some(&c) = chars_vec.get(i) {
342 i += 1;
343 pos += c.len_utf8();
344 if c == '{' && var_content.ends_with('$') {
345 depth += 1;
346 var_content.push(c);
347 } else if c == '}' {
348 depth -= 1;
349 if depth == 0 {
350 break;
351 }
352 var_content.push(c);
353 } else {
354 var_content.push(c);
355 }
356 }
357 let part = if let Some(name) = var_content.strip_prefix('#') {
358 StringPart::VarLength(name.to_string())
359 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
360 let expr = var_content
361 .strip_prefix("__ARITH:")
362 .and_then(|s| s.strip_suffix("__"))
363 .unwrap_or("");
364 StringPart::Arithmetic(expr.to_string())
365 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
366 let name = var_content[..colon_idx].to_string();
367 let default_str = &var_content[colon_idx + 2..];
368 let default = parse_interpolated_string(&unquote_default_word(default_str));
373 StringPart::VarWithDefault { name, default }
374 } else {
375 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
376 };
377 parts.push(SpannedPart {
378 part,
379 offset: base_offset + part_start,
380 len: pos - part_start,
381 });
382 current_text_start = pos;
383 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
384 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
385 i += 1; pos += 1;
387 if let Some(&digit) = chars_vec.get(i) {
388 let n = digit.to_digit(10).unwrap_or(0) as usize;
389 i += 1;
390 pos += digit.len_utf8();
391 parts.push(SpannedPart {
392 part: StringPart::Positional(n),
393 offset: base_offset + part_start,
394 len: pos - part_start,
395 });
396 }
397 current_text_start = pos;
398 } else if next == Some('@') {
399 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
400 i += 2; pos += 2;
402 parts.push(SpannedPart {
403 part: StringPart::AllArgs,
404 offset: base_offset + part_start,
405 len: pos - part_start,
406 });
407 current_text_start = pos;
408 } else if next == Some('#') {
409 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
410 i += 2; pos += 2;
412 parts.push(SpannedPart {
413 part: StringPart::ArgCount,
414 offset: base_offset + part_start,
415 len: pos - part_start,
416 });
417 current_text_start = pos;
418 } else if next == Some('?') {
419 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
420 i += 2; pos += 2;
422 parts.push(SpannedPart {
423 part: StringPart::LastExitCode,
424 offset: base_offset + part_start,
425 len: pos - part_start,
426 });
427 current_text_start = pos;
428 } else if next == Some('$') {
429 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
430 i += 2; pos += 2;
432 parts.push(SpannedPart {
433 part: StringPart::CurrentPid,
434 offset: base_offset + part_start,
435 len: pos - part_start,
436 });
437 current_text_start = pos;
438 } else if next.map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
439 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
440 i += 1; pos += 1;
442 let mut var_name = String::new();
443 while let Some(&c) = chars_vec.get(i) {
444 if c.is_ascii_alphanumeric() || c == '_' {
445 var_name.push(c);
446 i += 1;
447 pos += c.len_utf8();
448 } else {
449 break;
450 }
451 }
452 parts.push(SpannedPart {
453 part: StringPart::Var(VarPath::simple(var_name)),
454 offset: base_offset + part_start,
455 len: pos - part_start,
456 });
457 current_text_start = pos;
458 } else {
459 if current_text.is_empty() {
461 current_text_start = pos;
462 }
463 current_text.push(ch);
464 i += 1;
465 pos += 1;
466 }
467 } else {
468 if current_text.is_empty() {
469 current_text_start = pos;
470 }
471 current_text.push(ch);
472 i += 1;
473 pos += ch.len_utf8();
474 }
475 }
476
477 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
478
479 parts
480}
481
482fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
483 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
486
487 let mut parts = Vec::new();
488 let mut current_text = String::new();
489 let mut chars = s.chars().peekable();
490
491 while let Some(ch) = chars.next() {
492 if ch == '\x00' {
493 let mut marker = String::new();
495 while let Some(&c) = chars.peek() {
496 if c == '\x00' {
497 chars.next(); break;
499 }
500 if let Some(c) = chars.next() {
501 marker.push(c);
502 }
503 }
504 if marker == "DOLLAR" {
505 current_text.push('$');
506 }
507 } else if ch == '$' {
508 if chars.peek() == Some(&'(') {
510 if !current_text.is_empty() {
512 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
513 }
514
515 chars.next();
517
518 let mut cmd_content = String::new();
520 let mut paren_depth = 1;
521 for c in chars.by_ref() {
522 if c == '(' {
523 paren_depth += 1;
524 cmd_content.push(c);
525 } else if c == ')' {
526 paren_depth -= 1;
527 if paren_depth == 0 {
528 break;
529 }
530 cmd_content.push(c);
531 } else {
532 cmd_content.push(c);
533 }
534 }
535
536 if let Ok(program) = parse(&cmd_content) {
539 let stmts = strip_empty_stmts(program.statements);
540 if stmts.is_empty() {
541 current_text.push_str("$(");
543 current_text.push_str(&cmd_content);
544 current_text.push(')');
545 } else {
546 parts.push(StringPart::CommandSubst(stmts));
547 }
548 } else {
549 current_text.push_str("$(");
551 current_text.push_str(&cmd_content);
552 current_text.push(')');
553 }
554 } else if chars.peek() == Some(&'{') {
555 if !current_text.is_empty() {
557 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
558 }
559
560 chars.next();
562
563 let mut var_content = String::new();
565 let mut depth = 1;
566 for c in chars.by_ref() {
567 if c == '{' && var_content.ends_with('$') {
568 depth += 1;
569 var_content.push(c);
570 } else if c == '}' {
571 depth -= 1;
572 if depth == 0 {
573 break;
574 }
575 var_content.push(c);
576 } else {
577 var_content.push(c);
578 }
579 }
580
581 let part = if let Some(name) = var_content.strip_prefix('#') {
583 StringPart::VarLength(name.to_string())
585 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
586 let expr = var_content
588 .strip_prefix("__ARITH:")
589 .and_then(|s| s.strip_suffix("__"))
590 .unwrap_or("");
591 StringPart::Arithmetic(expr.to_string())
592 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
593 let name = var_content[..colon_idx].to_string();
595 let default_str = &var_content[colon_idx + 2..];
596 let default = parse_interpolated_string(&unquote_default_word(default_str));
597 StringPart::VarWithDefault { name, default }
598 } else {
599 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
601 };
602 parts.push(part);
603 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
604 if !current_text.is_empty() {
606 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
607 }
608 if let Some(digit) = chars.next() {
609 let n = digit.to_digit(10).unwrap_or(0) as usize;
610 parts.push(StringPart::Positional(n));
611 }
612 } else if chars.peek() == Some(&'@') {
613 if !current_text.is_empty() {
615 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
616 }
617 chars.next(); parts.push(StringPart::AllArgs);
619 } else if chars.peek() == Some(&'#') {
620 if !current_text.is_empty() {
622 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
623 }
624 chars.next(); parts.push(StringPart::ArgCount);
626 } else if chars.peek() == Some(&'?') {
627 if !current_text.is_empty() {
629 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
630 }
631 chars.next(); parts.push(StringPart::LastExitCode);
633 } else if chars.peek() == Some(&'$') {
634 if !current_text.is_empty() {
636 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
637 }
638 chars.next(); parts.push(StringPart::CurrentPid);
640 } else if chars.peek().map(|c| c.is_ascii_alphabetic() || *c == '_').unwrap_or(false) {
641 if !current_text.is_empty() {
643 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
644 }
645
646 let mut var_name = String::new();
648 while let Some(&c) = chars.peek() {
649 if c.is_ascii_alphanumeric() || c == '_' {
650 if let Some(c) = chars.next() {
651 var_name.push(c);
652 }
653 } else {
654 break;
655 }
656 }
657
658 parts.push(StringPart::Var(VarPath::simple(var_name)));
659 } else {
660 current_text.push(ch);
662 }
663 } else {
664 current_text.push(ch);
665 }
666 }
667
668 if !current_text.is_empty() {
669 parts.push(StringPart::Literal(current_text));
670 }
671
672 parts
673}
674
675#[derive(Debug, Clone)]
677pub struct ParseError {
678 pub span: Span,
679 pub message: String,
680}
681
682impl ParseError {
683 pub fn format(&self, source: &str) -> String {
688 let start = self.span.start;
689 let mut line = 1usize;
690 let mut col = 1usize;
691 for (i, ch) in source.char_indices() {
692 if i >= start {
693 break;
694 }
695 if ch == '\n' {
696 line += 1;
697 col = 1;
698 } else {
699 col += 1;
700 }
701 }
702 let line_content = {
703 let line_start = source[..start.min(source.len())]
704 .rfind('\n')
705 .map_or(0, |i| i + 1);
706 let line_end = source[start.min(source.len())..]
707 .find('\n')
708 .map_or(source.len(), |i| start + i);
709 source.get(line_start..line_end).unwrap_or("")
710 };
711 if line_content.is_empty() {
712 format!("{}:{} [parse]: {}", line, col, self.message)
713 } else {
714 format!(
715 "{}:{} [parse]: {}\n | {}",
716 line, col, self.message, line_content
717 )
718 }
719 }
720}
721
722impl std::fmt::Display for ParseError {
723 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
724 write!(f, "{} at {:?}", self.message, self.span)
725 }
726}
727
728impl std::error::Error for ParseError {}
729
730pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
732 let tokens = lexer::tokenize(source).map_err(|errs| {
734 errs.into_iter()
735 .map(|e| ParseError {
736 span: (e.span.start..e.span.end).into(),
737 message: format!("lexer error: {}", e.token),
738 })
739 .collect::<Vec<_>>()
740 })?;
741
742 let tokens: Vec<(Token, Span)> = tokens
744 .into_iter()
745 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
746 .collect();
747
748 let end_span: Span = (source.len()..source.len()).into();
750
751 let parser = program_parser();
753 let result = parser.parse(tokens.as_slice().map(end_span, |(t, s)| (t, s)));
754
755 let program = result.into_result().map_err(|errs| {
756 errs.into_iter()
757 .map(|e| ParseError {
758 span: *e.span(),
759 message: e.to_string(),
760 })
761 .collect::<Vec<_>>()
762 })?;
763
764 if first_ambiguous_stdin(&program.statements) {
769 return Err(vec![ParseError {
770 span: (0..0).into(),
774 message: "multiple stdin redirects on one command are ambiguous; \
775 use exactly one of `<`, `<<`, or `<<<`"
776 .to_string(),
777 }]);
778 }
779
780 Ok(program)
781}
782
783pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
785 let program = parse(source)?;
786 program
787 .statements
788 .into_iter()
789 .find(|s| !matches!(s, Stmt::Empty))
790 .ok_or_else(|| {
791 vec![ParseError {
792 span: (0..source.len()).into(),
793 message: "empty input".to_string(),
794 }]
795 })
796}
797
798fn program_parser<'tokens, 'src: 'tokens, I>(
804) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
805where
806 I: ValueInput<'tokens, Token = Token, Span = Span>,
807{
808 statement_parser()
809 .repeated()
810 .collect::<Vec<_>>()
811 .map(|statements| Program { statements })
812}
813
814fn statement_parser<'tokens, I>(
817) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
818where
819 I: ValueInput<'tokens, Token = Token, Span = Span>,
820{
821 recursive(|stmt| {
822 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
823
824 let break_stmt = just(Token::Break)
826 .ignore_then(
827 select! { Token::Int(n) => n as usize }.or_not()
828 )
829 .map(Stmt::Break);
830
831 let continue_stmt = just(Token::Continue)
833 .ignore_then(
834 select! { Token::Int(n) => n as usize }.or_not()
835 )
836 .map(Stmt::Continue);
837
838 let return_stmt = just(Token::Return)
840 .ignore_then(primary_expr_parser().or_not())
841 .map(|e| Stmt::Return(e.map(Box::new)));
842
843 let exit_stmt = just(Token::Exit)
845 .ignore_then(primary_expr_parser().or_not())
846 .map(|e| Stmt::Exit(e.map(Box::new)));
847
848 let set_flag_arg = choice((
857 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
858 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
859 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
861 ));
862
863 let option_value_str = select! {
867 Token::NumberIdent(s) => s,
868 Token::Int(n) => n.to_string(),
869 Token::Ident(s) => s,
870 };
871
872 let set_option_assign = ident_parser()
876 .then_ignore(just(Token::Eq))
877 .then(option_value_str)
878 .map(|(name, value)| {
879 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
880 });
881
882 let set_quoted_arg = select! {
886 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
887 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
888 };
889
890 let set_with_flags = just(Token::Set)
892 .then(set_flag_arg)
893 .then(
894 choice((
895 set_flag_arg,
896 set_option_assign,
898 set_quoted_arg,
899 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
901 ))
902 .repeated()
903 .collect::<Vec<_>>(),
904 )
905 .map(|((_, first_arg), mut rest_args)| {
906 let mut args = vec![first_arg];
907 args.append(&mut rest_args);
908 Stmt::Command(Command {
909 name: "set".to_string(),
910 args,
911 redirects: vec![],
912 })
913 });
914
915 let set_no_args = just(Token::Set)
918 .then(
919 choice((
920 just(Token::Newline).to(()),
921 just(Token::Semi).to(()),
922 just(Token::And).to(()),
923 just(Token::Or).to(()),
924 end(),
925 ))
926 .rewind(),
927 )
928 .map(|_| Stmt::Command(Command {
929 name: "set".to_string(),
930 args: vec![],
931 redirects: vec![],
932 }));
933
934 let set_command = set_with_flags.or(set_no_args);
938
939 let env_prefix_assign = ident_parser()
946 .then_ignore(just(Token::Eq))
947 .then(expr_parser())
948 .map(|(name, value)| Assignment { name, value, local: false });
949 let env_scoped = env_prefix_assign
950 .repeated()
951 .at_least(1)
952 .collect::<Vec<_>>()
953 .then(pipeline_parser().map(pipeline_into_stmt))
954 .map(|(assignments, body)| Stmt::EnvScoped {
955 assignments,
956 body: Box::new(body),
957 });
958
959 let base_statement = choice((
961 just(Token::Newline).to(Stmt::Empty),
962 set_command,
963 env_scoped,
964 assignment_parser().map(Stmt::Assignment),
965 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), if_parser(stmt.clone()).map(Stmt::If),
969 for_parser(stmt.clone()).map(Stmt::For),
970 while_parser(stmt.clone()).map(Stmt::While),
971 case_parser(stmt.clone()).map(Stmt::Case),
972 break_stmt,
973 continue_stmt,
974 return_stmt,
975 exit_stmt,
976 test_expr_stmt_parser().map(Stmt::Test),
977 pipeline_parser().map(pipeline_into_stmt),
979 ))
980 .boxed();
981
982 let and_chain = base_statement
986 .clone()
987 .foldl(
988 just(Token::And).ignore_then(base_statement).repeated(),
989 |left, right| Stmt::AndChain {
990 left: Box::new(left),
991 right: Box::new(right),
992 },
993 );
994
995 and_chain
996 .clone()
997 .foldl(
998 just(Token::Or).ignore_then(and_chain).repeated(),
999 |left, right| Stmt::OrChain {
1000 left: Box::new(left),
1001 right: Box::new(right),
1002 },
1003 )
1004 .then_ignore(terminator)
1005 })
1006}
1007
1008fn assignment_parser<'tokens, I>(
1010) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1011where
1012 I: ValueInput<'tokens, Token = Token, Span = Span>,
1013{
1014 let local_assignment = just(Token::Local)
1016 .ignore_then(ident_parser())
1017 .then_ignore(just(Token::Eq))
1018 .then(expr_parser())
1019 .map(|(name, value)| Assignment {
1020 name,
1021 value,
1022 local: true,
1023 });
1024
1025 let bash_assignment = ident_parser()
1028 .then_ignore(just(Token::Eq))
1029 .then(expr_parser())
1030 .map(|(name, value)| Assignment {
1031 name,
1032 value,
1033 local: false,
1034 });
1035
1036 choice((local_assignment, bash_assignment))
1037 .labelled("assignment")
1038 .boxed()
1039}
1040
1041fn posix_function_parser<'tokens, I, S>(
1045 stmt: S,
1046) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1047where
1048 I: ValueInput<'tokens, Token = Token, Span = Span>,
1049 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1050{
1051 ident_parser()
1052 .then_ignore(just(Token::LParen))
1053 .then_ignore(just(Token::RParen))
1054 .then_ignore(just(Token::LBrace))
1055 .then_ignore(just(Token::Newline).repeated())
1056 .then(
1057 stmt.repeated()
1058 .collect::<Vec<_>>()
1059 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1060 )
1061 .then_ignore(just(Token::Newline).repeated())
1062 .then_ignore(just(Token::RBrace))
1063 .map(|(name, body)| ToolDef { name, params: vec![], body })
1064 .labelled("POSIX function")
1065 .boxed()
1066}
1067
1068fn bash_function_parser<'tokens, I, S>(
1072 stmt: S,
1073) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1074where
1075 I: ValueInput<'tokens, Token = Token, Span = Span>,
1076 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1077{
1078 just(Token::Function)
1079 .ignore_then(ident_parser())
1080 .then_ignore(just(Token::LBrace))
1081 .then_ignore(just(Token::Newline).repeated())
1082 .then(
1083 stmt.repeated()
1084 .collect::<Vec<_>>()
1085 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1086 )
1087 .then_ignore(just(Token::Newline).repeated())
1088 .then_ignore(just(Token::RBrace))
1089 .map(|(name, body)| ToolDef { name, params: vec![], body })
1090 .labelled("bash function")
1091 .boxed()
1092}
1093
1094fn if_parser<'tokens, I, S>(
1101 stmt: S,
1102) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1103where
1104 I: ValueInput<'tokens, Token = Token, Span = Span>,
1105 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1106{
1107 let branch = condition_parser()
1109 .then_ignore(just(Token::Semi).or_not())
1110 .then_ignore(just(Token::Newline).repeated())
1111 .then_ignore(just(Token::Then))
1112 .then_ignore(just(Token::Newline).repeated())
1113 .then(
1114 stmt.clone()
1115 .repeated()
1116 .collect::<Vec<_>>()
1117 .map(|stmts: Vec<Stmt>| {
1118 stmts
1119 .into_iter()
1120 .filter(|s| !matches!(s, Stmt::Empty))
1121 .collect::<Vec<_>>()
1122 }),
1123 );
1124
1125 let elif_branch = just(Token::Elif)
1127 .ignore_then(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 else_branch = just(Token::Else)
1146 .ignore_then(just(Token::Newline).repeated())
1147 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1148 .map(|stmts: Vec<Stmt>| {
1149 stmts
1150 .into_iter()
1151 .filter(|s| !matches!(s, Stmt::Empty))
1152 .collect::<Vec<_>>()
1153 });
1154
1155 just(Token::If)
1156 .ignore_then(branch)
1157 .then(elif_branch.repeated().collect::<Vec<_>>())
1158 .then(else_branch.or_not())
1159 .then_ignore(just(Token::Fi))
1160 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1161 build_if_chain(condition, then_branch, elif_branches, else_branch)
1163 })
1164 .labelled("if statement")
1165 .boxed()
1166}
1167
1168fn build_if_chain(
1175 condition: Expr,
1176 then_branch: Vec<Stmt>,
1177 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1178 else_branch: Option<Vec<Stmt>>,
1179) -> IfStmt {
1180 if elif_branches.is_empty() {
1181 IfStmt {
1183 condition: Box::new(condition),
1184 then_branch,
1185 else_branch,
1186 }
1187 } else {
1188 let (elif_cond, elif_then) = elif_branches.remove(0);
1190 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1191 IfStmt {
1192 condition: Box::new(condition),
1193 then_branch,
1194 else_branch: Some(vec![Stmt::If(nested_if)]),
1195 }
1196 }
1197}
1198
1199fn for_parser<'tokens, I, S>(
1201 stmt: S,
1202) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1203where
1204 I: ValueInput<'tokens, Token = Token, Span = Span>,
1205 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1206{
1207 just(Token::For)
1208 .ignore_then(ident_parser())
1209 .then_ignore(just(Token::In))
1210 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1211 .then_ignore(just(Token::Semi).or_not())
1212 .then_ignore(just(Token::Newline).repeated())
1213 .then_ignore(just(Token::Do))
1214 .then_ignore(just(Token::Newline).repeated())
1215 .then(
1216 stmt.repeated()
1217 .collect::<Vec<_>>()
1218 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1219 )
1220 .then_ignore(just(Token::Done))
1221 .map(|((variable, items), body)| ForLoop {
1222 variable,
1223 items,
1224 body,
1225 })
1226 .labelled("for loop")
1227 .boxed()
1228}
1229
1230fn while_parser<'tokens, I, S>(
1232 stmt: S,
1233) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1234where
1235 I: ValueInput<'tokens, Token = Token, Span = Span>,
1236 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1237{
1238 just(Token::While)
1239 .ignore_then(condition_parser())
1240 .then_ignore(just(Token::Semi).or_not())
1241 .then_ignore(just(Token::Newline).repeated())
1242 .then_ignore(just(Token::Do))
1243 .then_ignore(just(Token::Newline).repeated())
1244 .then(
1245 stmt.repeated()
1246 .collect::<Vec<_>>()
1247 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1248 )
1249 .then_ignore(just(Token::Done))
1250 .map(|(condition, body)| WhileLoop {
1251 condition: Box::new(condition),
1252 body,
1253 })
1254 .labelled("while loop")
1255 .boxed()
1256}
1257
1258fn case_parser<'tokens, I, S>(
1265 stmt: S,
1266) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1267where
1268 I: ValueInput<'tokens, Token = Token, Span = Span>,
1269 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1270{
1271 let pattern_part = choice((
1274 select! { Token::GlobWord(s) => s },
1275 select! { Token::Ident(s) => s },
1276 select! { Token::NumberIdent(s) => s },
1277 select! { Token::DottedIdent(s) => s },
1278 select! { Token::String(s) => s },
1279 select! { Token::SingleString(s) => s },
1280 select! { Token::Int(n) => n.to_string() },
1281 select! { Token::Star => "*".to_string() },
1282 select! { Token::Question => "?".to_string() },
1283 select! { Token::Dot => ".".to_string() },
1284 select! { Token::DotDot => "..".to_string() },
1285 select! { Token::Tilde => "~".to_string() },
1286 select! { Token::TildePath(s) => s },
1287 select! { Token::RelativePath(s) => s },
1288 select! { Token::DotSlashPath(s) => s },
1289 select! { Token::Path(p) => p },
1290 select! { Token::VarRef(v) => v },
1291 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1292 just(Token::LBracket)
1294 .ignore_then(
1295 choice((
1296 select! { Token::Ident(s) => s },
1297 select! { Token::Int(n) => n.to_string() },
1298 just(Token::Colon).to(":".to_string()),
1299 just(Token::Bang).to("!".to_string()),
1301 select! { Token::ShortFlag(s) => format!("-{}", s) },
1303 ))
1304 .repeated()
1305 .at_least(1)
1306 .collect::<Vec<String>>()
1307 )
1308 .then_ignore(just(Token::RBracket))
1309 .map(|parts| format!("[{}]", parts.join(""))),
1310 just(Token::LBrace)
1312 .ignore_then(
1313 choice((
1314 select! { Token::Ident(s) => s },
1315 select! { Token::Int(n) => n.to_string() },
1316 ))
1317 .separated_by(just(Token::Comma))
1318 .at_least(1)
1319 .collect::<Vec<String>>()
1320 )
1321 .then_ignore(just(Token::RBrace))
1322 .map(|parts| format!("{{{}}}", parts.join(","))),
1323 ));
1324
1325 let pattern = pattern_part
1328 .repeated()
1329 .at_least(1)
1330 .collect::<Vec<String>>()
1331 .map(|parts| parts.join(""))
1332 .labelled("case pattern");
1333
1334 let patterns = pattern
1336 .separated_by(just(Token::Pipe))
1337 .at_least(1)
1338 .collect::<Vec<String>>()
1339 .labelled("case patterns");
1340
1341 let branch = just(Token::LParen)
1343 .or_not()
1344 .ignore_then(just(Token::Newline).repeated())
1345 .ignore_then(patterns)
1346 .then_ignore(just(Token::RParen))
1347 .then_ignore(just(Token::Newline).repeated())
1348 .then(
1349 stmt.clone()
1350 .repeated()
1351 .collect::<Vec<_>>()
1352 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1353 )
1354 .then_ignore(just(Token::DoubleSemi))
1355 .then_ignore(just(Token::Newline).repeated())
1356 .map(|(patterns, body)| CaseBranch { patterns, body })
1357 .labelled("case branch");
1358
1359 just(Token::Case)
1360 .ignore_then(expr_parser())
1361 .then_ignore(just(Token::In))
1362 .then_ignore(just(Token::Newline).repeated())
1363 .then(branch.repeated().collect::<Vec<_>>())
1364 .then_ignore(just(Token::Esac))
1365 .map(|(expr, branches)| CaseStmt { expr, branches })
1366 .labelled("case statement")
1367 .boxed()
1368}
1369
1370fn pipeline_parser<'tokens, I>(
1372) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1373where
1374 I: ValueInput<'tokens, Token = Token, Span = Span>,
1375{
1376 command_parser()
1377 .separated_by(just(Token::Pipe))
1378 .at_least(1)
1379 .collect::<Vec<_>>()
1380 .then(just(Token::Amp).or_not())
1381 .map(|(commands, bg)| Pipeline {
1382 commands,
1383 background: bg.is_some(),
1384 })
1385 .labelled("pipeline")
1386 .boxed()
1387}
1388
1389fn command_parser<'tokens, I>(
1392) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1393where
1394 I: ValueInput<'tokens, Token = Token, Span = Span>,
1395{
1396 let command_name = choice((
1398 ident_parser(),
1399 path_parser(),
1400 select! { Token::DotSlashPath(s) => s },
1401 just(Token::True).to("true".to_string()),
1402 just(Token::False).to("false".to_string()),
1403 just(Token::Dot).to(".".to_string()),
1404 ));
1405
1406 command_name
1416 .then(args_list_parser())
1417 .then(redirect_parser().repeated().collect::<Vec<_>>())
1418 .map(|((name, args), redirects)| Command {
1419 name,
1420 args,
1421 redirects,
1422 })
1423 .labelled("command")
1424 .boxed()
1425}
1426
1427fn pipeline_into_stmt(p: Pipeline) -> Stmt {
1432 if p.commands.len() == 1 && !p.background && p.commands[0].redirects.is_empty() {
1433 match p.commands.into_iter().next() {
1434 Some(cmd) => Stmt::Command(cmd),
1435 None => Stmt::Empty, }
1437 } else {
1438 Stmt::Pipeline(p)
1439 }
1440}
1441
1442fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
1446 cmd.redirects
1447 .iter()
1448 .filter(|r| {
1449 matches!(
1450 r.kind,
1451 RedirectKind::Stdin | RedirectKind::HereDoc | RedirectKind::HereString
1452 )
1453 })
1454 .count()
1455 > 1
1456}
1457
1458fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
1462 stmts.iter().any(stmt_has_ambiguous_stdin)
1463}
1464
1465fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
1466 match stmt {
1467 Stmt::Command(c) => command_has_ambiguous_stdin(c),
1468 Stmt::Pipeline(p) => p.commands.iter().any(command_has_ambiguous_stdin),
1469 Stmt::If(i) => {
1470 first_ambiguous_stdin(&i.then_branch)
1471 || i.else_branch
1472 .as_deref()
1473 .is_some_and(first_ambiguous_stdin)
1474 }
1475 Stmt::For(f) => first_ambiguous_stdin(&f.body),
1476 Stmt::While(w) => first_ambiguous_stdin(&w.body),
1477 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
1478 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
1479 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
1480 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
1481 }
1482 Stmt::EnvScoped { body, .. } => stmt_has_ambiguous_stdin(body),
1483 Stmt::Assignment(_)
1484 | Stmt::Break(_)
1485 | Stmt::Continue(_)
1486 | Stmt::Return(_)
1487 | Stmt::Exit(_)
1488 | Stmt::Test(_)
1489 | Stmt::Empty => false,
1490 }
1491}
1492
1493fn args_list_parser<'tokens, I>(
1497) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1498where
1499 I: ValueInput<'tokens, Token = Token, Span = Span>,
1500{
1501 let pre_dash = arg_before_double_dash_parser()
1508 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1509 .repeated()
1510 .collect::<Vec<(Arg, Span)>>()
1511 .try_map(|args, _span| {
1512 for pair in args.windows(2) {
1513 let (prev, prev_span) = &pair[0];
1514 let (next, next_span) = &pair[1];
1515 if matches!(prev, Arg::Positional(_))
1516 && matches!(next, Arg::Positional(_))
1517 && prev_span.end == next_span.start
1518 {
1519 return Err(Rich::custom(
1520 *next_span,
1521 "adjacent words with no space between them are not joined into one \
1522 argument (kaish does no token pasting); quote the whole word, e.g. \
1523 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\"",
1524 ));
1525 }
1526 }
1527 Ok(args.into_iter().map(|(arg, _)| arg).collect::<Vec<Arg>>())
1528 });
1529
1530 let double_dash = select! {
1532 Token::DoubleDash => Arg::DoubleDash,
1533 };
1534
1535 let post_dash_arg = choice((
1537 select! {
1539 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
1540 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
1541 },
1542 primary_expr_parser().map(Arg::Positional),
1544 ));
1545
1546 let post_dash = post_dash_arg.repeated().collect::<Vec<_>>();
1547
1548 pre_dash
1550 .then(double_dash.then(post_dash).or_not())
1551 .map(|(mut args, maybe_dd)| {
1552 if let Some((dd, post)) = maybe_dd {
1553 args.push(dd);
1554 args.extend(post);
1555 }
1556 args
1557 })
1558}
1559
1560fn arg_before_double_dash_parser<'tokens, I>(
1562) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1563where
1564 I: ValueInput<'tokens, Token = Token, Span = Span>,
1565{
1566 let long_flag_with_value = select! {
1568 Token::LongFlag(name) => name,
1569 }
1570 .then_ignore(just(Token::Eq))
1571 .then(primary_expr_parser())
1572 .map(|(key, value)| Arg::Named { key, value });
1573
1574 let long_flag = select! {
1576 Token::LongFlag(name) => Arg::LongFlag(name),
1577 };
1578
1579 let short_flag = select! {
1581 Token::ShortFlag(name) => Arg::ShortFlag(name),
1582 };
1583
1584 let named = select! {
1590 Token::Ident(s) => s,
1591 }
1592 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1593 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1594 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1595 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1596 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1598 Err(Rich::custom(
1599 span,
1600 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
1601 ))
1602 } else {
1603 Ok(Arg::WordAssign { key, value })
1604 }
1605 });
1606
1607 let positional = primary_expr_parser().map(Arg::Positional);
1609
1610 choice((
1613 long_flag_with_value,
1614 long_flag,
1615 short_flag,
1616 named,
1617 positional,
1618 ))
1619 .boxed()
1620}
1621
1622fn redirect_parser<'tokens, I>(
1624) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1625where
1626 I: ValueInput<'tokens, Token = Token, Span = Span>,
1627{
1628 let regular_redirect = select! {
1630 Token::GtGt => RedirectKind::StdoutAppend,
1631 Token::Gt => RedirectKind::StdoutOverwrite,
1632 Token::Lt => RedirectKind::Stdin,
1633 Token::Stderr => RedirectKind::Stderr,
1634 Token::Both => RedirectKind::Both,
1635 }
1636 .then(primary_expr_parser())
1637 .map(|(kind, target)| Redirect { kind, target });
1638
1639 let heredoc_redirect = just(Token::HereDocStart)
1647 .ignore_then(select! { Token::HereDoc(data) => data })
1648 .map(|data: HereDocData| {
1649 let target = if data.literal {
1650 let body = if data.strip_tabs {
1651 crate::interpreter::strip_leading_tabs(&data.content)
1652 } else {
1653 data.content
1654 };
1655 Expr::Literal(Value::String(body))
1656 } else {
1657 let parts = parse_interpolated_string_spanned(
1658 &data.content,
1659 data.body_start_offset,
1660 );
1661 if parts.len() == 1 && !data.strip_tabs {
1665 if let StringPart::Literal(text) = &parts[0].part {
1666 return Redirect {
1667 kind: RedirectKind::HereDoc,
1668 target: Expr::Literal(Value::String(text.clone())),
1669 };
1670 }
1671 }
1672 Expr::HereDocBody {
1673 parts,
1674 strip_tabs: data.strip_tabs,
1675 }
1676 };
1677 Redirect {
1678 kind: RedirectKind::HereDoc,
1679 target,
1680 }
1681 });
1682
1683 let herestring_redirect = just(Token::HereString)
1687 .ignore_then(primary_expr_parser())
1688 .map(|target| Redirect {
1689 kind: RedirectKind::HereString,
1690 target,
1691 });
1692
1693 let merge_stderr_redirect = just(Token::StderrToStdout)
1695 .map(|_| Redirect {
1696 kind: RedirectKind::MergeStderr,
1697 target: Expr::Literal(Value::Null),
1699 });
1700
1701 let merge_stdout_redirect = choice((
1703 just(Token::StdoutToStderr),
1704 just(Token::StdoutToStderr2),
1705 ))
1706 .map(|_| Redirect {
1707 kind: RedirectKind::MergeStdout,
1708 target: Expr::Literal(Value::Null),
1710 });
1711
1712 choice((
1713 heredoc_redirect,
1714 herestring_redirect,
1715 merge_stderr_redirect,
1716 merge_stdout_redirect,
1717 regular_redirect,
1718 ))
1719 .labelled("redirect")
1720 .boxed()
1721}
1722
1723fn test_expr_stmt_parser<'tokens, I>(
1733) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1734where
1735 I: ValueInput<'tokens, Token = Token, Span = Span>,
1736{
1737 let file_test_op = select! {
1739 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
1740 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
1741 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
1742 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
1743 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
1744 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
1745 };
1746
1747 let string_test_op = select! {
1749 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
1750 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
1751 };
1752
1753 let cmp_op = choice((
1756 just(Token::EqEq).to(TestCmpOp::Eq),
1757 just(Token::Eq).to(TestCmpOp::Eq),
1758 just(Token::NotEq).to(TestCmpOp::NotEq),
1759 just(Token::Match).to(TestCmpOp::Match),
1760 just(Token::NotMatch).to(TestCmpOp::NotMatch),
1761 just(Token::Gt).to(TestCmpOp::Gt),
1762 just(Token::Lt).to(TestCmpOp::Lt),
1763 just(Token::GtEq).to(TestCmpOp::GtEq),
1764 just(Token::LtEq).to(TestCmpOp::LtEq),
1765 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
1766 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
1767 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
1768 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
1769 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
1770 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
1771 ));
1772
1773 let file_test = file_test_op
1775 .then(primary_expr_parser())
1776 .map(|(op, path)| TestExpr::FileTest {
1777 op,
1778 path: Box::new(path),
1779 });
1780
1781 let string_test = string_test_op
1783 .then(primary_expr_parser())
1784 .map(|(op, value)| TestExpr::StringTest {
1785 op,
1786 value: Box::new(value),
1787 });
1788
1789 let comparison = primary_expr_parser()
1791 .then(cmp_op)
1792 .then(primary_expr_parser())
1793 .map(|((left, op), right)| TestExpr::Comparison {
1794 left: Box::new(left),
1795 op,
1796 right: Box::new(right),
1797 });
1798
1799 let primary_test = choice((file_test, string_test, comparison));
1801
1802 let unary = recursive(|unary| {
1816 let not_expr = just(Token::Bang)
1817 .ignore_then(unary)
1818 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
1819 choice((not_expr, primary_test.clone()))
1820 });
1821
1822 let and_expr = unary.clone().foldl(
1824 just(Token::And).ignore_then(unary).repeated(),
1825 |left, right| TestExpr::And {
1826 left: Box::new(left),
1827 right: Box::new(right),
1828 },
1829 );
1830
1831 let compound_test = and_expr.clone().foldl(
1833 just(Token::Or).ignore_then(and_expr).repeated(),
1834 |left, right| TestExpr::Or {
1835 left: Box::new(left),
1836 right: Box::new(right),
1837 },
1838 );
1839
1840 just(Token::LBracket)
1843 .then(just(Token::LBracket))
1844 .ignore_then(compound_test)
1845 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
1846 .labelled("test expression")
1847 .boxed()
1848}
1849
1850fn condition_parser<'tokens, I>(
1865) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1866where
1867 I: ValueInput<'tokens, Token = Token, Span = Span>,
1868{
1869 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
1871
1872 let command_condition = command_parser().map(Expr::Command);
1875
1876 let base = choice((test_expr_condition, command_condition));
1878
1879 let and_expr = base.clone().foldl(
1882 just(Token::And).ignore_then(base).repeated(),
1883 |left, right| Expr::BinaryOp {
1884 left: Box::new(left),
1885 op: BinaryOp::And,
1886 right: Box::new(right),
1887 },
1888 );
1889
1890 and_expr
1892 .clone()
1893 .foldl(
1894 just(Token::Or).ignore_then(and_expr).repeated(),
1895 |left, right| Expr::BinaryOp {
1896 left: Box::new(left),
1897 op: BinaryOp::Or,
1898 right: Box::new(right),
1899 },
1900 )
1901 .labelled("condition")
1902 .boxed()
1903}
1904
1905fn expr_parser<'tokens, I>(
1907) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1908where
1909 I: ValueInput<'tokens, Token = Token, Span = Span>,
1910{
1911 primary_expr_parser()
1913}
1914
1915fn primary_expr_parser<'tokens, I>(
1919) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1920where
1921 I: ValueInput<'tokens, Token = Token, Span = Span>,
1922{
1923 let positional = select! {
1925 Token::Positional(n) => Expr::Positional(n),
1926 Token::AllArgs => Expr::AllArgs,
1927 Token::ArgCount => Expr::ArgCount,
1928 Token::VarLength(name) => Expr::VarLength(name),
1929 Token::LastExitCode => Expr::LastExitCode,
1930 Token::CurrentPid => Expr::CurrentPid,
1931 };
1932
1933 let arithmetic = select! {
1935 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
1936 };
1937
1938 let keyword_as_bareword = select! {
1941 Token::Done => "done",
1942 Token::Fi => "fi",
1943 Token::Then => "then",
1944 Token::Else => "else",
1945 Token::Elif => "elif",
1946 Token::In => "in",
1947 Token::Do => "do",
1948 Token::Esac => "esac",
1949 Token::Set => "set",
1954 }
1955 .map(|s| Expr::Literal(Value::String(s.to_string())));
1956
1957 let plus_minus_bare = select! {
1959 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
1960 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
1961 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
1962 };
1963
1964 let glob_pattern = select! {
1966 Token::GlobWord(s) => Expr::GlobPattern(s),
1967 Token::Star => Expr::GlobPattern("*".to_string()),
1968 Token::Question => Expr::GlobPattern("?".to_string()),
1969 };
1970
1971 recursive(|expr| {
1972 choice((
1973 positional,
1974 arithmetic,
1975 cmd_subst_parser(expr.clone()),
1976 var_expr_parser(),
1977 interpolated_string_parser(),
1978 literal_parser().map(Expr::Literal),
1979 glob_pattern,
1981 ident_parser().map(|s| Expr::Literal(Value::String(s))),
1983 path_parser().map(|s| Expr::Literal(Value::String(s))),
1985 select! {
1988 Token::Dot => Expr::Literal(Value::String(".".into())),
1994 Token::DotDot => Expr::Literal(Value::String("..".into())),
1995 Token::Tilde => Expr::Literal(Value::String("~".into())),
1996 Token::TildePath(s) => Expr::Literal(Value::String(s)),
1997 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
1998 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
1999 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
2001 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
2006 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
2009 },
2010 plus_minus_bare,
2011 keyword_as_bareword,
2013 ))
2014 .labelled("expression")
2015 })
2016 .boxed()
2017}
2018
2019fn var_expr_parser<'tokens, I>(
2022) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2023where
2024 I: ValueInput<'tokens, Token = Token, Span = Span>,
2025{
2026 select! {
2027 Token::VarRef(raw) => parse_var_expr(&raw),
2028 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
2029 }
2030 .labelled("variable reference")
2031}
2032
2033fn cmd_subst_parser<'tokens, I, E>(
2037 expr: E,
2038) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2039where
2040 I: ValueInput<'tokens, Token = Token, Span = Span>,
2041 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone,
2042{
2043 let long_flag_with_value = select! {
2046 Token::LongFlag(name) => name,
2047 }
2048 .then_ignore(just(Token::Eq))
2049 .then(expr.clone())
2050 .map(|(key, value)| Arg::Named { key, value });
2051
2052 let long_flag = select! {
2054 Token::LongFlag(name) => Arg::LongFlag(name),
2055 };
2056
2057 let short_flag = select! {
2059 Token::ShortFlag(name) => Arg::ShortFlag(name),
2060 };
2061
2062 let named = ident_parser()
2064 .then_ignore(just(Token::Eq))
2065 .then(expr.clone())
2066 .map(|(key, value)| Arg::WordAssign { key, value });
2067
2068 let positional = expr.map(Arg::Positional);
2070
2071 let arg = choice((
2072 long_flag_with_value,
2073 long_flag,
2074 short_flag,
2075 named,
2076 positional,
2077 ));
2078
2079 let command_name = choice((
2081 ident_parser(),
2082 just(Token::True).to("true".to_string()),
2083 just(Token::False).to("false".to_string()),
2084 ));
2085
2086 let command = command_name
2088 .then(arg.repeated().collect::<Vec<_>>())
2089 .map(|(name, args)| Command {
2090 name,
2091 args,
2092 redirects: vec![],
2093 });
2094
2095 let pipeline = command
2097 .separated_by(just(Token::Pipe))
2098 .at_least(1)
2099 .collect::<Vec<_>>()
2100 .map(|commands| Pipeline {
2101 commands,
2102 background: false,
2103 });
2104
2105 let pipeline_stmt = pipeline.map(pipeline_into_stmt);
2108
2109 let and_chain = pipeline_stmt.clone().foldl(
2118 just(Token::And).ignore_then(pipeline_stmt.clone()).repeated(),
2119 |left, right| Stmt::AndChain {
2120 left: Box::new(left),
2121 right: Box::new(right),
2122 },
2123 );
2124 let chained = and_chain.clone().foldl(
2125 just(Token::Or).ignore_then(and_chain).repeated(),
2126 |left, right| Stmt::OrChain {
2127 left: Box::new(left),
2128 right: Box::new(right),
2129 },
2130 );
2131
2132 let separator = choice((just(Token::Newline), just(Token::Semi)));
2137 let body = separator
2138 .clone()
2139 .repeated()
2140 .ignore_then(
2141 chained
2142 .separated_by(separator.clone().repeated().at_least(1))
2143 .allow_trailing()
2144 .collect::<Vec<_>>(),
2145 )
2146 .then_ignore(separator.repeated());
2147
2148 just(Token::CmdSubstStart)
2149 .ignore_then(body)
2150 .then_ignore(just(Token::RParen))
2151 .map(Expr::CommandSubst)
2152 .labelled("command substitution")
2153}
2154
2155fn interpolated_string_parser<'tokens, I>(
2157) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2158where
2159 I: ValueInput<'tokens, Token = Token, Span = Span>,
2160{
2161 let double_quoted = select! {
2163 Token::String(s) => s,
2164 }
2165 .map(|s| {
2166 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
2168 let parts = parse_interpolated_string(&s);
2170 if parts.len() == 1
2171 && let StringPart::Literal(text) = &parts[0] {
2172 return Expr::Literal(Value::String(text.clone()));
2173 }
2174 Expr::Interpolated(parts)
2175 } else {
2176 Expr::Literal(Value::String(s))
2177 }
2178 });
2179
2180 let single_quoted = select! {
2182 Token::SingleString(s) => Expr::Literal(Value::String(s)),
2183 };
2184
2185 choice((single_quoted, double_quoted)).labelled("string")
2186}
2187
2188fn literal_parser<'tokens, I>(
2190) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2191where
2192 I: ValueInput<'tokens, Token = Token, Span = Span>,
2193{
2194 choice((
2195 select! {
2196 Token::True => Value::Bool(true),
2197 Token::False => Value::Bool(false),
2198 },
2199 select! {
2200 Token::Int(n) => Value::Int(n),
2201 Token::Float(f) => Value::Float(f),
2202 },
2203 ))
2204 .labelled("literal")
2205 .boxed()
2206}
2207
2208fn ident_parser<'tokens, I>(
2210) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2211where
2212 I: ValueInput<'tokens, Token = Token, Span = Span>,
2213{
2214 select! {
2215 Token::Ident(s) => s,
2216 }
2217 .labelled("identifier")
2218}
2219
2220fn path_parser<'tokens, I>(
2222) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2223where
2224 I: ValueInput<'tokens, Token = Token, Span = Span>,
2225{
2226 select! {
2227 Token::Path(s) => s,
2228 }
2229 .labelled("path")
2230}
2231
2232#[cfg(test)]
2233mod tests {
2234 use super::*;
2235
2236 fn subst_cmd(expr: &Expr) -> &Command {
2238 match expr {
2239 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2240 [Stmt::Command(cmd)] => cmd,
2241 other => panic!("expected a single command in $(), got {other:?}"),
2242 },
2243 other => panic!("expected command subst, got {other:?}"),
2244 }
2245 }
2246
2247 fn subst_pipeline(expr: &Expr) -> &Pipeline {
2249 match expr {
2250 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2251 [Stmt::Pipeline(p)] => p,
2252 other => panic!("expected a single pipeline in $(), got {other:?}"),
2253 },
2254 other => panic!("expected command subst, got {other:?}"),
2255 }
2256 }
2257
2258 #[test]
2259 fn parse_empty() {
2260 let result = parse("");
2261 assert!(result.is_ok());
2262 assert_eq!(result.expect("ok").statements.len(), 0);
2263 }
2264
2265 #[test]
2266 fn parse_newlines_only() {
2267 let result = parse("\n\n\n");
2268 assert!(result.is_ok());
2269 }
2270
2271 #[test]
2272 fn parse_simple_command() {
2273 let result = parse("echo");
2274 assert!(result.is_ok());
2275 let program = result.expect("ok");
2276 assert_eq!(program.statements.len(), 1);
2277 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2278 }
2279
2280 #[test]
2281 fn parse_command_with_string_arg() {
2282 let result = parse(r#"echo "hello""#);
2283 assert!(result.is_ok());
2284 let program = result.expect("ok");
2285 match &program.statements[0] {
2286 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
2287 _ => panic!("expected Command"),
2288 }
2289 }
2290
2291 #[test]
2292 fn parse_assignment() {
2293 let result = parse("X=5");
2294 assert!(result.is_ok());
2295 let program = result.expect("ok");
2296 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2297 }
2298
2299 #[test]
2300 fn parse_pipeline() {
2301 let result = parse("a | b | c");
2302 assert!(result.is_ok());
2303 let program = result.expect("ok");
2304 match &program.statements[0] {
2305 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2306 _ => panic!("expected Pipeline"),
2307 }
2308 }
2309
2310 #[test]
2311 fn parse_background_job() {
2312 let result = parse("cmd &");
2313 assert!(result.is_ok());
2314 let program = result.expect("ok");
2315 match &program.statements[0] {
2316 Stmt::Pipeline(p) => assert!(p.background),
2317 _ => panic!("expected Pipeline with background"),
2318 }
2319 }
2320
2321 #[test]
2322 fn parse_if_simple() {
2323 let result = parse("if true; then echo; fi");
2324 assert!(result.is_ok());
2325 let program = result.expect("ok");
2326 assert!(matches!(&program.statements[0], Stmt::If(_)));
2327 }
2328
2329 #[test]
2330 fn parse_if_else() {
2331 let result = parse("if true; then echo; else echo; fi");
2332 assert!(result.is_ok());
2333 let program = result.expect("ok");
2334 match &program.statements[0] {
2335 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
2336 _ => panic!("expected If"),
2337 }
2338 }
2339
2340 #[test]
2341 fn parse_elif_simple() {
2342 let result = parse("if true; then echo a; elif false; then echo b; fi");
2343 assert!(result.is_ok(), "parse failed: {:?}", result);
2344 let program = result.expect("ok");
2345 match &program.statements[0] {
2346 Stmt::If(if_stmt) => {
2347 assert!(if_stmt.else_branch.is_some());
2349 let else_branch = if_stmt.else_branch.as_ref().unwrap();
2350 assert_eq!(else_branch.len(), 1);
2351 assert!(matches!(&else_branch[0], Stmt::If(_)));
2352 }
2353 _ => panic!("expected If"),
2354 }
2355 }
2356
2357 #[test]
2358 fn parse_elif_with_else() {
2359 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
2360 assert!(result.is_ok(), "parse failed: {:?}", result);
2361 let program = result.expect("ok");
2362 match &program.statements[0] {
2363 Stmt::If(outer_if) => {
2364 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
2366 assert_eq!(else_branch.len(), 1);
2367 match &else_branch[0] {
2368 Stmt::If(inner_if) => {
2369 assert!(inner_if.else_branch.is_some());
2371 }
2372 _ => panic!("expected nested If from elif"),
2373 }
2374 }
2375 _ => panic!("expected If"),
2376 }
2377 }
2378
2379 #[test]
2380 fn parse_multiple_elif() {
2381 let result = parse(
2383 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
2384 );
2385 assert!(result.is_ok(), "parse failed: {:?}", result);
2386 }
2387
2388 #[test]
2389 fn parse_for_loop() {
2390 let result = parse("for X in items; do echo; done");
2391 assert!(result.is_ok());
2392 let program = result.expect("ok");
2393 assert!(matches!(&program.statements[0], Stmt::For(_)));
2394 }
2395
2396 #[test]
2397 fn parse_brackets_not_array_literal() {
2398 let result = parse("cmd [1");
2400 let _ = result;
2403 }
2404
2405 #[test]
2406 fn parse_named_arg() {
2407 let result = parse("cmd foo=5");
2411 assert!(result.is_ok());
2412 let program = result.expect("ok");
2413 match &program.statements[0] {
2414 Stmt::Command(cmd) => {
2415 assert_eq!(cmd.args.len(), 1);
2416 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
2417 }
2418 _ => panic!("expected Command"),
2419 }
2420 }
2421
2422 #[test]
2423 fn parse_short_flag() {
2424 let result = parse("ls -l");
2425 assert!(result.is_ok());
2426 let program = result.expect("ok");
2427 match &program.statements[0] {
2428 Stmt::Command(cmd) => {
2429 assert_eq!(cmd.name, "ls");
2430 assert_eq!(cmd.args.len(), 1);
2431 match &cmd.args[0] {
2432 Arg::ShortFlag(name) => assert_eq!(name, "l"),
2433 _ => panic!("expected ShortFlag"),
2434 }
2435 }
2436 _ => panic!("expected Command"),
2437 }
2438 }
2439
2440 #[test]
2441 fn parse_long_flag() {
2442 let result = parse("git push --force");
2443 assert!(result.is_ok());
2444 let program = result.expect("ok");
2445 match &program.statements[0] {
2446 Stmt::Command(cmd) => {
2447 assert_eq!(cmd.name, "git");
2448 assert_eq!(cmd.args.len(), 2);
2449 match &cmd.args[0] {
2450 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
2451 _ => panic!("expected Positional push"),
2452 }
2453 match &cmd.args[1] {
2454 Arg::LongFlag(name) => assert_eq!(name, "force"),
2455 _ => panic!("expected LongFlag"),
2456 }
2457 }
2458 _ => panic!("expected Command"),
2459 }
2460 }
2461
2462 #[test]
2463 fn parse_long_flag_with_value() {
2464 let result = parse(r#"git commit --message="hello""#);
2465 assert!(result.is_ok());
2466 let program = result.expect("ok");
2467 match &program.statements[0] {
2468 Stmt::Command(cmd) => {
2469 assert_eq!(cmd.name, "git");
2470 assert_eq!(cmd.args.len(), 2);
2471 match &cmd.args[1] {
2472 Arg::Named { key, value } => {
2473 assert_eq!(key, "message");
2474 match value {
2475 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
2476 _ => panic!("expected String value"),
2477 }
2478 }
2479 _ => panic!("expected Named from --flag=value"),
2480 }
2481 }
2482 _ => panic!("expected Command"),
2483 }
2484 }
2485
2486 #[test]
2487 fn parse_mixed_flags_and_args() {
2488 let result = parse(r#"git commit -m "message" --amend"#);
2489 assert!(result.is_ok());
2490 let program = result.expect("ok");
2491 match &program.statements[0] {
2492 Stmt::Command(cmd) => {
2493 assert_eq!(cmd.name, "git");
2494 assert_eq!(cmd.args.len(), 4);
2495 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
2497 match &cmd.args[1] {
2499 Arg::ShortFlag(name) => assert_eq!(name, "m"),
2500 _ => panic!("expected ShortFlag -m"),
2501 }
2502 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
2504 match &cmd.args[3] {
2506 Arg::LongFlag(name) => assert_eq!(name, "amend"),
2507 _ => panic!("expected LongFlag --amend"),
2508 }
2509 }
2510 _ => panic!("expected Command"),
2511 }
2512 }
2513
2514 #[test]
2515 fn parse_redirect_stdout() {
2516 let result = parse("cmd > file");
2517 assert!(result.is_ok());
2518 let program = result.expect("ok");
2519 match &program.statements[0] {
2521 Stmt::Pipeline(p) => {
2522 assert_eq!(p.commands.len(), 1);
2523 let cmd = &p.commands[0];
2524 assert_eq!(cmd.redirects.len(), 1);
2525 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
2526 }
2527 _ => panic!("expected Pipeline"),
2528 }
2529 }
2530
2531 #[test]
2532 fn parse_var_ref() {
2533 let result = parse("echo ${VAR}");
2534 assert!(result.is_ok());
2535 let program = result.expect("ok");
2536 match &program.statements[0] {
2537 Stmt::Command(cmd) => {
2538 assert_eq!(cmd.args.len(), 1);
2539 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
2540 }
2541 _ => panic!("expected Command"),
2542 }
2543 }
2544
2545 #[test]
2546 fn parse_multiple_statements() {
2547 let result = parse("a\nb\nc");
2548 assert!(result.is_ok());
2549 let program = result.expect("ok");
2550 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2551 assert_eq!(non_empty.len(), 3);
2552 }
2553
2554 #[test]
2555 fn parse_semicolon_separated() {
2556 let result = parse("a; b; c");
2557 assert!(result.is_ok());
2558 let program = result.expect("ok");
2559 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2560 assert_eq!(non_empty.len(), 3);
2561 }
2562
2563 #[test]
2564 fn parse_complex_pipeline() {
2565 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
2566 assert!(result.is_ok());
2567 let program = result.expect("ok");
2568 match &program.statements[0] {
2569 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2570 _ => panic!("expected Pipeline"),
2571 }
2572 }
2573
2574 #[test]
2575 fn parse_json_as_string_arg() {
2576 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
2578 assert!(result.is_ok());
2579 }
2580
2581 #[test]
2582 fn parse_mixed_args() {
2583 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
2584 assert!(result.is_ok());
2585 let program = result.expect("ok");
2586 match &program.statements[0] {
2587 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
2588 _ => panic!("expected Command"),
2589 }
2590 }
2591
2592 #[test]
2593 fn error_unterminated_string() {
2594 let result = parse(r#"echo "hello"#);
2595 assert!(result.is_err());
2596 }
2597
2598 #[test]
2599 fn error_unterminated_var_ref() {
2600 let result = parse("echo ${VAR");
2601 assert!(result.is_err());
2602 }
2603
2604 #[test]
2605 fn error_missing_fi() {
2606 let result = parse("if true; then echo");
2607 assert!(result.is_err());
2608 }
2609
2610 #[test]
2611 fn error_missing_done() {
2612 let result = parse("for X in items; do echo");
2613 assert!(result.is_err());
2614 }
2615
2616 #[test]
2617 fn parse_nested_cmd_subst() {
2618 let result = parse("X=$(echo $(date))").unwrap();
2620 match &result.statements[0] {
2621 Stmt::Assignment(a) => {
2622 assert_eq!(a.name, "X");
2623 let outer = subst_cmd(&a.value);
2624 assert_eq!(outer.name, "echo");
2625 match &outer.args[0] {
2627 Arg::Positional(inner_expr) => {
2628 assert_eq!(subst_cmd(inner_expr).name, "date");
2629 }
2630 other => panic!("expected nested cmd subst arg, got {:?}", other),
2631 }
2632 }
2633 other => panic!("expected assignment, got {:?}", other),
2634 }
2635 }
2636
2637 #[test]
2638 fn parse_deeply_nested_cmd_subst() {
2639 let result = parse("X=$(a $(b $(c)))").unwrap();
2641 match &result.statements[0] {
2642 Stmt::Assignment(a) => {
2643 let level1 = subst_cmd(&a.value);
2644 assert_eq!(level1.name, "a");
2645 match &level1.args[0] {
2646 Arg::Positional(level2_expr) => {
2647 let level2 = subst_cmd(level2_expr);
2648 assert_eq!(level2.name, "b");
2649 match &level2.args[0] {
2650 Arg::Positional(level3_expr) => {
2651 assert_eq!(subst_cmd(level3_expr).name, "c");
2652 }
2653 other => panic!("expected level3 cmd subst, got {:?}", other),
2654 }
2655 }
2656 other => panic!("expected level2 cmd subst, got {:?}", other),
2657 }
2658 }
2659 other => panic!("expected assignment, got {:?}", other),
2660 }
2661 }
2662
2663 #[test]
2668 fn value_int_preserved() {
2669 let result = parse("X=42").unwrap();
2670 match &result.statements[0] {
2671 Stmt::Assignment(a) => {
2672 assert_eq!(a.name, "X");
2673 match &a.value {
2674 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2675 other => panic!("expected int literal, got {:?}", other),
2676 }
2677 }
2678 other => panic!("expected assignment, got {:?}", other),
2679 }
2680 }
2681
2682 #[test]
2683 fn value_negative_int_preserved() {
2684 let result = parse("X=-99").unwrap();
2685 match &result.statements[0] {
2686 Stmt::Assignment(a) => match &a.value {
2687 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
2688 other => panic!("expected int, got {:?}", other),
2689 },
2690 other => panic!("expected assignment, got {:?}", other),
2691 }
2692 }
2693
2694 #[test]
2695 fn value_float_preserved() {
2696 let result = parse("PI=3.14").unwrap();
2697 match &result.statements[0] {
2698 Stmt::Assignment(a) => match &a.value {
2699 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
2700 other => panic!("expected float, got {:?}", other),
2701 },
2702 other => panic!("expected assignment, got {:?}", other),
2703 }
2704 }
2705
2706 #[test]
2707 fn value_string_preserved() {
2708 let result = parse(r#"echo "hello world""#).unwrap();
2709 match &result.statements[0] {
2710 Stmt::Command(cmd) => {
2711 assert_eq!(cmd.name, "echo");
2712 match &cmd.args[0] {
2713 Arg::Positional(Expr::Literal(Value::String(s))) => {
2714 assert_eq!(s, "hello world");
2715 }
2716 other => panic!("expected string arg, got {:?}", other),
2717 }
2718 }
2719 other => panic!("expected command, got {:?}", other),
2720 }
2721 }
2722
2723 #[test]
2724 fn value_string_with_escapes_preserved() {
2725 let result = parse(r#"echo "line1\nline2""#).unwrap();
2726 match &result.statements[0] {
2727 Stmt::Command(cmd) => match &cmd.args[0] {
2728 Arg::Positional(Expr::Literal(Value::String(s))) => {
2729 assert_eq!(s, "line1\nline2");
2730 }
2731 other => panic!("expected string, got {:?}", other),
2732 },
2733 other => panic!("expected command, got {:?}", other),
2734 }
2735 }
2736
2737 #[test]
2738 fn value_command_name_preserved() {
2739 let result = parse("my-command").unwrap();
2740 match &result.statements[0] {
2741 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
2742 other => panic!("expected command, got {:?}", other),
2743 }
2744 }
2745
2746 #[test]
2747 fn value_assignment_name_preserved() {
2748 let result = parse("MY_VAR=1").unwrap();
2749 match &result.statements[0] {
2750 Stmt::Assignment(a) => assert_eq!(a.name, "MY_VAR"),
2751 other => panic!("expected assignment, got {:?}", other),
2752 }
2753 }
2754
2755 #[test]
2756 fn value_for_variable_preserved() {
2757 let result = parse("for ITEM in items; do echo; done").unwrap();
2758 match &result.statements[0] {
2759 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
2760 other => panic!("expected for, got {:?}", other),
2761 }
2762 }
2763
2764 #[test]
2765 fn value_varref_name_preserved() {
2766 let result = parse("echo ${MESSAGE}").unwrap();
2767 match &result.statements[0] {
2768 Stmt::Command(cmd) => match &cmd.args[0] {
2769 Arg::Positional(Expr::VarRef(path)) => {
2770 assert_eq!(path.segments.len(), 1);
2771 let VarSegment::Field(name) = &path.segments[0];
2772 assert_eq!(name, "MESSAGE");
2773 }
2774 other => panic!("expected varref, got {:?}", other),
2775 },
2776 other => panic!("expected command, got {:?}", other),
2777 }
2778 }
2779
2780 #[test]
2781 fn value_varref_field_access_preserved() {
2782 let result = parse("echo ${RESULT.data}").unwrap();
2783 match &result.statements[0] {
2784 Stmt::Command(cmd) => match &cmd.args[0] {
2785 Arg::Positional(Expr::VarRef(path)) => {
2786 assert_eq!(path.segments.len(), 2);
2787 let VarSegment::Field(a) = &path.segments[0];
2788 let VarSegment::Field(b) = &path.segments[1];
2789 assert_eq!(a, "RESULT");
2790 assert_eq!(b, "data");
2791 }
2792 other => panic!("expected varref, got {:?}", other),
2793 },
2794 other => panic!("expected command, got {:?}", other),
2795 }
2796 }
2797
2798 #[test]
2799 fn value_varref_index_ignored() {
2800 let result = parse("echo ${ITEMS[0]}").unwrap();
2802 match &result.statements[0] {
2803 Stmt::Command(cmd) => match &cmd.args[0] {
2804 Arg::Positional(Expr::VarRef(path)) => {
2805 assert_eq!(path.segments.len(), 1);
2807 let VarSegment::Field(name) = &path.segments[0];
2808 assert_eq!(name, "ITEMS");
2809 }
2810 other => panic!("expected varref, got {:?}", other),
2811 },
2812 other => panic!("expected command, got {:?}", other),
2813 }
2814 }
2815
2816 #[test]
2817 fn value_named_arg_preserved() {
2818 let result = parse("cmd count=42").unwrap();
2822 match &result.statements[0] {
2823 Stmt::Command(cmd) => {
2824 assert_eq!(cmd.name, "cmd");
2825 match &cmd.args[0] {
2826 Arg::WordAssign { key, value } => {
2827 assert_eq!(key, "count");
2828 match value {
2829 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2830 other => panic!("expected int, got {:?}", other),
2831 }
2832 }
2833 other => panic!("expected WordAssign arg, got {:?}", other),
2834 }
2835 }
2836 other => panic!("expected command, got {:?}", other),
2837 }
2838 }
2839
2840 #[test]
2841 fn value_function_def_name_preserved() {
2842 let result = parse("greet() { echo }").unwrap();
2843 match &result.statements[0] {
2844 Stmt::ToolDef(t) => {
2845 assert_eq!(t.name, "greet");
2846 assert!(t.params.is_empty());
2847 }
2848 other => panic!("expected function def, got {:?}", other),
2849 }
2850 }
2851
2852 #[test]
2857 fn parse_comparison_equals() {
2858 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
2860 match &result.statements[0] {
2861 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2862 Expr::Test(test) => match test.as_ref() {
2863 TestExpr::Comparison { left, op, right } => {
2864 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2865 assert_eq!(*op, TestCmpOp::Eq);
2866 match right.as_ref() {
2867 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
2868 other => panic!("expected int, got {:?}", other),
2869 }
2870 }
2871 other => panic!("expected comparison, got {:?}", other),
2872 },
2873 other => panic!("expected test expr, got {:?}", other),
2874 },
2875 other => panic!("expected if, got {:?}", other),
2876 }
2877 }
2878
2879 #[test]
2880 fn parse_comparison_not_equals() {
2881 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
2882 match &result.statements[0] {
2883 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2884 Expr::Test(test) => match test.as_ref() {
2885 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
2886 other => panic!("expected comparison, got {:?}", other),
2887 },
2888 other => panic!("expected test expr, got {:?}", other),
2889 },
2890 other => panic!("expected if, got {:?}", other),
2891 }
2892 }
2893
2894 #[test]
2895 fn parse_comparison_less_than() {
2896 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
2897 match &result.statements[0] {
2898 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2899 Expr::Test(test) => match test.as_ref() {
2900 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
2901 other => panic!("expected comparison, got {:?}", other),
2902 },
2903 other => panic!("expected test expr, got {:?}", other),
2904 },
2905 other => panic!("expected if, got {:?}", other),
2906 }
2907 }
2908
2909 #[test]
2910 fn parse_comparison_greater_than() {
2911 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
2912 match &result.statements[0] {
2913 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2914 Expr::Test(test) => match test.as_ref() {
2915 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
2916 other => panic!("expected comparison, got {:?}", other),
2917 },
2918 other => panic!("expected test expr, got {:?}", other),
2919 },
2920 other => panic!("expected if, got {:?}", other),
2921 }
2922 }
2923
2924 #[test]
2925 fn parse_comparison_less_equal() {
2926 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
2927 match &result.statements[0] {
2928 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2929 Expr::Test(test) => match test.as_ref() {
2930 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
2931 other => panic!("expected comparison, got {:?}", other),
2932 },
2933 other => panic!("expected test expr, got {:?}", other),
2934 },
2935 other => panic!("expected if, got {:?}", other),
2936 }
2937 }
2938
2939 #[test]
2940 fn parse_comparison_greater_equal() {
2941 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
2942 match &result.statements[0] {
2943 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2944 Expr::Test(test) => match test.as_ref() {
2945 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
2946 other => panic!("expected comparison, got {:?}", other),
2947 },
2948 other => panic!("expected test expr, got {:?}", other),
2949 },
2950 other => panic!("expected if, got {:?}", other),
2951 }
2952 }
2953
2954 #[test]
2955 fn parse_regex_match() {
2956 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
2957 match &result.statements[0] {
2958 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2959 Expr::Test(test) => match test.as_ref() {
2960 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
2961 other => panic!("expected comparison, got {:?}", other),
2962 },
2963 other => panic!("expected test expr, got {:?}", other),
2964 },
2965 other => panic!("expected if, got {:?}", other),
2966 }
2967 }
2968
2969 #[test]
2970 fn parse_regex_not_match() {
2971 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
2972 match &result.statements[0] {
2973 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2974 Expr::Test(test) => match test.as_ref() {
2975 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
2976 other => panic!("expected comparison, got {:?}", other),
2977 },
2978 other => panic!("expected test expr, got {:?}", other),
2979 },
2980 other => panic!("expected if, got {:?}", other),
2981 }
2982 }
2983
2984 #[test]
2985 fn parse_string_interpolation() {
2986 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
2987 match &result.statements[0] {
2988 Stmt::Command(cmd) => match &cmd.args[0] {
2989 Arg::Positional(Expr::Interpolated(parts)) => {
2990 assert_eq!(parts.len(), 3);
2991 match &parts[0] {
2992 StringPart::Literal(s) => assert_eq!(s, "Hello "),
2993 other => panic!("expected literal, got {:?}", other),
2994 }
2995 match &parts[1] {
2996 StringPart::Var(path) => {
2997 assert_eq!(path.segments.len(), 1);
2998 let VarSegment::Field(name) = &path.segments[0];
2999 assert_eq!(name, "NAME");
3000 }
3001 other => panic!("expected var, got {:?}", other),
3002 }
3003 match &parts[2] {
3004 StringPart::Literal(s) => assert_eq!(s, "!"),
3005 other => panic!("expected literal, got {:?}", other),
3006 }
3007 }
3008 other => panic!("expected interpolated, got {:?}", other),
3009 },
3010 other => panic!("expected command, got {:?}", other),
3011 }
3012 }
3013
3014 #[test]
3015 fn parse_string_interpolation_multiple_vars() {
3016 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
3017 match &result.statements[0] {
3018 Stmt::Command(cmd) => match &cmd.args[0] {
3019 Arg::Positional(Expr::Interpolated(parts)) => {
3020 assert_eq!(parts.len(), 3);
3022 assert!(matches!(&parts[0], StringPart::Var(_)));
3023 assert!(matches!(&parts[1], StringPart::Literal(_)));
3024 assert!(matches!(&parts[2], StringPart::Var(_)));
3025 }
3026 other => panic!("expected interpolated, got {:?}", other),
3027 },
3028 other => panic!("expected command, got {:?}", other),
3029 }
3030 }
3031
3032 #[test]
3033 fn parse_empty_function_body() {
3034 let result = parse("empty() { }").unwrap();
3035 match &result.statements[0] {
3036 Stmt::ToolDef(t) => {
3037 assert_eq!(t.name, "empty");
3038 assert!(t.params.is_empty());
3039 assert!(t.body.is_empty());
3040 }
3041 other => panic!("expected function def, got {:?}", other),
3042 }
3043 }
3044
3045 #[test]
3046 fn parse_bash_style_function() {
3047 let result = parse("function greet { echo hello }").unwrap();
3048 match &result.statements[0] {
3049 Stmt::ToolDef(t) => {
3050 assert_eq!(t.name, "greet");
3051 assert!(t.params.is_empty());
3052 assert_eq!(t.body.len(), 1);
3053 }
3054 other => panic!("expected function def, got {:?}", other),
3055 }
3056 }
3057
3058 #[test]
3059 fn parse_comparison_string_values() {
3060 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
3061 match &result.statements[0] {
3062 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3063 Expr::Test(test) => match test.as_ref() {
3064 TestExpr::Comparison { left, op, right } => {
3065 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3066 assert_eq!(*op, TestCmpOp::Eq);
3067 match right.as_ref() {
3068 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
3069 other => panic!("expected string, got {:?}", other),
3070 }
3071 }
3072 other => panic!("expected comparison, got {:?}", other),
3073 },
3074 other => panic!("expected test expr, got {:?}", other),
3075 },
3076 other => panic!("expected if, got {:?}", other),
3077 }
3078 }
3079
3080 #[test]
3085 fn parse_cmd_subst_simple() {
3086 let result = parse("X=$(echo)").unwrap();
3087 match &result.statements[0] {
3088 Stmt::Assignment(a) => {
3089 assert_eq!(a.name, "X");
3090 assert_eq!(subst_cmd(&a.value).name, "echo");
3091 }
3092 other => panic!("expected assignment, got {:?}", other),
3093 }
3094 }
3095
3096 #[test]
3097 fn parse_cmd_subst_with_args() {
3098 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
3099 match &result.statements[0] {
3100 Stmt::Assignment(a) => {
3101 let cmd = subst_cmd(&a.value);
3102 assert_eq!(cmd.name, "fetch");
3103 assert_eq!(cmd.args.len(), 1);
3104 match &cmd.args[0] {
3105 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
3106 other => panic!("expected WordAssign arg, got {:?}", other),
3107 }
3108 }
3109 other => panic!("expected assignment, got {:?}", other),
3110 }
3111 }
3112
3113 #[test]
3114 fn parse_cmd_subst_pipeline() {
3115 let result = parse("X=$(cat file | grep pattern)").unwrap();
3116 match &result.statements[0] {
3117 Stmt::Assignment(a) => {
3118 let pipeline = subst_pipeline(&a.value);
3119 assert_eq!(pipeline.commands.len(), 2);
3120 assert_eq!(pipeline.commands[0].name, "cat");
3121 assert_eq!(pipeline.commands[1].name, "grep");
3122 }
3123 other => panic!("expected assignment, got {:?}", other),
3124 }
3125 }
3126
3127 #[test]
3128 fn parse_cmd_subst_in_condition() {
3129 let result = parse("if kaish-validate; then echo; fi").unwrap();
3131 match &result.statements[0] {
3132 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3133 Expr::Command(cmd) => {
3134 assert_eq!(cmd.name, "kaish-validate");
3135 }
3136 other => panic!("expected command, got {:?}", other),
3137 },
3138 other => panic!("expected if, got {:?}", other),
3139 }
3140 }
3141
3142 #[test]
3147 fn parse_env_prefix_single() {
3148 let result = parse("FOO=bar echo hi").unwrap();
3149 match &result.statements[0] {
3150 Stmt::EnvScoped { assignments, body } => {
3151 assert_eq!(assignments.len(), 1);
3152 assert_eq!(assignments[0].name, "FOO");
3153 assert!(!assignments[0].local);
3154 match body.as_ref() {
3155 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
3156 other => panic!("expected command body, got {other:?}"),
3157 }
3158 }
3159 other => panic!("expected env-scoped, got {other:?}"),
3160 }
3161 }
3162
3163 #[test]
3164 fn parse_env_prefix_multiple() {
3165 let result = parse("A=1 B=2 run").unwrap();
3166 match &result.statements[0] {
3167 Stmt::EnvScoped { assignments, body } => {
3168 assert_eq!(assignments.len(), 2);
3169 assert_eq!(assignments[0].name, "A");
3170 assert_eq!(assignments[1].name, "B");
3171 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
3172 }
3173 other => panic!("expected env-scoped, got {other:?}"),
3174 }
3175 }
3176
3177 #[test]
3178 fn parse_bare_assignment_is_not_env_scoped() {
3179 let result = parse("FOO=bar").unwrap();
3181 assert!(
3182 matches!(&result.statements[0], Stmt::Assignment(a) if a.name == "FOO"),
3183 "got {:?}",
3184 result.statements[0]
3185 );
3186 }
3187
3188 #[test]
3189 fn parse_assignment_then_and_chain_does_not_over_capture() {
3190 let result = parse("FOO=bar && echo hi").unwrap();
3193 match &result.statements[0] {
3194 Stmt::AndChain { left, right } => {
3195 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name == "FOO"));
3196 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
3197 }
3198 other => panic!("expected and-chain, got {other:?}"),
3199 }
3200 }
3201
3202 #[test]
3203 fn parse_env_prefix_pipeline_body() {
3204 let result = parse("FOO=bar cat | grep x").unwrap();
3205 match &result.statements[0] {
3206 Stmt::EnvScoped { assignments, body } => {
3207 assert_eq!(assignments[0].name, "FOO");
3208 match body.as_ref() {
3209 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 2),
3210 other => panic!("expected pipeline body, got {other:?}"),
3211 }
3212 }
3213 other => panic!("expected env-scoped, got {other:?}"),
3214 }
3215 }
3216
3217 fn parse_err_message(source: &str) -> String {
3222 parse(source)
3223 .expect_err("expected a parse error")
3224 .iter()
3225 .map(|e| e.message.clone())
3226 .collect::<Vec<_>>()
3227 .join(" ")
3228 }
3229
3230 #[test]
3231 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
3232 let msg = parse_err_message("echo /tmp/$(echo x).txt");
3235 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
3236 }
3237
3238 #[test]
3239 fn argv_splat_var_glued_to_path_is_rejected() {
3240 assert!(parse("echo $dir/out.txt").is_err());
3241 }
3242
3243 #[test]
3244 fn argv_splat_three_way_glue_is_rejected() {
3245 assert!(parse("echo foo$(echo bar)baz").is_err());
3246 }
3247
3248 #[test]
3249 fn argv_splat_quoted_word_is_accepted() {
3250 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
3252 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
3253 }
3254
3255 #[test]
3256 fn argv_single_token_words_are_not_splat() {
3257 assert!(parse("echo file.txt").is_ok(), "file.txt");
3259 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
3260 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
3261 }
3262
3263 #[test]
3264 fn argv_spaced_words_are_not_splat() {
3265 assert!(parse("echo a b c").is_ok());
3266 assert!(parse("echo /tmp/x $(echo y)").is_ok());
3267 }
3268
3269 #[test]
3270 fn parse_cmd_subst_in_command_arg() {
3271 let result = parse("echo $(whoami)").unwrap();
3272 match &result.statements[0] {
3273 Stmt::Command(cmd) => {
3274 assert_eq!(cmd.name, "echo");
3275 match &cmd.args[0] {
3276 Arg::Positional(expr) => {
3277 assert_eq!(subst_cmd(expr).name, "whoami");
3278 }
3279 other => panic!("expected command subst, got {:?}", other),
3280 }
3281 }
3282 other => panic!("expected command, got {:?}", other),
3283 }
3284 }
3285
3286 #[test]
3291 fn parse_condition_and() {
3292 let result = parse("if check-a && check-b; then echo; fi").unwrap();
3294 match &result.statements[0] {
3295 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3296 Expr::BinaryOp { left, op, right } => {
3297 assert_eq!(*op, BinaryOp::And);
3298 assert!(matches!(left.as_ref(), Expr::Command(_)));
3299 assert!(matches!(right.as_ref(), Expr::Command(_)));
3300 }
3301 other => panic!("expected binary op, got {:?}", other),
3302 },
3303 other => panic!("expected if, got {:?}", other),
3304 }
3305 }
3306
3307 #[test]
3308 fn parse_condition_or() {
3309 let result = parse("if try-a || try-b; then echo; fi").unwrap();
3310 match &result.statements[0] {
3311 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3312 Expr::BinaryOp { left, op, right } => {
3313 assert_eq!(*op, BinaryOp::Or);
3314 assert!(matches!(left.as_ref(), Expr::Command(_)));
3315 assert!(matches!(right.as_ref(), Expr::Command(_)));
3316 }
3317 other => panic!("expected binary op, got {:?}", other),
3318 },
3319 other => panic!("expected if, got {:?}", other),
3320 }
3321 }
3322
3323 #[test]
3324 fn parse_condition_and_or_precedence() {
3325 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
3327 match &result.statements[0] {
3328 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3329 Expr::BinaryOp { left, op, right } => {
3330 assert_eq!(*op, BinaryOp::Or);
3332 match left.as_ref() {
3334 Expr::BinaryOp { op: inner_op, .. } => {
3335 assert_eq!(*inner_op, BinaryOp::And);
3336 }
3337 other => panic!("expected binary op (&&), got {:?}", other),
3338 }
3339 assert!(matches!(right.as_ref(), Expr::Command(_)));
3341 }
3342 other => panic!("expected binary op, got {:?}", other),
3343 },
3344 other => panic!("expected if, got {:?}", other),
3345 }
3346 }
3347
3348 #[test]
3349 fn parse_condition_multiple_and() {
3350 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
3351 match &result.statements[0] {
3352 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3353 Expr::BinaryOp { left, op, .. } => {
3354 assert_eq!(*op, BinaryOp::And);
3355 match left.as_ref() {
3357 Expr::BinaryOp { op: inner_op, .. } => {
3358 assert_eq!(*inner_op, BinaryOp::And);
3359 }
3360 other => panic!("expected binary op, got {:?}", other),
3361 }
3362 }
3363 other => panic!("expected binary op, got {:?}", other),
3364 },
3365 other => panic!("expected if, got {:?}", other),
3366 }
3367 }
3368
3369 #[test]
3370 fn parse_condition_mixed_comparison_and_logical() {
3371 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
3373 match &result.statements[0] {
3374 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3375 Expr::BinaryOp { left, op, right } => {
3376 assert_eq!(*op, BinaryOp::And);
3377 match left.as_ref() {
3379 Expr::Test(test) => match test.as_ref() {
3380 TestExpr::Comparison { op: left_op, .. } => {
3381 assert_eq!(*left_op, TestCmpOp::Eq);
3382 }
3383 other => panic!("expected comparison, got {:?}", other),
3384 },
3385 other => panic!("expected test, got {:?}", other),
3386 }
3387 match right.as_ref() {
3389 Expr::Test(test) => match test.as_ref() {
3390 TestExpr::Comparison { op: right_op, .. } => {
3391 assert_eq!(*right_op, TestCmpOp::NumGt);
3392 }
3393 other => panic!("expected comparison, got {:?}", other),
3394 },
3395 other => panic!("expected test, got {:?}", other),
3396 }
3397 }
3398 other => panic!("expected binary op, got {:?}", other),
3399 },
3400 other => panic!("expected if, got {:?}", other),
3401 }
3402 }
3403
3404 #[test]
3410 fn script_level1_linear() {
3411 let script = r#"
3412NAME="kaish"
3413VERSION=1
3414TIMEOUT=30
3415ITEMS="alpha beta gamma"
3416
3417echo "Starting ${NAME} v${VERSION}"
3418cat "README.md" | grep pattern="install" | head count=5
3419fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
3420echo "Items: ${ITEMS}"
3421"#;
3422 let result = parse(script).unwrap();
3423 let stmts: Vec<_> = result.statements.iter()
3424 .filter(|s| !matches!(s, Stmt::Empty))
3425 .collect();
3426
3427 assert_eq!(stmts.len(), 8);
3428 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(_))); }
3437
3438 #[test]
3440 fn script_level2_branching() {
3441 let script = r#"
3442RESULT=$(kaish-validate "input.json")
3443
3444if [[ ${RESULT.ok} == true ]]; then
3445 echo "Validation passed"
3446 process "input.json" > "output.json"
3447else
3448 echo "Validation failed: ${RESULT.err}"
3449fi
3450
3451if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
3452 echo "Count in valid range"
3453fi
3454
3455if check-network || check-cache; then
3456 fetch url=${URL}
3457fi
3458"#;
3459 let result = parse(script).unwrap();
3460 let stmts: Vec<_> = result.statements.iter()
3461 .filter(|s| !matches!(s, Stmt::Empty))
3462 .collect();
3463
3464 assert_eq!(stmts.len(), 4);
3465
3466 match stmts[0] {
3468 Stmt::Assignment(a) => {
3469 assert_eq!(a.name, "RESULT");
3470 assert!(matches!(&a.value, Expr::CommandSubst(_)));
3471 }
3472 other => panic!("expected assignment, got {:?}", other),
3473 }
3474
3475 match stmts[1] {
3477 Stmt::If(if_stmt) => {
3478 assert_eq!(if_stmt.then_branch.len(), 2);
3479 assert!(if_stmt.else_branch.is_some());
3480 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
3481 }
3482 other => panic!("expected if, got {:?}", other),
3483 }
3484
3485 match stmts[2] {
3487 Stmt::If(if_stmt) => {
3488 match if_stmt.condition.as_ref() {
3489 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3490 other => panic!("expected && condition, got {:?}", other),
3491 }
3492 }
3493 other => panic!("expected if, got {:?}", other),
3494 }
3495
3496 match stmts[3] {
3498 Stmt::If(if_stmt) => {
3499 match if_stmt.condition.as_ref() {
3500 Expr::BinaryOp { op, left, right } => {
3501 assert_eq!(*op, BinaryOp::Or);
3502 assert!(matches!(left.as_ref(), Expr::Command(_)));
3503 assert!(matches!(right.as_ref(), Expr::Command(_)));
3504 }
3505 other => panic!("expected || condition, got {:?}", other),
3506 }
3507 }
3508 other => panic!("expected if, got {:?}", other),
3509 }
3510 }
3511
3512 #[test]
3514 fn script_level3_loops_and_functions() {
3515 let script = r#"
3516greet() {
3517 echo "Hello, $1!"
3518}
3519
3520fetch_all() {
3521 for URL in $@; do
3522 fetch url=${URL}
3523 done
3524}
3525
3526USERS="alice bob charlie"
3527
3528for USER in ${USERS}; do
3529 greet ${USER}
3530 if [[ ${USER} == "bob" ]]; then
3531 echo "Found Bob!"
3532 fi
3533done
3534
3535long-running-task &
3536"#;
3537 let result = parse(script).unwrap();
3538 let stmts: Vec<_> = result.statements.iter()
3539 .filter(|s| !matches!(s, Stmt::Empty))
3540 .collect();
3541
3542 assert_eq!(stmts.len(), 5);
3543
3544 match stmts[0] {
3546 Stmt::ToolDef(t) => {
3547 assert_eq!(t.name, "greet");
3548 assert!(t.params.is_empty());
3549 }
3550 other => panic!("expected function def, got {:?}", other),
3551 }
3552
3553 match stmts[1] {
3555 Stmt::ToolDef(t) => {
3556 assert_eq!(t.name, "fetch_all");
3557 assert_eq!(t.body.len(), 1);
3558 assert!(matches!(&t.body[0], Stmt::For(_)));
3559 }
3560 other => panic!("expected function def, got {:?}", other),
3561 }
3562
3563 assert!(matches!(stmts[2], Stmt::Assignment(_)));
3565
3566 match stmts[3] {
3568 Stmt::For(f) => {
3569 assert_eq!(f.variable, "USER");
3570 assert_eq!(f.body.len(), 2);
3571 assert!(matches!(&f.body[0], Stmt::Command(_)));
3572 assert!(matches!(&f.body[1], Stmt::If(_)));
3573 }
3574 other => panic!("expected for loop, got {:?}", other),
3575 }
3576
3577 match stmts[4] {
3579 Stmt::Pipeline(p) => {
3580 assert!(p.background);
3581 assert_eq!(p.commands[0].name, "long-running-task");
3582 }
3583 other => panic!("expected pipeline (background), got {:?}", other),
3584 }
3585 }
3586
3587 #[test]
3589 fn script_level4_complex_nesting() {
3590 let script = r#"
3591RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
3592
3593if ping host=${HOST} && [[ ${RESULT} == true ]]; then
3594 for SERVER in "prod-1 prod-2"; do
3595 deploy target=${SERVER} port=8080
3596 if [[ $? -ne 0 ]]; then
3597 notify channel="ops" message="Deploy failed"
3598 fi
3599 done
3600fi
3601"#;
3602 let result = parse(script).unwrap();
3603 let stmts: Vec<_> = result.statements.iter()
3604 .filter(|s| !matches!(s, Stmt::Empty))
3605 .collect();
3606
3607 assert_eq!(stmts.len(), 2);
3608
3609 match stmts[0] {
3611 Stmt::Assignment(a) => {
3612 assert_eq!(a.name, "RESULT");
3613 assert_eq!(subst_pipeline(&a.value).commands.len(), 3);
3614 }
3615 other => panic!("expected assignment, got {:?}", other),
3616 }
3617
3618 match stmts[1] {
3620 Stmt::If(if_stmt) => {
3621 match if_stmt.condition.as_ref() {
3622 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3623 other => panic!("expected && condition, got {:?}", other),
3624 }
3625 assert_eq!(if_stmt.then_branch.len(), 1);
3626 match &if_stmt.then_branch[0] {
3627 Stmt::For(f) => {
3628 assert_eq!(f.body.len(), 2);
3629 assert!(matches!(&f.body[1], Stmt::If(_)));
3630 }
3631 other => panic!("expected for in if body, got {:?}", other),
3632 }
3633 }
3634 other => panic!("expected if, got {:?}", other),
3635 }
3636 }
3637
3638 #[test]
3640 fn script_level5_edge_cases() {
3641 let script = r#"
3642echo ""
3643echo "quotes: \"nested\" here"
3644echo "escapes: \n\t\r\\"
3645echo "unicode: \u2764"
3646
3647X=-99999
3648Y=3.14159265358979
3649Z=-0.001
3650
3651cmd a=1 b="two" c=true d=false e=null
3652
3653if true; then
3654 if false; then
3655 echo "inner"
3656 else
3657 echo "else"
3658 fi
3659fi
3660
3661for I in "a b c"; do
3662 echo ${I}
3663done
3664
3665no_params() {
3666 echo "no params"
3667}
3668
3669function all_args {
3670 echo "args: $@"
3671}
3672
3673a | b | c | d | e &
3674cmd 2> "errors.log"
3675cmd &> "all.log"
3676cmd >> "append.log"
3677cmd < "input.txt"
3678"#;
3679 let result = parse(script).unwrap();
3680 let stmts: Vec<_> = result.statements.iter()
3681 .filter(|s| !matches!(s, Stmt::Empty))
3682 .collect();
3683
3684 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
3686
3687 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
3689 assert!(bg_stmt.is_some(), "expected background pipeline");
3690
3691 match bg_stmt.unwrap() {
3692 Stmt::Pipeline(p) => {
3693 assert_eq!(p.commands.len(), 5);
3694 assert!(p.background);
3695 }
3696 _ => unreachable!(),
3697 }
3698 }
3699
3700 #[test]
3705 fn parse_keyword_as_variable_rejected() {
3706 let result = parse(r#"if="value""#);
3709 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
3710
3711 let result = parse("while=true");
3712 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
3713
3714 let result = parse(r#"then="next""#);
3715 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
3716 }
3717
3718 #[test]
3719 fn parse_set_command_with_flag() {
3720 let result = parse("set -e");
3721 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
3722 let program = result.unwrap();
3723 match &program.statements[0] {
3724 Stmt::Command(cmd) => {
3725 assert_eq!(cmd.name, "set");
3726 assert_eq!(cmd.args.len(), 1);
3727 match &cmd.args[0] {
3728 Arg::ShortFlag(f) => assert_eq!(f, "e"),
3729 other => panic!("expected ShortFlag, got {:?}", other),
3730 }
3731 }
3732 other => panic!("expected Command, got {:?}", other),
3733 }
3734 }
3735
3736 #[test]
3737 fn parse_set_command_no_args() {
3738 let result = parse("set");
3739 assert!(result.is_ok(), "failed to parse set: {:?}", result);
3740 let program = result.unwrap();
3741 match &program.statements[0] {
3742 Stmt::Command(cmd) => {
3743 assert_eq!(cmd.name, "set");
3744 assert_eq!(cmd.args.len(), 0);
3745 }
3746 other => panic!("expected Command, got {:?}", other),
3747 }
3748 }
3749
3750 #[test]
3751 fn parse_set_assignment_vs_command() {
3752 let result = parse("X=5");
3754 assert!(result.is_ok());
3755 let program = result.unwrap();
3756 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
3757
3758 let result = parse("set -e");
3760 assert!(result.is_ok());
3761 let program = result.unwrap();
3762 assert!(matches!(&program.statements[0], Stmt::Command(_)));
3763 }
3764
3765 #[test]
3766 fn parse_true_as_command() {
3767 let result = parse("true");
3768 assert!(result.is_ok());
3769 let program = result.unwrap();
3770 match &program.statements[0] {
3771 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
3772 other => panic!("expected Command(true), got {:?}", other),
3773 }
3774 }
3775
3776 #[test]
3777 fn parse_false_as_command() {
3778 let result = parse("false");
3779 assert!(result.is_ok());
3780 let program = result.unwrap();
3781 match &program.statements[0] {
3782 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
3783 other => panic!("expected Command(false), got {:?}", other),
3784 }
3785 }
3786
3787 #[test]
3788 fn parse_dot_as_source_alias() {
3789 let result = parse(". script.kai");
3790 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
3791 let program = result.unwrap();
3792 match &program.statements[0] {
3793 Stmt::Command(cmd) => {
3794 assert_eq!(cmd.name, ".");
3795 assert_eq!(cmd.args.len(), 1);
3796 }
3797 other => panic!("expected Command(.), got {:?}", other),
3798 }
3799 }
3800
3801 #[test]
3802 fn parse_source_command() {
3803 let result = parse("source utils.kai");
3804 assert!(result.is_ok(), "failed to parse source: {:?}", result);
3805 let program = result.unwrap();
3806 match &program.statements[0] {
3807 Stmt::Command(cmd) => {
3808 assert_eq!(cmd.name, "source");
3809 assert_eq!(cmd.args.len(), 1);
3810 }
3811 other => panic!("expected Command(source), got {:?}", other),
3812 }
3813 }
3814
3815 #[test]
3816 fn parse_test_expr_file_test() {
3817 let result = parse(r#"[[ -f "/path/file" ]]"#);
3819 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
3820 }
3821
3822 #[test]
3823 fn parse_test_expr_comparison() {
3824 let result = parse(r#"[[ $X == "value" ]]"#);
3825 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
3826 }
3827
3828 #[test]
3829 fn parse_test_expr_single_eq() {
3830 let result = parse(r#"[[ $X = "value" ]]"#);
3832 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
3833 let program = result.unwrap();
3834 match &program.statements[0] {
3835 Stmt::Test(TestExpr::Comparison { op, .. }) => {
3836 assert_eq!(op, &TestCmpOp::Eq);
3837 }
3838 other => panic!("expected Test(Comparison), got {:?}", other),
3839 }
3840 }
3841
3842 #[test]
3843 fn parse_while_loop() {
3844 let result = parse("while true; do echo; done");
3845 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
3846 let program = result.unwrap();
3847 assert!(matches!(&program.statements[0], Stmt::While(_)));
3848 }
3849
3850 #[test]
3851 fn parse_break_with_level() {
3852 let result = parse("break 2");
3853 assert!(result.is_ok());
3854 let program = result.unwrap();
3855 match &program.statements[0] {
3856 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
3857 other => panic!("expected Break(2), got {:?}", other),
3858 }
3859 }
3860
3861 #[test]
3862 fn parse_continue_with_level() {
3863 let result = parse("continue 3");
3864 assert!(result.is_ok());
3865 let program = result.unwrap();
3866 match &program.statements[0] {
3867 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
3868 other => panic!("expected Continue(3), got {:?}", other),
3869 }
3870 }
3871
3872 #[test]
3873 fn parse_exit_with_code() {
3874 let result = parse("exit 1");
3875 assert!(result.is_ok());
3876 let program = result.unwrap();
3877 match &program.statements[0] {
3878 Stmt::Exit(Some(expr)) => {
3879 match expr.as_ref() {
3880 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
3881 other => panic!("expected Int(1), got {:?}", other),
3882 }
3883 }
3884 other => panic!("expected Exit(1), got {:?}", other),
3885 }
3886 }
3887
3888 #[test]
3895 fn spanned_literal_only_records_byte_range() {
3896 let parts = parse_interpolated_string_spanned("hello world", 100);
3897 assert_eq!(parts.len(), 1);
3898 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
3899 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
3900 assert_eq!(parts[0].len, 11);
3901 }
3902
3903 #[test]
3904 fn spanned_braced_var_at_zero() {
3905 let parts = parse_interpolated_string_spanned("${X}", 50);
3906 assert_eq!(parts.len(), 1);
3907 assert!(matches!(&parts[0].part, StringPart::Var(_)));
3908 assert_eq!(parts[0].offset, 50);
3909 assert_eq!(parts[0].len, 4); }
3911
3912 #[test]
3913 fn spanned_simple_var_then_literal() {
3914 let parts = parse_interpolated_string_spanned("$X end", 10);
3915 assert_eq!(parts.len(), 2);
3916 assert!(matches!(&parts[0].part, StringPart::Var(_)));
3917 assert_eq!(parts[0].offset, 10);
3918 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
3920 assert_eq!(parts[1].offset, 12);
3921 assert_eq!(parts[1].len, 4);
3922 }
3923
3924 #[test]
3925 fn spanned_mixed_literal_var_literal() {
3926 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0);
3927 assert_eq!(parts.len(), 3);
3928 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
3930 assert_eq!(parts[0].offset, 0);
3931 assert_eq!(parts[0].len, 3);
3932 assert!(matches!(&parts[1].part, StringPart::Var(_)));
3934 assert_eq!(parts[1].offset, 3);
3935 assert_eq!(parts[1].len, 4);
3936 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
3938 assert_eq!(parts[2].offset, 7);
3939 assert_eq!(parts[2].len, 4);
3940 }
3941
3942 #[test]
3943 fn spanned_positional_param() {
3944 let parts = parse_interpolated_string_spanned("$1 done", 0);
3945 assert_eq!(parts.len(), 2);
3946 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
3947 assert_eq!(parts[0].offset, 0);
3948 assert_eq!(parts[0].len, 2); }
3950
3951 #[test]
3952 fn spanned_special_dollar_dollar() {
3953 let parts = parse_interpolated_string_spanned("$$", 5);
3954 assert_eq!(parts.len(), 1);
3955 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
3956 assert_eq!(parts[0].offset, 5);
3957 assert_eq!(parts[0].len, 2);
3958 }
3959
3960 #[test]
3961 fn spanned_arithmetic_marker_recognised() {
3962 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0);
3966 assert_eq!(parts.len(), 1);
3967 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
3968 }
3969
3970 #[test]
3971 fn spanned_default_separator_yields_var_with_default() {
3972 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0);
3973 assert_eq!(parts.len(), 1);
3974 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
3975 assert_eq!(parts[0].offset, 0);
3976 assert_eq!(parts[0].len, 14); }
3978
3979 #[test]
3980 fn spanned_no_dollar_runs_one_literal() {
3981 let parts = parse_interpolated_string_spanned("plain text only", 7);
3982 assert_eq!(parts.len(), 1);
3983 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
3984 assert_eq!(parts[0].offset, 7);
3985 assert_eq!(parts[0].len, 15);
3986 }
3987
3988 #[test]
3989 fn spanned_matches_unspanned_part_count() {
3990 let cases = [
3993 "hello",
3994 "$X",
3995 "${X}",
3996 "${X:-d}",
3997 "hi $A and $B",
3998 "$0 $1 $2",
3999 "$$ $? $#",
4000 ];
4001 for s in &cases {
4002 let unspanned = parse_interpolated_string(s);
4003 let spanned = parse_interpolated_string_spanned(s, 0);
4004 assert_eq!(
4005 unspanned.len(),
4006 spanned.len(),
4007 "part count differs for {:?}",
4008 s
4009 );
4010 }
4011 }
4012
4013 #[test]
4014 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
4015 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0);
4020 assert_eq!(parts.len(), 2);
4021
4022 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
4023 assert_eq!(parts[0].offset, 0);
4024 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
4025
4026 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4027 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
4028 assert_eq!(parts[1].len, 4);
4029 }
4030
4031 #[test]
4032 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
4033 let parts = parse_interpolated_string_spanned("hello 世界 world", 0);
4036 assert_eq!(parts.len(), 1);
4037 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
4038 assert_eq!(parts[0].offset, 0);
4039 assert_eq!(parts[0].len, 18);
4040 }
4041
4042 #[test]
4043 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
4044 let parts = parse_interpolated_string_spanned("\\$", 0);
4047 assert_eq!(parts.len(), 1);
4048 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
4049 assert_eq!(parts[0].offset, 0);
4050 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
4051 }
4052
4053 #[test]
4054 fn spanned_escape_backslash_collapses_pair_to_one() {
4055 let parts = parse_interpolated_string_spanned("\\\\", 0);
4056 assert_eq!(parts.len(), 1);
4057 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
4058 assert_eq!(parts[0].len, 2);
4059 }
4060}