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 is_comma_literal_arg(arg: &Arg) -> bool {
1496 matches!(arg, Arg::Positional(Expr::Literal(Value::String(s))) if s == ",")
1497}
1498
1499fn args_list_parser<'tokens, I>(
1503) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1504where
1505 I: ValueInput<'tokens, Token = Token, Span = Span>,
1506{
1507 let pre_dash = arg_before_double_dash_parser()
1514 .map_with(|arg, e| -> (Arg, Span) { (arg, e.span()) })
1515 .repeated()
1516 .collect::<Vec<(Arg, Span)>>()
1517 .try_map(|args, _span| {
1518 for pair in args.windows(2) {
1519 let (prev, prev_span) = &pair[0];
1520 let (next, next_span) = &pair[1];
1521 if matches!(prev, Arg::Positional(_))
1522 && matches!(next, Arg::Positional(_))
1523 && prev_span.end == next_span.start
1524 {
1525 let msg = if is_comma_literal_arg(prev) || is_comma_literal_arg(next) {
1530 "an unquoted comma splits this into separate words — kaish reserves \
1531 `,` (brace expansion, lists); quote a comma-bearing argument to keep \
1532 it one word, e.g. cut -f \"1,3\", sort -k \"2,2n\", or echo \"a,b\""
1533 } else {
1534 "adjacent words with no space between them are not joined into one \
1535 argument (kaish does no token pasting); quote the whole word, e.g. \
1536 \"/tmp/$(echo x).txt\" or \"$dir/out.txt\""
1537 };
1538 return Err(Rich::custom(*next_span, msg));
1539 }
1540 }
1541 Ok(args.into_iter().map(|(arg, _)| arg).collect::<Vec<Arg>>())
1542 });
1543
1544 let double_dash = select! {
1546 Token::DoubleDash => Arg::DoubleDash,
1547 };
1548
1549 let post_dash_arg = choice((
1551 select! {
1553 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
1554 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
1555 },
1556 primary_expr_parser().map(Arg::Positional),
1558 ));
1559
1560 let post_dash = post_dash_arg.repeated().collect::<Vec<_>>();
1561
1562 pre_dash
1564 .then(double_dash.then(post_dash).or_not())
1565 .map(|(mut args, maybe_dd)| {
1566 if let Some((dd, post)) = maybe_dd {
1567 args.push(dd);
1568 args.extend(post);
1569 }
1570 args
1571 })
1572}
1573
1574fn keyword_word<'tokens, I>(
1583) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1584where
1585 I: ValueInput<'tokens, Token = Token, Span = Span>,
1586{
1587 select! {
1588 Token::Set => "set",
1589 Token::Local => "local",
1590 Token::If => "if",
1591 Token::Then => "then",
1592 Token::Else => "else",
1593 Token::Elif => "elif",
1594 Token::Fi => "fi",
1595 Token::For => "for",
1596 Token::While => "while",
1597 Token::In => "in",
1598 Token::Do => "do",
1599 Token::Done => "done",
1600 Token::Case => "case",
1601 Token::Esac => "esac",
1602 Token::Function => "function",
1603 Token::Break => "break",
1604 Token::Continue => "continue",
1605 Token::Return => "return",
1606 Token::Exit => "exit",
1607 }
1608 .map(|s| s.to_string())
1609}
1610
1611fn arg_before_double_dash_parser<'tokens, I>(
1613) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1614where
1615 I: ValueInput<'tokens, Token = Token, Span = Span>,
1616{
1617 let long_flag_with_value = select! {
1619 Token::LongFlag(name) => name,
1620 }
1621 .then_ignore(just(Token::Eq))
1622 .then(primary_expr_parser())
1623 .map(|(key, value)| Arg::Named { key, value });
1624
1625 let long_flag = select! {
1627 Token::LongFlag(name) => Arg::LongFlag(name),
1628 };
1629
1630 let short_flag = select! {
1632 Token::ShortFlag(name) => Arg::ShortFlag(name),
1633 };
1634
1635 let named = choice((
1641 select! { Token::Ident(s) => s },
1642 keyword_word(),
1643 ))
1644 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1645 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1646 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1647 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1648 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1650 Err(Rich::custom(
1651 span,
1652 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
1653 ))
1654 } else {
1655 Ok(Arg::WordAssign { key, value })
1656 }
1657 });
1658
1659 let positional = primary_expr_parser().map(Arg::Positional);
1661
1662 choice((
1665 long_flag_with_value,
1666 long_flag,
1667 short_flag,
1668 named,
1669 positional,
1670 ))
1671 .boxed()
1672}
1673
1674fn redirect_parser<'tokens, I>(
1676) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1677where
1678 I: ValueInput<'tokens, Token = Token, Span = Span>,
1679{
1680 let regular_redirect = select! {
1682 Token::GtGt => RedirectKind::StdoutAppend,
1683 Token::Gt => RedirectKind::StdoutOverwrite,
1684 Token::Lt => RedirectKind::Stdin,
1685 Token::Stderr => RedirectKind::Stderr,
1686 Token::Both => RedirectKind::Both,
1687 }
1688 .then(primary_expr_parser())
1689 .map(|(kind, target)| Redirect { kind, target });
1690
1691 let heredoc_redirect = just(Token::HereDocStart)
1699 .ignore_then(select! { Token::HereDoc(data) => data })
1700 .map(|data: HereDocData| {
1701 let target = if data.literal {
1702 let body = if data.strip_tabs {
1703 crate::interpreter::strip_leading_tabs(&data.content)
1704 } else {
1705 data.content
1706 };
1707 Expr::Literal(Value::String(body))
1708 } else {
1709 let parts = parse_interpolated_string_spanned(
1710 &data.content,
1711 data.body_start_offset,
1712 );
1713 if parts.len() == 1 && !data.strip_tabs {
1717 if let StringPart::Literal(text) = &parts[0].part {
1718 return Redirect {
1719 kind: RedirectKind::HereDoc,
1720 target: Expr::Literal(Value::String(text.clone())),
1721 };
1722 }
1723 }
1724 Expr::HereDocBody {
1725 parts,
1726 strip_tabs: data.strip_tabs,
1727 }
1728 };
1729 Redirect {
1730 kind: RedirectKind::HereDoc,
1731 target,
1732 }
1733 });
1734
1735 let herestring_redirect = just(Token::HereString)
1739 .ignore_then(primary_expr_parser())
1740 .map(|target| Redirect {
1741 kind: RedirectKind::HereString,
1742 target,
1743 });
1744
1745 let merge_stderr_redirect = just(Token::StderrToStdout)
1747 .map(|_| Redirect {
1748 kind: RedirectKind::MergeStderr,
1749 target: Expr::Literal(Value::Null),
1751 });
1752
1753 let merge_stdout_redirect = choice((
1755 just(Token::StdoutToStderr),
1756 just(Token::StdoutToStderr2),
1757 ))
1758 .map(|_| Redirect {
1759 kind: RedirectKind::MergeStdout,
1760 target: Expr::Literal(Value::Null),
1762 });
1763
1764 choice((
1765 heredoc_redirect,
1766 herestring_redirect,
1767 merge_stderr_redirect,
1768 merge_stdout_redirect,
1769 regular_redirect,
1770 ))
1771 .labelled("redirect")
1772 .boxed()
1773}
1774
1775fn test_expr_stmt_parser<'tokens, I>(
1785) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1786where
1787 I: ValueInput<'tokens, Token = Token, Span = Span>,
1788{
1789 let file_test_op = select! {
1791 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
1792 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
1793 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
1794 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
1795 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
1796 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
1797 };
1798
1799 let string_test_op = select! {
1801 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
1802 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
1803 };
1804
1805 let cmp_op = choice((
1808 just(Token::EqEq).to(TestCmpOp::Eq),
1809 just(Token::Eq).to(TestCmpOp::Eq),
1810 just(Token::NotEq).to(TestCmpOp::NotEq),
1811 just(Token::Match).to(TestCmpOp::Match),
1812 just(Token::NotMatch).to(TestCmpOp::NotMatch),
1813 just(Token::Gt).to(TestCmpOp::Gt),
1814 just(Token::Lt).to(TestCmpOp::Lt),
1815 just(Token::GtEq).to(TestCmpOp::GtEq),
1816 just(Token::LtEq).to(TestCmpOp::LtEq),
1817 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
1818 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
1819 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
1820 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
1821 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
1822 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
1823 ));
1824
1825 let file_test = file_test_op
1827 .then(primary_expr_parser())
1828 .map(|(op, path)| TestExpr::FileTest {
1829 op,
1830 path: Box::new(path),
1831 });
1832
1833 let string_test = string_test_op
1835 .then(primary_expr_parser())
1836 .map(|(op, value)| TestExpr::StringTest {
1837 op,
1838 value: Box::new(value),
1839 });
1840
1841 let comparison = primary_expr_parser()
1843 .then(cmp_op)
1844 .then(primary_expr_parser())
1845 .map(|((left, op), right)| TestExpr::Comparison {
1846 left: Box::new(left),
1847 op,
1848 right: Box::new(right),
1849 });
1850
1851 let primary_test = choice((file_test, string_test, comparison));
1853
1854 let unary = recursive(|unary| {
1868 let not_expr = just(Token::Bang)
1869 .ignore_then(unary)
1870 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
1871 choice((not_expr, primary_test.clone()))
1872 });
1873
1874 let and_expr = unary.clone().foldl(
1876 just(Token::And).ignore_then(unary).repeated(),
1877 |left, right| TestExpr::And {
1878 left: Box::new(left),
1879 right: Box::new(right),
1880 },
1881 );
1882
1883 let compound_test = and_expr.clone().foldl(
1885 just(Token::Or).ignore_then(and_expr).repeated(),
1886 |left, right| TestExpr::Or {
1887 left: Box::new(left),
1888 right: Box::new(right),
1889 },
1890 );
1891
1892 just(Token::LBracket)
1895 .then(just(Token::LBracket))
1896 .ignore_then(compound_test)
1897 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
1898 .labelled("test expression")
1899 .boxed()
1900}
1901
1902fn condition_parser<'tokens, I>(
1917) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1918where
1919 I: ValueInput<'tokens, Token = Token, Span = Span>,
1920{
1921 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
1923
1924 let command_condition = command_parser().map(Expr::Command);
1927
1928 let base = choice((test_expr_condition, command_condition));
1930
1931 let and_expr = base.clone().foldl(
1934 just(Token::And).ignore_then(base).repeated(),
1935 |left, right| Expr::BinaryOp {
1936 left: Box::new(left),
1937 op: BinaryOp::And,
1938 right: Box::new(right),
1939 },
1940 );
1941
1942 and_expr
1944 .clone()
1945 .foldl(
1946 just(Token::Or).ignore_then(and_expr).repeated(),
1947 |left, right| Expr::BinaryOp {
1948 left: Box::new(left),
1949 op: BinaryOp::Or,
1950 right: Box::new(right),
1951 },
1952 )
1953 .labelled("condition")
1954 .boxed()
1955}
1956
1957fn expr_parser<'tokens, I>(
1959) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1960where
1961 I: ValueInput<'tokens, Token = Token, Span = Span>,
1962{
1963 primary_expr_parser()
1965}
1966
1967fn primary_expr_parser<'tokens, I>(
1971) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1972where
1973 I: ValueInput<'tokens, Token = Token, Span = Span>,
1974{
1975 let positional = select! {
1977 Token::Positional(n) => Expr::Positional(n),
1978 Token::AllArgs => Expr::AllArgs,
1979 Token::ArgCount => Expr::ArgCount,
1980 Token::VarLength(name) => Expr::VarLength(name),
1981 Token::LastExitCode => Expr::LastExitCode,
1982 Token::CurrentPid => Expr::CurrentPid,
1983 };
1984
1985 let arithmetic = select! {
1987 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
1988 };
1989
1990 let keyword_as_bareword = select! {
1993 Token::Done => "done",
1994 Token::Fi => "fi",
1995 Token::Then => "then",
1996 Token::Else => "else",
1997 Token::Elif => "elif",
1998 Token::In => "in",
1999 Token::Do => "do",
2000 Token::Esac => "esac",
2001 Token::Set => "set",
2006 }
2007 .map(|s| Expr::Literal(Value::String(s.to_string())));
2008
2009 let plus_minus_bare = select! {
2011 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
2012 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
2013 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
2014 };
2015
2016 let glob_pattern = select! {
2018 Token::GlobWord(s) => Expr::GlobPattern(s),
2019 Token::Star => Expr::GlobPattern("*".to_string()),
2020 Token::Question => Expr::GlobPattern("?".to_string()),
2021 };
2022
2023 recursive(|expr| {
2024 choice((
2025 positional,
2026 arithmetic,
2027 cmd_subst_parser(expr.clone()),
2028 var_expr_parser(),
2029 interpolated_string_parser(),
2030 literal_parser().map(Expr::Literal),
2031 glob_pattern,
2033 ident_parser().map(|s| Expr::Literal(Value::String(s))),
2035 path_parser().map(|s| Expr::Literal(Value::String(s))),
2037 select! {
2040 Token::Dot => Expr::Literal(Value::String(".".into())),
2046 Token::DotDot => Expr::Literal(Value::String("..".into())),
2047 Token::Comma => Expr::Literal(Value::String(",".into())),
2054 Token::Colon => Expr::Literal(Value::String(":".into())),
2061 Token::Tilde => Expr::Literal(Value::String("~".into())),
2062 Token::TildePath(s) => Expr::Literal(Value::String(s)),
2063 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
2064 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
2065 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
2067 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
2072 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
2075 },
2076 plus_minus_bare,
2077 keyword_as_bareword,
2079 ))
2080 .labelled("expression")
2081 })
2082 .boxed()
2083}
2084
2085fn var_expr_parser<'tokens, I>(
2088) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2089where
2090 I: ValueInput<'tokens, Token = Token, Span = Span>,
2091{
2092 select! {
2093 Token::VarRef(raw) => parse_var_expr(&raw),
2094 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
2095 }
2096 .labelled("variable reference")
2097}
2098
2099fn cmd_subst_parser<'tokens, I, E>(
2103 expr: E,
2104) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2105where
2106 I: ValueInput<'tokens, Token = Token, Span = Span>,
2107 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone,
2108{
2109 let long_flag_with_value = select! {
2112 Token::LongFlag(name) => name,
2113 }
2114 .then_ignore(just(Token::Eq))
2115 .then(expr.clone())
2116 .map(|(key, value)| Arg::Named { key, value });
2117
2118 let long_flag = select! {
2120 Token::LongFlag(name) => Arg::LongFlag(name),
2121 };
2122
2123 let short_flag = select! {
2125 Token::ShortFlag(name) => Arg::ShortFlag(name),
2126 };
2127
2128 let named = choice((ident_parser(), keyword_word()))
2131 .then_ignore(just(Token::Eq))
2132 .then(expr.clone())
2133 .map(|(key, value)| Arg::WordAssign { key, value });
2134
2135 let positional = expr.map(Arg::Positional);
2137
2138 let arg = choice((
2139 long_flag_with_value,
2140 long_flag,
2141 short_flag,
2142 named,
2143 positional,
2144 ));
2145
2146 let command_name = choice((
2148 ident_parser(),
2149 just(Token::True).to("true".to_string()),
2150 just(Token::False).to("false".to_string()),
2151 ));
2152
2153 let command = command_name
2155 .then(arg.repeated().collect::<Vec<_>>())
2156 .map(|(name, args)| Command {
2157 name,
2158 args,
2159 redirects: vec![],
2160 });
2161
2162 let pipeline = command
2164 .separated_by(just(Token::Pipe))
2165 .at_least(1)
2166 .collect::<Vec<_>>()
2167 .map(|commands| Pipeline {
2168 commands,
2169 background: false,
2170 });
2171
2172 let pipeline_stmt = pipeline.map(pipeline_into_stmt);
2175
2176 let and_chain = pipeline_stmt.clone().foldl(
2185 just(Token::And).ignore_then(pipeline_stmt.clone()).repeated(),
2186 |left, right| Stmt::AndChain {
2187 left: Box::new(left),
2188 right: Box::new(right),
2189 },
2190 );
2191 let chained = and_chain.clone().foldl(
2192 just(Token::Or).ignore_then(and_chain).repeated(),
2193 |left, right| Stmt::OrChain {
2194 left: Box::new(left),
2195 right: Box::new(right),
2196 },
2197 );
2198
2199 let separator = choice((just(Token::Newline), just(Token::Semi)));
2204 let body = separator
2205 .clone()
2206 .repeated()
2207 .ignore_then(
2208 chained
2209 .separated_by(separator.clone().repeated().at_least(1))
2210 .allow_trailing()
2211 .collect::<Vec<_>>(),
2212 )
2213 .then_ignore(separator.repeated());
2214
2215 just(Token::CmdSubstStart)
2216 .ignore_then(body)
2217 .then_ignore(just(Token::RParen))
2218 .map(Expr::CommandSubst)
2219 .labelled("command substitution")
2220}
2221
2222fn interpolated_string_parser<'tokens, I>(
2224) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2225where
2226 I: ValueInput<'tokens, Token = Token, Span = Span>,
2227{
2228 let double_quoted = select! {
2230 Token::String(s) => s,
2231 }
2232 .map(|s| {
2233 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
2235 let parts = parse_interpolated_string(&s);
2237 if parts.len() == 1
2238 && let StringPart::Literal(text) = &parts[0] {
2239 return Expr::Literal(Value::String(text.clone()));
2240 }
2241 Expr::Interpolated(parts)
2242 } else {
2243 Expr::Literal(Value::String(s))
2244 }
2245 });
2246
2247 let single_quoted = select! {
2249 Token::SingleString(s) => Expr::Literal(Value::String(s)),
2250 };
2251
2252 choice((single_quoted, double_quoted)).labelled("string")
2253}
2254
2255fn literal_parser<'tokens, I>(
2257) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2258where
2259 I: ValueInput<'tokens, Token = Token, Span = Span>,
2260{
2261 choice((
2262 select! {
2263 Token::True => Value::Bool(true),
2264 Token::False => Value::Bool(false),
2265 },
2266 select! {
2267 Token::Int(n) => Value::Int(n),
2268 Token::Float(f) => Value::Float(f),
2269 },
2270 ))
2271 .labelled("literal")
2272 .boxed()
2273}
2274
2275fn ident_parser<'tokens, I>(
2277) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2278where
2279 I: ValueInput<'tokens, Token = Token, Span = Span>,
2280{
2281 select! {
2282 Token::Ident(s) => s,
2283 }
2284 .labelled("identifier")
2285}
2286
2287fn path_parser<'tokens, I>(
2289) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2290where
2291 I: ValueInput<'tokens, Token = Token, Span = Span>,
2292{
2293 select! {
2294 Token::Path(s) => s,
2295 }
2296 .labelled("path")
2297}
2298
2299#[cfg(test)]
2300#[allow(clippy::approx_constant)]
2301mod tests {
2302 use super::*;
2303
2304 fn subst_cmd(expr: &Expr) -> &Command {
2306 match expr {
2307 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2308 [Stmt::Command(cmd)] => cmd,
2309 other => panic!("expected a single command in $(), got {other:?}"),
2310 },
2311 other => panic!("expected command subst, got {other:?}"),
2312 }
2313 }
2314
2315 fn subst_pipeline(expr: &Expr) -> &Pipeline {
2317 match expr {
2318 Expr::CommandSubst(stmts) => match stmts.as_slice() {
2319 [Stmt::Pipeline(p)] => p,
2320 other => panic!("expected a single pipeline in $(), got {other:?}"),
2321 },
2322 other => panic!("expected command subst, got {other:?}"),
2323 }
2324 }
2325
2326 #[test]
2327 fn parse_empty() {
2328 let result = parse("");
2329 assert!(result.is_ok());
2330 assert_eq!(result.expect("ok").statements.len(), 0);
2331 }
2332
2333 #[test]
2334 fn parse_newlines_only() {
2335 let result = parse("\n\n\n");
2336 assert!(result.is_ok());
2337 }
2338
2339 #[test]
2340 fn parse_simple_command() {
2341 let result = parse("echo");
2342 assert!(result.is_ok());
2343 let program = result.expect("ok");
2344 assert_eq!(program.statements.len(), 1);
2345 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2346 }
2347
2348 #[test]
2349 fn parse_command_with_string_arg() {
2350 let result = parse(r#"echo "hello""#);
2351 assert!(result.is_ok());
2352 let program = result.expect("ok");
2353 match &program.statements[0] {
2354 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
2355 _ => panic!("expected Command"),
2356 }
2357 }
2358
2359 #[test]
2360 fn parse_assignment() {
2361 let result = parse("X=5");
2362 assert!(result.is_ok());
2363 let program = result.expect("ok");
2364 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2365 }
2366
2367 #[test]
2368 fn parse_pipeline() {
2369 let result = parse("a | b | c");
2370 assert!(result.is_ok());
2371 let program = result.expect("ok");
2372 match &program.statements[0] {
2373 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2374 _ => panic!("expected Pipeline"),
2375 }
2376 }
2377
2378 #[test]
2379 fn parse_background_job() {
2380 let result = parse("cmd &");
2381 assert!(result.is_ok());
2382 let program = result.expect("ok");
2383 match &program.statements[0] {
2384 Stmt::Pipeline(p) => assert!(p.background),
2385 _ => panic!("expected Pipeline with background"),
2386 }
2387 }
2388
2389 #[test]
2390 fn parse_if_simple() {
2391 let result = parse("if true; then echo; fi");
2392 assert!(result.is_ok());
2393 let program = result.expect("ok");
2394 assert!(matches!(&program.statements[0], Stmt::If(_)));
2395 }
2396
2397 #[test]
2398 fn parse_if_else() {
2399 let result = parse("if true; then echo; else echo; fi");
2400 assert!(result.is_ok());
2401 let program = result.expect("ok");
2402 match &program.statements[0] {
2403 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
2404 _ => panic!("expected If"),
2405 }
2406 }
2407
2408 #[test]
2409 fn parse_elif_simple() {
2410 let result = parse("if true; then echo a; elif false; then echo b; fi");
2411 assert!(result.is_ok(), "parse failed: {:?}", result);
2412 let program = result.expect("ok");
2413 match &program.statements[0] {
2414 Stmt::If(if_stmt) => {
2415 assert!(if_stmt.else_branch.is_some());
2417 let else_branch = if_stmt.else_branch.as_ref().unwrap();
2418 assert_eq!(else_branch.len(), 1);
2419 assert!(matches!(&else_branch[0], Stmt::If(_)));
2420 }
2421 _ => panic!("expected If"),
2422 }
2423 }
2424
2425 #[test]
2426 fn parse_elif_with_else() {
2427 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
2428 assert!(result.is_ok(), "parse failed: {:?}", result);
2429 let program = result.expect("ok");
2430 match &program.statements[0] {
2431 Stmt::If(outer_if) => {
2432 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
2434 assert_eq!(else_branch.len(), 1);
2435 match &else_branch[0] {
2436 Stmt::If(inner_if) => {
2437 assert!(inner_if.else_branch.is_some());
2439 }
2440 _ => panic!("expected nested If from elif"),
2441 }
2442 }
2443 _ => panic!("expected If"),
2444 }
2445 }
2446
2447 #[test]
2448 fn parse_multiple_elif() {
2449 let result = parse(
2451 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
2452 );
2453 assert!(result.is_ok(), "parse failed: {:?}", result);
2454 }
2455
2456 #[test]
2457 fn parse_for_loop() {
2458 let result = parse("for X in items; do echo; done");
2459 assert!(result.is_ok());
2460 let program = result.expect("ok");
2461 assert!(matches!(&program.statements[0], Stmt::For(_)));
2462 }
2463
2464 #[test]
2465 fn parse_brackets_not_array_literal() {
2466 let result = parse("cmd [1");
2468 let _ = result;
2471 }
2472
2473 #[test]
2474 fn parse_named_arg() {
2475 let result = parse("cmd foo=5");
2479 assert!(result.is_ok());
2480 let program = result.expect("ok");
2481 match &program.statements[0] {
2482 Stmt::Command(cmd) => {
2483 assert_eq!(cmd.args.len(), 1);
2484 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
2485 }
2486 _ => panic!("expected Command"),
2487 }
2488 }
2489
2490 #[test]
2491 fn parse_short_flag() {
2492 let result = parse("ls -l");
2493 assert!(result.is_ok());
2494 let program = result.expect("ok");
2495 match &program.statements[0] {
2496 Stmt::Command(cmd) => {
2497 assert_eq!(cmd.name, "ls");
2498 assert_eq!(cmd.args.len(), 1);
2499 match &cmd.args[0] {
2500 Arg::ShortFlag(name) => assert_eq!(name, "l"),
2501 _ => panic!("expected ShortFlag"),
2502 }
2503 }
2504 _ => panic!("expected Command"),
2505 }
2506 }
2507
2508 #[test]
2509 fn parse_long_flag() {
2510 let result = parse("git push --force");
2511 assert!(result.is_ok());
2512 let program = result.expect("ok");
2513 match &program.statements[0] {
2514 Stmt::Command(cmd) => {
2515 assert_eq!(cmd.name, "git");
2516 assert_eq!(cmd.args.len(), 2);
2517 match &cmd.args[0] {
2518 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
2519 _ => panic!("expected Positional push"),
2520 }
2521 match &cmd.args[1] {
2522 Arg::LongFlag(name) => assert_eq!(name, "force"),
2523 _ => panic!("expected LongFlag"),
2524 }
2525 }
2526 _ => panic!("expected Command"),
2527 }
2528 }
2529
2530 #[test]
2531 fn parse_long_flag_with_value() {
2532 let result = parse(r#"git commit --message="hello""#);
2533 assert!(result.is_ok());
2534 let program = result.expect("ok");
2535 match &program.statements[0] {
2536 Stmt::Command(cmd) => {
2537 assert_eq!(cmd.name, "git");
2538 assert_eq!(cmd.args.len(), 2);
2539 match &cmd.args[1] {
2540 Arg::Named { key, value } => {
2541 assert_eq!(key, "message");
2542 match value {
2543 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
2544 _ => panic!("expected String value"),
2545 }
2546 }
2547 _ => panic!("expected Named from --flag=value"),
2548 }
2549 }
2550 _ => panic!("expected Command"),
2551 }
2552 }
2553
2554 #[test]
2555 fn parse_mixed_flags_and_args() {
2556 let result = parse(r#"git commit -m "message" --amend"#);
2557 assert!(result.is_ok());
2558 let program = result.expect("ok");
2559 match &program.statements[0] {
2560 Stmt::Command(cmd) => {
2561 assert_eq!(cmd.name, "git");
2562 assert_eq!(cmd.args.len(), 4);
2563 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
2565 match &cmd.args[1] {
2567 Arg::ShortFlag(name) => assert_eq!(name, "m"),
2568 _ => panic!("expected ShortFlag -m"),
2569 }
2570 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
2572 match &cmd.args[3] {
2574 Arg::LongFlag(name) => assert_eq!(name, "amend"),
2575 _ => panic!("expected LongFlag --amend"),
2576 }
2577 }
2578 _ => panic!("expected Command"),
2579 }
2580 }
2581
2582 #[test]
2583 fn parse_redirect_stdout() {
2584 let result = parse("cmd > file");
2585 assert!(result.is_ok());
2586 let program = result.expect("ok");
2587 match &program.statements[0] {
2589 Stmt::Pipeline(p) => {
2590 assert_eq!(p.commands.len(), 1);
2591 let cmd = &p.commands[0];
2592 assert_eq!(cmd.redirects.len(), 1);
2593 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
2594 }
2595 _ => panic!("expected Pipeline"),
2596 }
2597 }
2598
2599 #[test]
2600 fn parse_var_ref() {
2601 let result = parse("echo ${VAR}");
2602 assert!(result.is_ok());
2603 let program = result.expect("ok");
2604 match &program.statements[0] {
2605 Stmt::Command(cmd) => {
2606 assert_eq!(cmd.args.len(), 1);
2607 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
2608 }
2609 _ => panic!("expected Command"),
2610 }
2611 }
2612
2613 #[test]
2614 fn parse_multiple_statements() {
2615 let result = parse("a\nb\nc");
2616 assert!(result.is_ok());
2617 let program = result.expect("ok");
2618 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2619 assert_eq!(non_empty.len(), 3);
2620 }
2621
2622 #[test]
2623 fn parse_semicolon_separated() {
2624 let result = parse("a; b; c");
2625 assert!(result.is_ok());
2626 let program = result.expect("ok");
2627 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2628 assert_eq!(non_empty.len(), 3);
2629 }
2630
2631 #[test]
2632 fn parse_complex_pipeline() {
2633 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
2634 assert!(result.is_ok());
2635 let program = result.expect("ok");
2636 match &program.statements[0] {
2637 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2638 _ => panic!("expected Pipeline"),
2639 }
2640 }
2641
2642 #[test]
2643 fn parse_json_as_string_arg() {
2644 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
2646 assert!(result.is_ok());
2647 }
2648
2649 #[test]
2650 fn parse_mixed_args() {
2651 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
2652 assert!(result.is_ok());
2653 let program = result.expect("ok");
2654 match &program.statements[0] {
2655 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
2656 _ => panic!("expected Command"),
2657 }
2658 }
2659
2660 #[test]
2661 fn error_unterminated_string() {
2662 let result = parse(r#"echo "hello"#);
2663 assert!(result.is_err());
2664 }
2665
2666 #[test]
2667 fn error_unterminated_var_ref() {
2668 let result = parse("echo ${VAR");
2669 assert!(result.is_err());
2670 }
2671
2672 #[test]
2673 fn error_missing_fi() {
2674 let result = parse("if true; then echo");
2675 assert!(result.is_err());
2676 }
2677
2678 #[test]
2679 fn error_missing_done() {
2680 let result = parse("for X in items; do echo");
2681 assert!(result.is_err());
2682 }
2683
2684 #[test]
2685 fn parse_nested_cmd_subst() {
2686 let result = parse("X=$(echo $(date))").unwrap();
2688 match &result.statements[0] {
2689 Stmt::Assignment(a) => {
2690 assert_eq!(a.name, "X");
2691 let outer = subst_cmd(&a.value);
2692 assert_eq!(outer.name, "echo");
2693 match &outer.args[0] {
2695 Arg::Positional(inner_expr) => {
2696 assert_eq!(subst_cmd(inner_expr).name, "date");
2697 }
2698 other => panic!("expected nested cmd subst arg, got {:?}", other),
2699 }
2700 }
2701 other => panic!("expected assignment, got {:?}", other),
2702 }
2703 }
2704
2705 #[test]
2706 fn parse_deeply_nested_cmd_subst() {
2707 let result = parse("X=$(a $(b $(c)))").unwrap();
2709 match &result.statements[0] {
2710 Stmt::Assignment(a) => {
2711 let level1 = subst_cmd(&a.value);
2712 assert_eq!(level1.name, "a");
2713 match &level1.args[0] {
2714 Arg::Positional(level2_expr) => {
2715 let level2 = subst_cmd(level2_expr);
2716 assert_eq!(level2.name, "b");
2717 match &level2.args[0] {
2718 Arg::Positional(level3_expr) => {
2719 assert_eq!(subst_cmd(level3_expr).name, "c");
2720 }
2721 other => panic!("expected level3 cmd subst, got {:?}", other),
2722 }
2723 }
2724 other => panic!("expected level2 cmd subst, got {:?}", other),
2725 }
2726 }
2727 other => panic!("expected assignment, got {:?}", other),
2728 }
2729 }
2730
2731 #[test]
2736 fn value_int_preserved() {
2737 let result = parse("X=42").unwrap();
2738 match &result.statements[0] {
2739 Stmt::Assignment(a) => {
2740 assert_eq!(a.name, "X");
2741 match &a.value {
2742 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2743 other => panic!("expected int literal, got {:?}", other),
2744 }
2745 }
2746 other => panic!("expected assignment, got {:?}", other),
2747 }
2748 }
2749
2750 #[test]
2751 fn value_negative_int_preserved() {
2752 let result = parse("X=-99").unwrap();
2753 match &result.statements[0] {
2754 Stmt::Assignment(a) => match &a.value {
2755 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
2756 other => panic!("expected int, got {:?}", other),
2757 },
2758 other => panic!("expected assignment, got {:?}", other),
2759 }
2760 }
2761
2762 #[test]
2763 fn value_float_preserved() {
2764 let result = parse("PI=3.14").unwrap();
2765 match &result.statements[0] {
2766 Stmt::Assignment(a) => match &a.value {
2767 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
2768 other => panic!("expected float, got {:?}", other),
2769 },
2770 other => panic!("expected assignment, got {:?}", other),
2771 }
2772 }
2773
2774 #[test]
2775 fn value_string_preserved() {
2776 let result = parse(r#"echo "hello world""#).unwrap();
2777 match &result.statements[0] {
2778 Stmt::Command(cmd) => {
2779 assert_eq!(cmd.name, "echo");
2780 match &cmd.args[0] {
2781 Arg::Positional(Expr::Literal(Value::String(s))) => {
2782 assert_eq!(s, "hello world");
2783 }
2784 other => panic!("expected string arg, got {:?}", other),
2785 }
2786 }
2787 other => panic!("expected command, got {:?}", other),
2788 }
2789 }
2790
2791 #[test]
2792 fn value_string_with_escapes_preserved() {
2793 let result = parse(r#"echo "line1\nline2""#).unwrap();
2794 match &result.statements[0] {
2795 Stmt::Command(cmd) => match &cmd.args[0] {
2796 Arg::Positional(Expr::Literal(Value::String(s))) => {
2797 assert_eq!(s, "line1\nline2");
2798 }
2799 other => panic!("expected string, got {:?}", other),
2800 },
2801 other => panic!("expected command, got {:?}", other),
2802 }
2803 }
2804
2805 #[test]
2806 fn value_command_name_preserved() {
2807 let result = parse("my-command").unwrap();
2808 match &result.statements[0] {
2809 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
2810 other => panic!("expected command, got {:?}", other),
2811 }
2812 }
2813
2814 #[test]
2815 fn value_assignment_name_preserved() {
2816 let result = parse("MY_VAR=1").unwrap();
2817 match &result.statements[0] {
2818 Stmt::Assignment(a) => assert_eq!(a.name, "MY_VAR"),
2819 other => panic!("expected assignment, got {:?}", other),
2820 }
2821 }
2822
2823 #[test]
2824 fn value_for_variable_preserved() {
2825 let result = parse("for ITEM in items; do echo; done").unwrap();
2826 match &result.statements[0] {
2827 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
2828 other => panic!("expected for, got {:?}", other),
2829 }
2830 }
2831
2832 #[test]
2833 fn value_varref_name_preserved() {
2834 let result = parse("echo ${MESSAGE}").unwrap();
2835 match &result.statements[0] {
2836 Stmt::Command(cmd) => match &cmd.args[0] {
2837 Arg::Positional(Expr::VarRef(path)) => {
2838 assert_eq!(path.segments.len(), 1);
2839 let VarSegment::Field(name) = &path.segments[0];
2840 assert_eq!(name, "MESSAGE");
2841 }
2842 other => panic!("expected varref, got {:?}", other),
2843 },
2844 other => panic!("expected command, got {:?}", other),
2845 }
2846 }
2847
2848 #[test]
2849 fn value_varref_field_access_preserved() {
2850 let result = parse("echo ${RESULT.data}").unwrap();
2851 match &result.statements[0] {
2852 Stmt::Command(cmd) => match &cmd.args[0] {
2853 Arg::Positional(Expr::VarRef(path)) => {
2854 assert_eq!(path.segments.len(), 2);
2855 let VarSegment::Field(a) = &path.segments[0];
2856 let VarSegment::Field(b) = &path.segments[1];
2857 assert_eq!(a, "RESULT");
2858 assert_eq!(b, "data");
2859 }
2860 other => panic!("expected varref, got {:?}", other),
2861 },
2862 other => panic!("expected command, got {:?}", other),
2863 }
2864 }
2865
2866 #[test]
2867 fn value_varref_index_ignored() {
2868 let result = parse("echo ${ITEMS[0]}").unwrap();
2870 match &result.statements[0] {
2871 Stmt::Command(cmd) => match &cmd.args[0] {
2872 Arg::Positional(Expr::VarRef(path)) => {
2873 assert_eq!(path.segments.len(), 1);
2875 let VarSegment::Field(name) = &path.segments[0];
2876 assert_eq!(name, "ITEMS");
2877 }
2878 other => panic!("expected varref, got {:?}", other),
2879 },
2880 other => panic!("expected command, got {:?}", other),
2881 }
2882 }
2883
2884 #[test]
2885 fn value_named_arg_preserved() {
2886 let result = parse("cmd count=42").unwrap();
2890 match &result.statements[0] {
2891 Stmt::Command(cmd) => {
2892 assert_eq!(cmd.name, "cmd");
2893 match &cmd.args[0] {
2894 Arg::WordAssign { key, value } => {
2895 assert_eq!(key, "count");
2896 match value {
2897 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2898 other => panic!("expected int, got {:?}", other),
2899 }
2900 }
2901 other => panic!("expected WordAssign arg, got {:?}", other),
2902 }
2903 }
2904 other => panic!("expected command, got {:?}", other),
2905 }
2906 }
2907
2908 #[test]
2909 fn value_function_def_name_preserved() {
2910 let result = parse("greet() { echo }").unwrap();
2911 match &result.statements[0] {
2912 Stmt::ToolDef(t) => {
2913 assert_eq!(t.name, "greet");
2914 assert!(t.params.is_empty());
2915 }
2916 other => panic!("expected function def, got {:?}", other),
2917 }
2918 }
2919
2920 #[test]
2925 fn parse_comparison_equals() {
2926 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
2928 match &result.statements[0] {
2929 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2930 Expr::Test(test) => match test.as_ref() {
2931 TestExpr::Comparison { left, op, right } => {
2932 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2933 assert_eq!(*op, TestCmpOp::Eq);
2934 match right.as_ref() {
2935 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
2936 other => panic!("expected int, got {:?}", other),
2937 }
2938 }
2939 other => panic!("expected comparison, got {:?}", other),
2940 },
2941 other => panic!("expected test expr, got {:?}", other),
2942 },
2943 other => panic!("expected if, got {:?}", other),
2944 }
2945 }
2946
2947 #[test]
2948 fn parse_comparison_not_equals() {
2949 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
2950 match &result.statements[0] {
2951 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2952 Expr::Test(test) => match test.as_ref() {
2953 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
2954 other => panic!("expected comparison, got {:?}", other),
2955 },
2956 other => panic!("expected test expr, got {:?}", other),
2957 },
2958 other => panic!("expected if, got {:?}", other),
2959 }
2960 }
2961
2962 #[test]
2963 fn parse_comparison_less_than() {
2964 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
2965 match &result.statements[0] {
2966 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2967 Expr::Test(test) => match test.as_ref() {
2968 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
2969 other => panic!("expected comparison, got {:?}", other),
2970 },
2971 other => panic!("expected test expr, got {:?}", other),
2972 },
2973 other => panic!("expected if, got {:?}", other),
2974 }
2975 }
2976
2977 #[test]
2978 fn parse_comparison_greater_than() {
2979 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
2980 match &result.statements[0] {
2981 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2982 Expr::Test(test) => match test.as_ref() {
2983 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
2984 other => panic!("expected comparison, got {:?}", other),
2985 },
2986 other => panic!("expected test expr, got {:?}", other),
2987 },
2988 other => panic!("expected if, got {:?}", other),
2989 }
2990 }
2991
2992 #[test]
2993 fn parse_comparison_less_equal() {
2994 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
2995 match &result.statements[0] {
2996 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2997 Expr::Test(test) => match test.as_ref() {
2998 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
2999 other => panic!("expected comparison, got {:?}", other),
3000 },
3001 other => panic!("expected test expr, got {:?}", other),
3002 },
3003 other => panic!("expected if, got {:?}", other),
3004 }
3005 }
3006
3007 #[test]
3008 fn parse_comparison_greater_equal() {
3009 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
3010 match &result.statements[0] {
3011 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3012 Expr::Test(test) => match test.as_ref() {
3013 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
3014 other => panic!("expected comparison, got {:?}", other),
3015 },
3016 other => panic!("expected test expr, got {:?}", other),
3017 },
3018 other => panic!("expected if, got {:?}", other),
3019 }
3020 }
3021
3022 #[test]
3023 fn parse_regex_match() {
3024 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
3025 match &result.statements[0] {
3026 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3027 Expr::Test(test) => match test.as_ref() {
3028 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
3029 other => panic!("expected comparison, got {:?}", other),
3030 },
3031 other => panic!("expected test expr, got {:?}", other),
3032 },
3033 other => panic!("expected if, got {:?}", other),
3034 }
3035 }
3036
3037 #[test]
3038 fn parse_regex_not_match() {
3039 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
3040 match &result.statements[0] {
3041 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3042 Expr::Test(test) => match test.as_ref() {
3043 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
3044 other => panic!("expected comparison, got {:?}", other),
3045 },
3046 other => panic!("expected test expr, got {:?}", other),
3047 },
3048 other => panic!("expected if, got {:?}", other),
3049 }
3050 }
3051
3052 #[test]
3053 fn parse_string_interpolation() {
3054 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
3055 match &result.statements[0] {
3056 Stmt::Command(cmd) => match &cmd.args[0] {
3057 Arg::Positional(Expr::Interpolated(parts)) => {
3058 assert_eq!(parts.len(), 3);
3059 match &parts[0] {
3060 StringPart::Literal(s) => assert_eq!(s, "Hello "),
3061 other => panic!("expected literal, got {:?}", other),
3062 }
3063 match &parts[1] {
3064 StringPart::Var(path) => {
3065 assert_eq!(path.segments.len(), 1);
3066 let VarSegment::Field(name) = &path.segments[0];
3067 assert_eq!(name, "NAME");
3068 }
3069 other => panic!("expected var, got {:?}", other),
3070 }
3071 match &parts[2] {
3072 StringPart::Literal(s) => assert_eq!(s, "!"),
3073 other => panic!("expected literal, got {:?}", other),
3074 }
3075 }
3076 other => panic!("expected interpolated, got {:?}", other),
3077 },
3078 other => panic!("expected command, got {:?}", other),
3079 }
3080 }
3081
3082 #[test]
3083 fn parse_string_interpolation_multiple_vars() {
3084 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
3085 match &result.statements[0] {
3086 Stmt::Command(cmd) => match &cmd.args[0] {
3087 Arg::Positional(Expr::Interpolated(parts)) => {
3088 assert_eq!(parts.len(), 3);
3090 assert!(matches!(&parts[0], StringPart::Var(_)));
3091 assert!(matches!(&parts[1], StringPart::Literal(_)));
3092 assert!(matches!(&parts[2], StringPart::Var(_)));
3093 }
3094 other => panic!("expected interpolated, got {:?}", other),
3095 },
3096 other => panic!("expected command, got {:?}", other),
3097 }
3098 }
3099
3100 #[test]
3101 fn parse_empty_function_body() {
3102 let result = parse("empty() { }").unwrap();
3103 match &result.statements[0] {
3104 Stmt::ToolDef(t) => {
3105 assert_eq!(t.name, "empty");
3106 assert!(t.params.is_empty());
3107 assert!(t.body.is_empty());
3108 }
3109 other => panic!("expected function def, got {:?}", other),
3110 }
3111 }
3112
3113 #[test]
3114 fn parse_bash_style_function() {
3115 let result = parse("function greet { echo hello }").unwrap();
3116 match &result.statements[0] {
3117 Stmt::ToolDef(t) => {
3118 assert_eq!(t.name, "greet");
3119 assert!(t.params.is_empty());
3120 assert_eq!(t.body.len(), 1);
3121 }
3122 other => panic!("expected function def, got {:?}", other),
3123 }
3124 }
3125
3126 #[test]
3127 fn parse_comparison_string_values() {
3128 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
3129 match &result.statements[0] {
3130 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3131 Expr::Test(test) => match test.as_ref() {
3132 TestExpr::Comparison { left, op, right } => {
3133 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
3134 assert_eq!(*op, TestCmpOp::Eq);
3135 match right.as_ref() {
3136 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
3137 other => panic!("expected string, got {:?}", other),
3138 }
3139 }
3140 other => panic!("expected comparison, got {:?}", other),
3141 },
3142 other => panic!("expected test expr, got {:?}", other),
3143 },
3144 other => panic!("expected if, got {:?}", other),
3145 }
3146 }
3147
3148 #[test]
3153 fn parse_cmd_subst_simple() {
3154 let result = parse("X=$(echo)").unwrap();
3155 match &result.statements[0] {
3156 Stmt::Assignment(a) => {
3157 assert_eq!(a.name, "X");
3158 assert_eq!(subst_cmd(&a.value).name, "echo");
3159 }
3160 other => panic!("expected assignment, got {:?}", other),
3161 }
3162 }
3163
3164 #[test]
3165 fn parse_cmd_subst_with_args() {
3166 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
3167 match &result.statements[0] {
3168 Stmt::Assignment(a) => {
3169 let cmd = subst_cmd(&a.value);
3170 assert_eq!(cmd.name, "fetch");
3171 assert_eq!(cmd.args.len(), 1);
3172 match &cmd.args[0] {
3173 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
3174 other => panic!("expected WordAssign arg, got {:?}", other),
3175 }
3176 }
3177 other => panic!("expected assignment, got {:?}", other),
3178 }
3179 }
3180
3181 #[test]
3182 fn parse_cmd_subst_pipeline() {
3183 let result = parse("X=$(cat file | grep pattern)").unwrap();
3184 match &result.statements[0] {
3185 Stmt::Assignment(a) => {
3186 let pipeline = subst_pipeline(&a.value);
3187 assert_eq!(pipeline.commands.len(), 2);
3188 assert_eq!(pipeline.commands[0].name, "cat");
3189 assert_eq!(pipeline.commands[1].name, "grep");
3190 }
3191 other => panic!("expected assignment, got {:?}", other),
3192 }
3193 }
3194
3195 #[test]
3196 fn parse_cmd_subst_in_condition() {
3197 let result = parse("if kaish-validate; then echo; fi").unwrap();
3199 match &result.statements[0] {
3200 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3201 Expr::Command(cmd) => {
3202 assert_eq!(cmd.name, "kaish-validate");
3203 }
3204 other => panic!("expected command, got {:?}", other),
3205 },
3206 other => panic!("expected if, got {:?}", other),
3207 }
3208 }
3209
3210 #[test]
3215 fn parse_env_prefix_single() {
3216 let result = parse("FOO=bar echo hi").unwrap();
3217 match &result.statements[0] {
3218 Stmt::EnvScoped { assignments, body } => {
3219 assert_eq!(assignments.len(), 1);
3220 assert_eq!(assignments[0].name, "FOO");
3221 assert!(!assignments[0].local);
3222 match body.as_ref() {
3223 Stmt::Command(cmd) => assert_eq!(cmd.name, "echo"),
3224 other => panic!("expected command body, got {other:?}"),
3225 }
3226 }
3227 other => panic!("expected env-scoped, got {other:?}"),
3228 }
3229 }
3230
3231 #[test]
3232 fn parse_env_prefix_multiple() {
3233 let result = parse("A=1 B=2 run").unwrap();
3234 match &result.statements[0] {
3235 Stmt::EnvScoped { assignments, body } => {
3236 assert_eq!(assignments.len(), 2);
3237 assert_eq!(assignments[0].name, "A");
3238 assert_eq!(assignments[1].name, "B");
3239 assert!(matches!(body.as_ref(), Stmt::Command(c) if c.name == "run"));
3240 }
3241 other => panic!("expected env-scoped, got {other:?}"),
3242 }
3243 }
3244
3245 #[test]
3246 fn parse_bare_assignment_is_not_env_scoped() {
3247 let result = parse("FOO=bar").unwrap();
3249 assert!(
3250 matches!(&result.statements[0], Stmt::Assignment(a) if a.name == "FOO"),
3251 "got {:?}",
3252 result.statements[0]
3253 );
3254 }
3255
3256 #[test]
3257 fn parse_assignment_then_and_chain_does_not_over_capture() {
3258 let result = parse("FOO=bar && echo hi").unwrap();
3261 match &result.statements[0] {
3262 Stmt::AndChain { left, right } => {
3263 assert!(matches!(left.as_ref(), Stmt::Assignment(a) if a.name == "FOO"));
3264 assert!(matches!(right.as_ref(), Stmt::Command(c) if c.name == "echo"));
3265 }
3266 other => panic!("expected and-chain, got {other:?}"),
3267 }
3268 }
3269
3270 #[test]
3271 fn parse_env_prefix_pipeline_body() {
3272 let result = parse("FOO=bar cat | grep x").unwrap();
3273 match &result.statements[0] {
3274 Stmt::EnvScoped { assignments, body } => {
3275 assert_eq!(assignments[0].name, "FOO");
3276 match body.as_ref() {
3277 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 2),
3278 other => panic!("expected pipeline body, got {other:?}"),
3279 }
3280 }
3281 other => panic!("expected env-scoped, got {other:?}"),
3282 }
3283 }
3284
3285 fn parse_err_message(source: &str) -> String {
3290 parse(source)
3291 .expect_err("expected a parse error")
3292 .iter()
3293 .map(|e| e.message.clone())
3294 .collect::<Vec<_>>()
3295 .join(" ")
3296 }
3297
3298 #[test]
3299 fn argv_splat_cmdsubst_glued_to_path_is_rejected() {
3300 let msg = parse_err_message("echo /tmp/$(echo x).txt");
3303 assert!(msg.contains("quote"), "expected quote hint, got: {msg}");
3304 }
3305
3306 #[test]
3307 fn argv_splat_var_glued_to_path_is_rejected() {
3308 assert!(parse("echo $dir/out.txt").is_err());
3309 }
3310
3311 #[test]
3312 fn argv_splat_three_way_glue_is_rejected() {
3313 assert!(parse("echo foo$(echo bar)baz").is_err());
3314 }
3315
3316 #[test]
3317 fn argv_splat_quoted_word_is_accepted() {
3318 assert!(parse(r#"echo "/tmp/$(echo x).txt""#).is_ok());
3320 assert!(parse(r#"echo "$dir/out.txt""#).is_ok());
3321 }
3322
3323 #[test]
3324 fn argv_single_token_words_are_not_splat() {
3325 assert!(parse("echo file.txt").is_ok(), "file.txt");
3327 assert!(parse("echo a.b.c").is_ok(), "a.b.c");
3328 assert!(parse("echo v1.2.3").is_ok(), "v1.2.3");
3329 }
3330
3331 #[test]
3332 fn argv_spaced_words_are_not_splat() {
3333 assert!(parse("echo a b c").is_ok());
3334 assert!(parse("echo /tmp/x $(echo y)").is_ok());
3335 }
3336
3337 #[test]
3338 fn parse_cmd_subst_in_command_arg() {
3339 let result = parse("echo $(whoami)").unwrap();
3340 match &result.statements[0] {
3341 Stmt::Command(cmd) => {
3342 assert_eq!(cmd.name, "echo");
3343 match &cmd.args[0] {
3344 Arg::Positional(expr) => {
3345 assert_eq!(subst_cmd(expr).name, "whoami");
3346 }
3347 other => panic!("expected command subst, got {:?}", other),
3348 }
3349 }
3350 other => panic!("expected command, got {:?}", other),
3351 }
3352 }
3353
3354 #[test]
3359 fn parse_condition_and() {
3360 let result = parse("if check-a && check-b; then echo; fi").unwrap();
3362 match &result.statements[0] {
3363 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3364 Expr::BinaryOp { left, op, right } => {
3365 assert_eq!(*op, BinaryOp::And);
3366 assert!(matches!(left.as_ref(), Expr::Command(_)));
3367 assert!(matches!(right.as_ref(), Expr::Command(_)));
3368 }
3369 other => panic!("expected binary op, got {:?}", other),
3370 },
3371 other => panic!("expected if, got {:?}", other),
3372 }
3373 }
3374
3375 #[test]
3376 fn parse_condition_or() {
3377 let result = parse("if try-a || try-b; then echo; fi").unwrap();
3378 match &result.statements[0] {
3379 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3380 Expr::BinaryOp { left, op, right } => {
3381 assert_eq!(*op, BinaryOp::Or);
3382 assert!(matches!(left.as_ref(), Expr::Command(_)));
3383 assert!(matches!(right.as_ref(), Expr::Command(_)));
3384 }
3385 other => panic!("expected binary op, got {:?}", other),
3386 },
3387 other => panic!("expected if, got {:?}", other),
3388 }
3389 }
3390
3391 #[test]
3392 fn parse_condition_and_or_precedence() {
3393 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
3395 match &result.statements[0] {
3396 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3397 Expr::BinaryOp { left, op, right } => {
3398 assert_eq!(*op, BinaryOp::Or);
3400 match left.as_ref() {
3402 Expr::BinaryOp { op: inner_op, .. } => {
3403 assert_eq!(*inner_op, BinaryOp::And);
3404 }
3405 other => panic!("expected binary op (&&), got {:?}", other),
3406 }
3407 assert!(matches!(right.as_ref(), Expr::Command(_)));
3409 }
3410 other => panic!("expected binary op, got {:?}", other),
3411 },
3412 other => panic!("expected if, got {:?}", other),
3413 }
3414 }
3415
3416 #[test]
3417 fn parse_condition_multiple_and() {
3418 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
3419 match &result.statements[0] {
3420 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3421 Expr::BinaryOp { left, op, .. } => {
3422 assert_eq!(*op, BinaryOp::And);
3423 match left.as_ref() {
3425 Expr::BinaryOp { op: inner_op, .. } => {
3426 assert_eq!(*inner_op, BinaryOp::And);
3427 }
3428 other => panic!("expected binary op, got {:?}", other),
3429 }
3430 }
3431 other => panic!("expected binary op, got {:?}", other),
3432 },
3433 other => panic!("expected if, got {:?}", other),
3434 }
3435 }
3436
3437 #[test]
3438 fn parse_condition_mixed_comparison_and_logical() {
3439 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
3441 match &result.statements[0] {
3442 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3443 Expr::BinaryOp { left, op, right } => {
3444 assert_eq!(*op, BinaryOp::And);
3445 match left.as_ref() {
3447 Expr::Test(test) => match test.as_ref() {
3448 TestExpr::Comparison { op: left_op, .. } => {
3449 assert_eq!(*left_op, TestCmpOp::Eq);
3450 }
3451 other => panic!("expected comparison, got {:?}", other),
3452 },
3453 other => panic!("expected test, got {:?}", other),
3454 }
3455 match right.as_ref() {
3457 Expr::Test(test) => match test.as_ref() {
3458 TestExpr::Comparison { op: right_op, .. } => {
3459 assert_eq!(*right_op, TestCmpOp::NumGt);
3460 }
3461 other => panic!("expected comparison, got {:?}", other),
3462 },
3463 other => panic!("expected test, got {:?}", other),
3464 }
3465 }
3466 other => panic!("expected binary op, got {:?}", other),
3467 },
3468 other => panic!("expected if, got {:?}", other),
3469 }
3470 }
3471
3472 #[test]
3478 fn script_level1_linear() {
3479 let script = r#"
3480NAME="kaish"
3481VERSION=1
3482TIMEOUT=30
3483ITEMS="alpha beta gamma"
3484
3485echo "Starting ${NAME} v${VERSION}"
3486cat "README.md" | grep pattern="install" | head count=5
3487fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
3488echo "Items: ${ITEMS}"
3489"#;
3490 let result = parse(script).unwrap();
3491 let stmts: Vec<_> = result.statements.iter()
3492 .filter(|s| !matches!(s, Stmt::Empty))
3493 .collect();
3494
3495 assert_eq!(stmts.len(), 8);
3496 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(_))); }
3505
3506 #[test]
3508 fn script_level2_branching() {
3509 let script = r#"
3510RESULT=$(kaish-validate "input.json")
3511
3512if [[ ${RESULT.ok} == true ]]; then
3513 echo "Validation passed"
3514 process "input.json" > "output.json"
3515else
3516 echo "Validation failed: ${RESULT.err}"
3517fi
3518
3519if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
3520 echo "Count in valid range"
3521fi
3522
3523if check-network || check-cache; then
3524 fetch url=${URL}
3525fi
3526"#;
3527 let result = parse(script).unwrap();
3528 let stmts: Vec<_> = result.statements.iter()
3529 .filter(|s| !matches!(s, Stmt::Empty))
3530 .collect();
3531
3532 assert_eq!(stmts.len(), 4);
3533
3534 match stmts[0] {
3536 Stmt::Assignment(a) => {
3537 assert_eq!(a.name, "RESULT");
3538 assert!(matches!(&a.value, Expr::CommandSubst(_)));
3539 }
3540 other => panic!("expected assignment, got {:?}", other),
3541 }
3542
3543 match stmts[1] {
3545 Stmt::If(if_stmt) => {
3546 assert_eq!(if_stmt.then_branch.len(), 2);
3547 assert!(if_stmt.else_branch.is_some());
3548 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
3549 }
3550 other => panic!("expected if, got {:?}", other),
3551 }
3552
3553 match stmts[2] {
3555 Stmt::If(if_stmt) => {
3556 match if_stmt.condition.as_ref() {
3557 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3558 other => panic!("expected && condition, got {:?}", other),
3559 }
3560 }
3561 other => panic!("expected if, got {:?}", other),
3562 }
3563
3564 match stmts[3] {
3566 Stmt::If(if_stmt) => {
3567 match if_stmt.condition.as_ref() {
3568 Expr::BinaryOp { op, left, right } => {
3569 assert_eq!(*op, BinaryOp::Or);
3570 assert!(matches!(left.as_ref(), Expr::Command(_)));
3571 assert!(matches!(right.as_ref(), Expr::Command(_)));
3572 }
3573 other => panic!("expected || condition, got {:?}", other),
3574 }
3575 }
3576 other => panic!("expected if, got {:?}", other),
3577 }
3578 }
3579
3580 #[test]
3582 fn script_level3_loops_and_functions() {
3583 let script = r#"
3584greet() {
3585 echo "Hello, $1!"
3586}
3587
3588fetch_all() {
3589 for URL in $@; do
3590 fetch url=${URL}
3591 done
3592}
3593
3594USERS="alice bob charlie"
3595
3596for USER in ${USERS}; do
3597 greet ${USER}
3598 if [[ ${USER} == "bob" ]]; then
3599 echo "Found Bob!"
3600 fi
3601done
3602
3603long-running-task &
3604"#;
3605 let result = parse(script).unwrap();
3606 let stmts: Vec<_> = result.statements.iter()
3607 .filter(|s| !matches!(s, Stmt::Empty))
3608 .collect();
3609
3610 assert_eq!(stmts.len(), 5);
3611
3612 match stmts[0] {
3614 Stmt::ToolDef(t) => {
3615 assert_eq!(t.name, "greet");
3616 assert!(t.params.is_empty());
3617 }
3618 other => panic!("expected function def, got {:?}", other),
3619 }
3620
3621 match stmts[1] {
3623 Stmt::ToolDef(t) => {
3624 assert_eq!(t.name, "fetch_all");
3625 assert_eq!(t.body.len(), 1);
3626 assert!(matches!(&t.body[0], Stmt::For(_)));
3627 }
3628 other => panic!("expected function def, got {:?}", other),
3629 }
3630
3631 assert!(matches!(stmts[2], Stmt::Assignment(_)));
3633
3634 match stmts[3] {
3636 Stmt::For(f) => {
3637 assert_eq!(f.variable, "USER");
3638 assert_eq!(f.body.len(), 2);
3639 assert!(matches!(&f.body[0], Stmt::Command(_)));
3640 assert!(matches!(&f.body[1], Stmt::If(_)));
3641 }
3642 other => panic!("expected for loop, got {:?}", other),
3643 }
3644
3645 match stmts[4] {
3647 Stmt::Pipeline(p) => {
3648 assert!(p.background);
3649 assert_eq!(p.commands[0].name, "long-running-task");
3650 }
3651 other => panic!("expected pipeline (background), got {:?}", other),
3652 }
3653 }
3654
3655 #[test]
3657 fn script_level4_complex_nesting() {
3658 let script = r#"
3659RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
3660
3661if ping host=${HOST} && [[ ${RESULT} == true ]]; then
3662 for SERVER in "prod-1 prod-2"; do
3663 deploy target=${SERVER} port=8080
3664 if [[ $? -ne 0 ]]; then
3665 notify channel="ops" message="Deploy failed"
3666 fi
3667 done
3668fi
3669"#;
3670 let result = parse(script).unwrap();
3671 let stmts: Vec<_> = result.statements.iter()
3672 .filter(|s| !matches!(s, Stmt::Empty))
3673 .collect();
3674
3675 assert_eq!(stmts.len(), 2);
3676
3677 match stmts[0] {
3679 Stmt::Assignment(a) => {
3680 assert_eq!(a.name, "RESULT");
3681 assert_eq!(subst_pipeline(&a.value).commands.len(), 3);
3682 }
3683 other => panic!("expected assignment, got {:?}", other),
3684 }
3685
3686 match stmts[1] {
3688 Stmt::If(if_stmt) => {
3689 match if_stmt.condition.as_ref() {
3690 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3691 other => panic!("expected && condition, got {:?}", other),
3692 }
3693 assert_eq!(if_stmt.then_branch.len(), 1);
3694 match &if_stmt.then_branch[0] {
3695 Stmt::For(f) => {
3696 assert_eq!(f.body.len(), 2);
3697 assert!(matches!(&f.body[1], Stmt::If(_)));
3698 }
3699 other => panic!("expected for in if body, got {:?}", other),
3700 }
3701 }
3702 other => panic!("expected if, got {:?}", other),
3703 }
3704 }
3705
3706 #[test]
3708 fn script_level5_edge_cases() {
3709 let script = r#"
3710echo ""
3711echo "quotes: \"nested\" here"
3712echo "escapes: \n\t\r\\"
3713echo "unicode: \u2764"
3714
3715X=-99999
3716Y=3.14159265358979
3717Z=-0.001
3718
3719cmd a=1 b="two" c=true d=false e=null
3720
3721if true; then
3722 if false; then
3723 echo "inner"
3724 else
3725 echo "else"
3726 fi
3727fi
3728
3729for I in "a b c"; do
3730 echo ${I}
3731done
3732
3733no_params() {
3734 echo "no params"
3735}
3736
3737function all_args {
3738 echo "args: $@"
3739}
3740
3741a | b | c | d | e &
3742cmd 2> "errors.log"
3743cmd &> "all.log"
3744cmd >> "append.log"
3745cmd < "input.txt"
3746"#;
3747 let result = parse(script).unwrap();
3748 let stmts: Vec<_> = result.statements.iter()
3749 .filter(|s| !matches!(s, Stmt::Empty))
3750 .collect();
3751
3752 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
3754
3755 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
3757 assert!(bg_stmt.is_some(), "expected background pipeline");
3758
3759 match bg_stmt.unwrap() {
3760 Stmt::Pipeline(p) => {
3761 assert_eq!(p.commands.len(), 5);
3762 assert!(p.background);
3763 }
3764 _ => unreachable!(),
3765 }
3766 }
3767
3768 #[test]
3773 fn parse_keyword_as_variable_rejected() {
3774 let result = parse(r#"if="value""#);
3777 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
3778
3779 let result = parse("while=true");
3780 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
3781
3782 let result = parse(r#"then="next""#);
3783 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
3784 }
3785
3786 #[test]
3787 fn parse_set_command_with_flag() {
3788 let result = parse("set -e");
3789 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
3790 let program = result.unwrap();
3791 match &program.statements[0] {
3792 Stmt::Command(cmd) => {
3793 assert_eq!(cmd.name, "set");
3794 assert_eq!(cmd.args.len(), 1);
3795 match &cmd.args[0] {
3796 Arg::ShortFlag(f) => assert_eq!(f, "e"),
3797 other => panic!("expected ShortFlag, got {:?}", other),
3798 }
3799 }
3800 other => panic!("expected Command, got {:?}", other),
3801 }
3802 }
3803
3804 #[test]
3805 fn parse_set_command_no_args() {
3806 let result = parse("set");
3807 assert!(result.is_ok(), "failed to parse set: {:?}", result);
3808 let program = result.unwrap();
3809 match &program.statements[0] {
3810 Stmt::Command(cmd) => {
3811 assert_eq!(cmd.name, "set");
3812 assert_eq!(cmd.args.len(), 0);
3813 }
3814 other => panic!("expected Command, got {:?}", other),
3815 }
3816 }
3817
3818 #[test]
3819 fn parse_set_assignment_vs_command() {
3820 let result = parse("X=5");
3822 assert!(result.is_ok());
3823 let program = result.unwrap();
3824 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
3825
3826 let result = parse("set -e");
3828 assert!(result.is_ok());
3829 let program = result.unwrap();
3830 assert!(matches!(&program.statements[0], Stmt::Command(_)));
3831 }
3832
3833 #[test]
3834 fn parse_true_as_command() {
3835 let result = parse("true");
3836 assert!(result.is_ok());
3837 let program = result.unwrap();
3838 match &program.statements[0] {
3839 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
3840 other => panic!("expected Command(true), got {:?}", other),
3841 }
3842 }
3843
3844 #[test]
3845 fn parse_false_as_command() {
3846 let result = parse("false");
3847 assert!(result.is_ok());
3848 let program = result.unwrap();
3849 match &program.statements[0] {
3850 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
3851 other => panic!("expected Command(false), got {:?}", other),
3852 }
3853 }
3854
3855 #[test]
3856 fn parse_dot_as_source_alias() {
3857 let result = parse(". script.kai");
3858 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
3859 let program = result.unwrap();
3860 match &program.statements[0] {
3861 Stmt::Command(cmd) => {
3862 assert_eq!(cmd.name, ".");
3863 assert_eq!(cmd.args.len(), 1);
3864 }
3865 other => panic!("expected Command(.), got {:?}", other),
3866 }
3867 }
3868
3869 #[test]
3870 fn parse_source_command() {
3871 let result = parse("source utils.kai");
3872 assert!(result.is_ok(), "failed to parse source: {:?}", result);
3873 let program = result.unwrap();
3874 match &program.statements[0] {
3875 Stmt::Command(cmd) => {
3876 assert_eq!(cmd.name, "source");
3877 assert_eq!(cmd.args.len(), 1);
3878 }
3879 other => panic!("expected Command(source), got {:?}", other),
3880 }
3881 }
3882
3883 #[test]
3884 fn parse_test_expr_file_test() {
3885 let result = parse(r#"[[ -f "/path/file" ]]"#);
3887 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
3888 }
3889
3890 #[test]
3891 fn parse_test_expr_comparison() {
3892 let result = parse(r#"[[ $X == "value" ]]"#);
3893 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
3894 }
3895
3896 #[test]
3897 fn parse_test_expr_single_eq() {
3898 let result = parse(r#"[[ $X = "value" ]]"#);
3900 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
3901 let program = result.unwrap();
3902 match &program.statements[0] {
3903 Stmt::Test(TestExpr::Comparison { op, .. }) => {
3904 assert_eq!(op, &TestCmpOp::Eq);
3905 }
3906 other => panic!("expected Test(Comparison), got {:?}", other),
3907 }
3908 }
3909
3910 #[test]
3911 fn parse_while_loop() {
3912 let result = parse("while true; do echo; done");
3913 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
3914 let program = result.unwrap();
3915 assert!(matches!(&program.statements[0], Stmt::While(_)));
3916 }
3917
3918 #[test]
3919 fn parse_break_with_level() {
3920 let result = parse("break 2");
3921 assert!(result.is_ok());
3922 let program = result.unwrap();
3923 match &program.statements[0] {
3924 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
3925 other => panic!("expected Break(2), got {:?}", other),
3926 }
3927 }
3928
3929 #[test]
3930 fn parse_continue_with_level() {
3931 let result = parse("continue 3");
3932 assert!(result.is_ok());
3933 let program = result.unwrap();
3934 match &program.statements[0] {
3935 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
3936 other => panic!("expected Continue(3), got {:?}", other),
3937 }
3938 }
3939
3940 #[test]
3941 fn parse_exit_with_code() {
3942 let result = parse("exit 1");
3943 assert!(result.is_ok());
3944 let program = result.unwrap();
3945 match &program.statements[0] {
3946 Stmt::Exit(Some(expr)) => {
3947 match expr.as_ref() {
3948 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
3949 other => panic!("expected Int(1), got {:?}", other),
3950 }
3951 }
3952 other => panic!("expected Exit(1), got {:?}", other),
3953 }
3954 }
3955
3956 #[test]
3963 fn spanned_literal_only_records_byte_range() {
3964 let parts = parse_interpolated_string_spanned("hello world", 100);
3965 assert_eq!(parts.len(), 1);
3966 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
3967 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
3968 assert_eq!(parts[0].len, 11);
3969 }
3970
3971 #[test]
3972 fn spanned_braced_var_at_zero() {
3973 let parts = parse_interpolated_string_spanned("${X}", 50);
3974 assert_eq!(parts.len(), 1);
3975 assert!(matches!(&parts[0].part, StringPart::Var(_)));
3976 assert_eq!(parts[0].offset, 50);
3977 assert_eq!(parts[0].len, 4); }
3979
3980 #[test]
3981 fn spanned_simple_var_then_literal() {
3982 let parts = parse_interpolated_string_spanned("$X end", 10);
3983 assert_eq!(parts.len(), 2);
3984 assert!(matches!(&parts[0].part, StringPart::Var(_)));
3985 assert_eq!(parts[0].offset, 10);
3986 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
3988 assert_eq!(parts[1].offset, 12);
3989 assert_eq!(parts[1].len, 4);
3990 }
3991
3992 #[test]
3993 fn spanned_mixed_literal_var_literal() {
3994 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0);
3995 assert_eq!(parts.len(), 3);
3996 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
3998 assert_eq!(parts[0].offset, 0);
3999 assert_eq!(parts[0].len, 3);
4000 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4002 assert_eq!(parts[1].offset, 3);
4003 assert_eq!(parts[1].len, 4);
4004 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
4006 assert_eq!(parts[2].offset, 7);
4007 assert_eq!(parts[2].len, 4);
4008 }
4009
4010 #[test]
4011 fn spanned_positional_param() {
4012 let parts = parse_interpolated_string_spanned("$1 done", 0);
4013 assert_eq!(parts.len(), 2);
4014 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
4015 assert_eq!(parts[0].offset, 0);
4016 assert_eq!(parts[0].len, 2); }
4018
4019 #[test]
4020 fn spanned_special_dollar_dollar() {
4021 let parts = parse_interpolated_string_spanned("$$", 5);
4022 assert_eq!(parts.len(), 1);
4023 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
4024 assert_eq!(parts[0].offset, 5);
4025 assert_eq!(parts[0].len, 2);
4026 }
4027
4028 #[test]
4029 fn spanned_arithmetic_marker_recognised() {
4030 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0);
4034 assert_eq!(parts.len(), 1);
4035 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
4036 }
4037
4038 #[test]
4039 fn spanned_default_separator_yields_var_with_default() {
4040 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0);
4041 assert_eq!(parts.len(), 1);
4042 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
4043 assert_eq!(parts[0].offset, 0);
4044 assert_eq!(parts[0].len, 14); }
4046
4047 #[test]
4048 fn spanned_no_dollar_runs_one_literal() {
4049 let parts = parse_interpolated_string_spanned("plain text only", 7);
4050 assert_eq!(parts.len(), 1);
4051 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
4052 assert_eq!(parts[0].offset, 7);
4053 assert_eq!(parts[0].len, 15);
4054 }
4055
4056 #[test]
4057 fn spanned_matches_unspanned_part_count() {
4058 let cases = [
4061 "hello",
4062 "$X",
4063 "${X}",
4064 "${X:-d}",
4065 "hi $A and $B",
4066 "$0 $1 $2",
4067 "$$ $? $#",
4068 ];
4069 for s in &cases {
4070 let unspanned = parse_interpolated_string(s);
4071 let spanned = parse_interpolated_string_spanned(s, 0);
4072 assert_eq!(
4073 unspanned.len(),
4074 spanned.len(),
4075 "part count differs for {:?}",
4076 s
4077 );
4078 }
4079 }
4080
4081 #[test]
4082 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
4083 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0);
4088 assert_eq!(parts.len(), 2);
4089
4090 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
4091 assert_eq!(parts[0].offset, 0);
4092 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
4093
4094 assert!(matches!(&parts[1].part, StringPart::Var(_)));
4095 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
4096 assert_eq!(parts[1].len, 4);
4097 }
4098
4099 #[test]
4100 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
4101 let parts = parse_interpolated_string_spanned("hello 世界 world", 0);
4104 assert_eq!(parts.len(), 1);
4105 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
4106 assert_eq!(parts[0].offset, 0);
4107 assert_eq!(parts[0].len, 18);
4108 }
4109
4110 #[test]
4111 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
4112 let parts = parse_interpolated_string_spanned("\\$", 0);
4115 assert_eq!(parts.len(), 1);
4116 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
4117 assert_eq!(parts[0].offset, 0);
4118 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
4119 }
4120
4121 #[test]
4122 fn spanned_escape_backslash_collapses_pair_to_one() {
4123 let parts = parse_interpolated_string_spanned("\\\\", 0);
4124 assert_eq!(parts.len(), 1);
4125 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
4126 assert_eq!(parts[0].len, 2);
4127 }
4128}