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