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 stmt_to_pipeline(stmt: Stmt) -> Option<Pipeline> {
145 match stmt {
146 Stmt::Pipeline(p) => Some(p),
147 Stmt::Command(cmd) => Some(Pipeline {
148 commands: vec![cmd],
149 background: false,
150 }),
151 _ => None,
152 }
153}
154
155fn parse_interpolated_string_spanned(s: &str, base_offset: usize) -> Vec<SpannedPart> {
174 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
175
176 let chars_vec: Vec<char> = s.chars().collect();
177 let mut i = 0;
178 let mut pos: usize = 0;
179
180 let mut parts: Vec<SpannedPart> = Vec::new();
181 let mut current_text = String::new();
182 let mut current_text_start: usize = pos;
183
184 let push_literal =
185 |current_text: &mut String, start: &mut usize, end: usize, parts: &mut Vec<SpannedPart>| {
186 if !current_text.is_empty() {
187 parts.push(SpannedPart {
188 part: StringPart::Literal(std::mem::take(current_text)),
189 offset: base_offset + *start,
190 len: end - *start,
191 });
192 *start = end;
193 }
194 };
195
196 while i < chars_vec.len() {
197 let ch = chars_vec[i];
198
199 if ch == '\x00' {
200 let start = pos;
202 i += 1;
203 pos += 1;
204 let mut marker = String::new();
205 while let Some(&c) = chars_vec.get(i) {
206 if c == '\x00' {
207 i += 1;
208 pos += 1;
209 break;
210 }
211 marker.push(c);
212 i += 1;
213 pos += c.len_utf8();
214 }
215 if marker == "DOLLAR" {
216 if current_text.is_empty() {
217 current_text_start = start;
218 }
219 current_text.push('$');
220 }
221 } else if ch == '\\' {
222 let next = chars_vec.get(i + 1).copied();
228 match next {
229 Some('$') => {
230 if current_text.is_empty() {
231 current_text_start = pos;
232 }
233 current_text.push('$');
234 i += 2;
235 pos += 2;
236 }
237 Some('\\') => {
238 if current_text.is_empty() {
239 current_text_start = pos;
240 }
241 current_text.push('\\');
242 i += 2;
243 pos += 2;
244 }
245 Some('\n') => {
246 i += 2;
249 pos += 2;
250 if current_text.is_empty() {
251 current_text_start = pos;
252 }
253 }
254 Some('\r') => {
255 i += 2;
257 pos += 2;
258 if chars_vec.get(i) == Some(&'\n') {
259 i += 1;
260 pos += 1;
261 }
262 if current_text.is_empty() {
263 current_text_start = pos;
264 }
265 }
266 _ => {
267 if current_text.is_empty() {
271 current_text_start = pos;
272 }
273 current_text.push('\\');
274 i += 1;
275 pos += 1;
276 }
277 }
278 } else if ch == '$' {
279 let part_start = pos;
281 let next = chars_vec.get(i + 1).copied();
282
283 if next == Some('(') && chars_vec.get(i + 2) != Some(&'(') {
284 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
286 i += 2; pos += 2;
288 let mut cmd_content = String::new();
289 let mut depth = 1;
290 while let Some(&c) = chars_vec.get(i) {
291 i += 1;
292 pos += c.len_utf8();
293 if c == '(' {
294 depth += 1;
295 cmd_content.push(c);
296 } else if c == ')' {
297 depth -= 1;
298 if depth == 0 {
299 break;
300 }
301 cmd_content.push(c);
302 } else {
303 cmd_content.push(c);
304 }
305 }
306 let inserted = if let Ok(program) = parse(&cmd_content) {
307 if let Some(stmt) = program.statements.first() {
308 if let Some(pipeline) = stmt_to_pipeline(stmt.clone()) {
309 parts.push(SpannedPart {
310 part: StringPart::CommandSubst(pipeline),
311 offset: base_offset + part_start,
312 len: pos - part_start,
313 });
314 true
315 } else {
316 false
317 }
318 } else {
319 false
320 }
321 } else {
322 false
323 };
324 if inserted {
325 current_text_start = pos;
328 } else {
329 if current_text.is_empty() {
334 current_text_start = part_start;
335 }
336 current_text.push_str("$(");
337 current_text.push_str(&cmd_content);
338 current_text.push(')');
339 }
340 } else if next == Some('{') {
341 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
342 i += 2; pos += 2;
344 let mut var_content = String::new();
345 let mut depth = 1;
346 while let Some(&c) = chars_vec.get(i) {
347 i += 1;
348 pos += c.len_utf8();
349 if c == '{' && var_content.ends_with('$') {
350 depth += 1;
351 var_content.push(c);
352 } else if c == '}' {
353 depth -= 1;
354 if depth == 0 {
355 break;
356 }
357 var_content.push(c);
358 } else {
359 var_content.push(c);
360 }
361 }
362 let part = if let Some(name) = var_content.strip_prefix('#') {
363 StringPart::VarLength(name.to_string())
364 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
365 let expr = var_content
366 .strip_prefix("__ARITH:")
367 .and_then(|s| s.strip_suffix("__"))
368 .unwrap_or("");
369 StringPart::Arithmetic(expr.to_string())
370 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
371 let name = var_content[..colon_idx].to_string();
372 let default_str = &var_content[colon_idx + 2..];
373 let default = parse_interpolated_string(&unquote_default_word(default_str));
378 StringPart::VarWithDefault { name, default }
379 } else {
380 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
381 };
382 parts.push(SpannedPart {
383 part,
384 offset: base_offset + part_start,
385 len: pos - part_start,
386 });
387 current_text_start = pos;
388 } else if next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
389 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
390 i += 1; pos += 1;
392 if let Some(&digit) = chars_vec.get(i) {
393 let n = digit.to_digit(10).unwrap_or(0) as usize;
394 i += 1;
395 pos += digit.len_utf8();
396 parts.push(SpannedPart {
397 part: StringPart::Positional(n),
398 offset: base_offset + part_start,
399 len: pos - part_start,
400 });
401 }
402 current_text_start = pos;
403 } else if next == Some('@') {
404 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
405 i += 2; pos += 2;
407 parts.push(SpannedPart {
408 part: StringPart::AllArgs,
409 offset: base_offset + part_start,
410 len: pos - part_start,
411 });
412 current_text_start = pos;
413 } else if next == Some('#') {
414 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
415 i += 2; pos += 2;
417 parts.push(SpannedPart {
418 part: StringPart::ArgCount,
419 offset: base_offset + part_start,
420 len: pos - part_start,
421 });
422 current_text_start = pos;
423 } else if next == Some('?') {
424 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
425 i += 2; pos += 2;
427 parts.push(SpannedPart {
428 part: StringPart::LastExitCode,
429 offset: base_offset + part_start,
430 len: pos - part_start,
431 });
432 current_text_start = pos;
433 } else if next == Some('$') {
434 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
435 i += 2; pos += 2;
437 parts.push(SpannedPart {
438 part: StringPart::CurrentPid,
439 offset: base_offset + part_start,
440 len: pos - part_start,
441 });
442 current_text_start = pos;
443 } else if next.map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
444 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
445 i += 1; pos += 1;
447 let mut var_name = String::new();
448 while let Some(&c) = chars_vec.get(i) {
449 if c.is_ascii_alphanumeric() || c == '_' {
450 var_name.push(c);
451 i += 1;
452 pos += c.len_utf8();
453 } else {
454 break;
455 }
456 }
457 parts.push(SpannedPart {
458 part: StringPart::Var(VarPath::simple(var_name)),
459 offset: base_offset + part_start,
460 len: pos - part_start,
461 });
462 current_text_start = pos;
463 } else {
464 if current_text.is_empty() {
466 current_text_start = pos;
467 }
468 current_text.push(ch);
469 i += 1;
470 pos += 1;
471 }
472 } else {
473 if current_text.is_empty() {
474 current_text_start = pos;
475 }
476 current_text.push(ch);
477 i += 1;
478 pos += ch.len_utf8();
479 }
480 }
481
482 push_literal(&mut current_text, &mut current_text_start, pos, &mut parts);
483
484 parts
485}
486
487fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
488 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
491
492 let mut parts = Vec::new();
493 let mut current_text = String::new();
494 let mut chars = s.chars().peekable();
495
496 while let Some(ch) = chars.next() {
497 if ch == '\x00' {
498 let mut marker = String::new();
500 while let Some(&c) = chars.peek() {
501 if c == '\x00' {
502 chars.next(); break;
504 }
505 if let Some(c) = chars.next() {
506 marker.push(c);
507 }
508 }
509 if marker == "DOLLAR" {
510 current_text.push('$');
511 }
512 } else if ch == '$' {
513 if chars.peek() == Some(&'(') {
515 if !current_text.is_empty() {
517 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
518 }
519
520 chars.next();
522
523 let mut cmd_content = String::new();
525 let mut paren_depth = 1;
526 for c in chars.by_ref() {
527 if c == '(' {
528 paren_depth += 1;
529 cmd_content.push(c);
530 } else if c == ')' {
531 paren_depth -= 1;
532 if paren_depth == 0 {
533 break;
534 }
535 cmd_content.push(c);
536 } else {
537 cmd_content.push(c);
538 }
539 }
540
541 if let Ok(program) = parse(&cmd_content) {
544 if let Some(stmt) = program.statements.first() {
546 if let Some(pipeline) = stmt_to_pipeline(stmt.clone()) {
547 parts.push(StringPart::CommandSubst(pipeline));
548 } else {
549 current_text.push_str("$(");
551 current_text.push_str(&cmd_content);
552 current_text.push(')');
553 }
554 }
555 } else {
556 current_text.push_str("$(");
558 current_text.push_str(&cmd_content);
559 current_text.push(')');
560 }
561 } else if chars.peek() == Some(&'{') {
562 if !current_text.is_empty() {
564 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
565 }
566
567 chars.next();
569
570 let mut var_content = String::new();
572 let mut depth = 1;
573 for c in chars.by_ref() {
574 if c == '{' && var_content.ends_with('$') {
575 depth += 1;
576 var_content.push(c);
577 } else if c == '}' {
578 depth -= 1;
579 if depth == 0 {
580 break;
581 }
582 var_content.push(c);
583 } else {
584 var_content.push(c);
585 }
586 }
587
588 let part = if let Some(name) = var_content.strip_prefix('#') {
590 StringPart::VarLength(name.to_string())
592 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
593 let expr = var_content
595 .strip_prefix("__ARITH:")
596 .and_then(|s| s.strip_suffix("__"))
597 .unwrap_or("");
598 StringPart::Arithmetic(expr.to_string())
599 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
600 let name = var_content[..colon_idx].to_string();
602 let default_str = &var_content[colon_idx + 2..];
603 let default = parse_interpolated_string(&unquote_default_word(default_str));
604 StringPart::VarWithDefault { name, default }
605 } else {
606 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
608 };
609 parts.push(part);
610 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
611 if !current_text.is_empty() {
613 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
614 }
615 if let Some(digit) = chars.next() {
616 let n = digit.to_digit(10).unwrap_or(0) as usize;
617 parts.push(StringPart::Positional(n));
618 }
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::AllArgs);
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::ArgCount);
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::LastExitCode);
640 } else if chars.peek() == Some(&'$') {
641 if !current_text.is_empty() {
643 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
644 }
645 chars.next(); parts.push(StringPart::CurrentPid);
647 } else if chars.peek().map(|c| c.is_ascii_alphabetic() || *c == '_').unwrap_or(false) {
648 if !current_text.is_empty() {
650 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
651 }
652
653 let mut var_name = String::new();
655 while let Some(&c) = chars.peek() {
656 if c.is_ascii_alphanumeric() || c == '_' {
657 if let Some(c) = chars.next() {
658 var_name.push(c);
659 }
660 } else {
661 break;
662 }
663 }
664
665 parts.push(StringPart::Var(VarPath::simple(var_name)));
666 } else {
667 current_text.push(ch);
669 }
670 } else {
671 current_text.push(ch);
672 }
673 }
674
675 if !current_text.is_empty() {
676 parts.push(StringPart::Literal(current_text));
677 }
678
679 parts
680}
681
682#[derive(Debug, Clone)]
684pub struct ParseError {
685 pub span: Span,
686 pub message: String,
687}
688
689impl ParseError {
690 pub fn format(&self, source: &str) -> String {
695 let start = self.span.start;
696 let mut line = 1usize;
697 let mut col = 1usize;
698 for (i, ch) in source.char_indices() {
699 if i >= start {
700 break;
701 }
702 if ch == '\n' {
703 line += 1;
704 col = 1;
705 } else {
706 col += 1;
707 }
708 }
709 let line_content = {
710 let line_start = source[..start.min(source.len())]
711 .rfind('\n')
712 .map_or(0, |i| i + 1);
713 let line_end = source[start.min(source.len())..]
714 .find('\n')
715 .map_or(source.len(), |i| start + i);
716 source.get(line_start..line_end).unwrap_or("")
717 };
718 if line_content.is_empty() {
719 format!("{}:{} [parse]: {}", line, col, self.message)
720 } else {
721 format!(
722 "{}:{} [parse]: {}\n | {}",
723 line, col, self.message, line_content
724 )
725 }
726 }
727}
728
729impl std::fmt::Display for ParseError {
730 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
731 write!(f, "{} at {:?}", self.message, self.span)
732 }
733}
734
735impl std::error::Error for ParseError {}
736
737pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
739 let tokens = lexer::tokenize(source).map_err(|errs| {
741 errs.into_iter()
742 .map(|e| ParseError {
743 span: (e.span.start..e.span.end).into(),
744 message: format!("lexer error: {}", e.token),
745 })
746 .collect::<Vec<_>>()
747 })?;
748
749 let tokens: Vec<(Token, Span)> = tokens
751 .into_iter()
752 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
753 .collect();
754
755 let end_span: Span = (source.len()..source.len()).into();
757
758 let parser = program_parser();
760 let result = parser.parse(tokens.as_slice().map(end_span, |(t, s)| (t, s)));
761
762 let program = result.into_result().map_err(|errs| {
763 errs.into_iter()
764 .map(|e| ParseError {
765 span: *e.span(),
766 message: e.to_string(),
767 })
768 .collect::<Vec<_>>()
769 })?;
770
771 if first_ambiguous_stdin(&program.statements) {
776 return Err(vec![ParseError {
777 span: (0..0).into(),
781 message: "multiple stdin redirects on one command are ambiguous; \
782 use exactly one of `<`, `<<`, or `<<<`"
783 .to_string(),
784 }]);
785 }
786
787 Ok(program)
788}
789
790pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
792 let program = parse(source)?;
793 program
794 .statements
795 .into_iter()
796 .find(|s| !matches!(s, Stmt::Empty))
797 .ok_or_else(|| {
798 vec![ParseError {
799 span: (0..source.len()).into(),
800 message: "empty input".to_string(),
801 }]
802 })
803}
804
805fn program_parser<'tokens, 'src: 'tokens, I>(
811) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
812where
813 I: ValueInput<'tokens, Token = Token, Span = Span>,
814{
815 statement_parser()
816 .repeated()
817 .collect::<Vec<_>>()
818 .map(|statements| Program { statements })
819}
820
821fn statement_parser<'tokens, I>(
824) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
825where
826 I: ValueInput<'tokens, Token = Token, Span = Span>,
827{
828 recursive(|stmt| {
829 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
830
831 let break_stmt = just(Token::Break)
833 .ignore_then(
834 select! { Token::Int(n) => n as usize }.or_not()
835 )
836 .map(Stmt::Break);
837
838 let continue_stmt = just(Token::Continue)
840 .ignore_then(
841 select! { Token::Int(n) => n as usize }.or_not()
842 )
843 .map(Stmt::Continue);
844
845 let return_stmt = just(Token::Return)
847 .ignore_then(primary_expr_parser().or_not())
848 .map(|e| Stmt::Return(e.map(Box::new)));
849
850 let exit_stmt = just(Token::Exit)
852 .ignore_then(primary_expr_parser().or_not())
853 .map(|e| Stmt::Exit(e.map(Box::new)));
854
855 let set_flag_arg = choice((
864 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
865 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
866 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
868 ));
869
870 let option_value_str = select! {
874 Token::NumberIdent(s) => s,
875 Token::Int(n) => n.to_string(),
876 Token::Ident(s) => s,
877 };
878
879 let set_option_assign = ident_parser()
883 .then_ignore(just(Token::Eq))
884 .then(option_value_str)
885 .map(|(name, value)| {
886 Arg::Positional(Expr::Literal(Value::String(format!("{name}={value}"))))
887 });
888
889 let set_quoted_arg = select! {
893 Token::String(s) => Arg::Positional(Expr::Literal(Value::String(s))),
894 Token::SingleString(s) => Arg::Positional(Expr::Literal(Value::String(s))),
895 };
896
897 let set_with_flags = just(Token::Set)
899 .then(set_flag_arg)
900 .then(
901 choice((
902 set_flag_arg,
903 set_option_assign,
905 set_quoted_arg,
906 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
908 ))
909 .repeated()
910 .collect::<Vec<_>>(),
911 )
912 .map(|((_, first_arg), mut rest_args)| {
913 let mut args = vec![first_arg];
914 args.append(&mut rest_args);
915 Stmt::Command(Command {
916 name: "set".to_string(),
917 args,
918 redirects: vec![],
919 })
920 });
921
922 let set_no_args = just(Token::Set)
925 .then(
926 choice((
927 just(Token::Newline).to(()),
928 just(Token::Semi).to(()),
929 just(Token::And).to(()),
930 just(Token::Or).to(()),
931 end(),
932 ))
933 .rewind(),
934 )
935 .map(|_| Stmt::Command(Command {
936 name: "set".to_string(),
937 args: vec![],
938 redirects: vec![],
939 }));
940
941 let set_command = set_with_flags.or(set_no_args);
945
946 let base_statement = choice((
948 just(Token::Newline).to(Stmt::Empty),
949 set_command,
950 assignment_parser().map(Stmt::Assignment),
951 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), if_parser(stmt.clone()).map(Stmt::If),
955 for_parser(stmt.clone()).map(Stmt::For),
956 while_parser(stmt.clone()).map(Stmt::While),
957 case_parser(stmt.clone()).map(Stmt::Case),
958 break_stmt,
959 continue_stmt,
960 return_stmt,
961 exit_stmt,
962 test_expr_stmt_parser().map(Stmt::Test),
963 pipeline_parser().map(|p| {
965 if p.commands.len() == 1 && !p.background {
967 if p.commands[0].redirects.is_empty() {
969 match p.commands.into_iter().next() {
971 Some(cmd) => Stmt::Command(cmd),
972 None => Stmt::Empty, }
974 } else {
975 Stmt::Pipeline(p)
976 }
977 } else {
978 Stmt::Pipeline(p)
979 }
980 }),
981 ))
982 .boxed();
983
984 let and_chain = base_statement
988 .clone()
989 .foldl(
990 just(Token::And).ignore_then(base_statement).repeated(),
991 |left, right| Stmt::AndChain {
992 left: Box::new(left),
993 right: Box::new(right),
994 },
995 );
996
997 and_chain
998 .clone()
999 .foldl(
1000 just(Token::Or).ignore_then(and_chain).repeated(),
1001 |left, right| Stmt::OrChain {
1002 left: Box::new(left),
1003 right: Box::new(right),
1004 },
1005 )
1006 .then_ignore(terminator)
1007 })
1008}
1009
1010fn assignment_parser<'tokens, I>(
1012) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1013where
1014 I: ValueInput<'tokens, Token = Token, Span = Span>,
1015{
1016 let local_assignment = just(Token::Local)
1018 .ignore_then(ident_parser())
1019 .then_ignore(just(Token::Eq))
1020 .then(expr_parser())
1021 .map(|(name, value)| Assignment {
1022 name,
1023 value,
1024 local: true,
1025 });
1026
1027 let bash_assignment = ident_parser()
1030 .then_ignore(just(Token::Eq))
1031 .then(expr_parser())
1032 .map(|(name, value)| Assignment {
1033 name,
1034 value,
1035 local: false,
1036 });
1037
1038 choice((local_assignment, bash_assignment))
1039 .labelled("assignment")
1040 .boxed()
1041}
1042
1043fn posix_function_parser<'tokens, I, S>(
1047 stmt: S,
1048) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1049where
1050 I: ValueInput<'tokens, Token = Token, Span = Span>,
1051 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1052{
1053 ident_parser()
1054 .then_ignore(just(Token::LParen))
1055 .then_ignore(just(Token::RParen))
1056 .then_ignore(just(Token::LBrace))
1057 .then_ignore(just(Token::Newline).repeated())
1058 .then(
1059 stmt.repeated()
1060 .collect::<Vec<_>>()
1061 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1062 )
1063 .then_ignore(just(Token::Newline).repeated())
1064 .then_ignore(just(Token::RBrace))
1065 .map(|(name, body)| ToolDef { name, params: vec![], body })
1066 .labelled("POSIX function")
1067 .boxed()
1068}
1069
1070fn bash_function_parser<'tokens, I, S>(
1074 stmt: S,
1075) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1076where
1077 I: ValueInput<'tokens, Token = Token, Span = Span>,
1078 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1079{
1080 just(Token::Function)
1081 .ignore_then(ident_parser())
1082 .then_ignore(just(Token::LBrace))
1083 .then_ignore(just(Token::Newline).repeated())
1084 .then(
1085 stmt.repeated()
1086 .collect::<Vec<_>>()
1087 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1088 )
1089 .then_ignore(just(Token::Newline).repeated())
1090 .then_ignore(just(Token::RBrace))
1091 .map(|(name, body)| ToolDef { name, params: vec![], body })
1092 .labelled("bash function")
1093 .boxed()
1094}
1095
1096fn if_parser<'tokens, I, S>(
1103 stmt: S,
1104) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1105where
1106 I: ValueInput<'tokens, Token = Token, Span = Span>,
1107 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1108{
1109 let branch = condition_parser()
1111 .then_ignore(just(Token::Semi).or_not())
1112 .then_ignore(just(Token::Newline).repeated())
1113 .then_ignore(just(Token::Then))
1114 .then_ignore(just(Token::Newline).repeated())
1115 .then(
1116 stmt.clone()
1117 .repeated()
1118 .collect::<Vec<_>>()
1119 .map(|stmts: Vec<Stmt>| {
1120 stmts
1121 .into_iter()
1122 .filter(|s| !matches!(s, Stmt::Empty))
1123 .collect::<Vec<_>>()
1124 }),
1125 );
1126
1127 let elif_branch = just(Token::Elif)
1129 .ignore_then(condition_parser())
1130 .then_ignore(just(Token::Semi).or_not())
1131 .then_ignore(just(Token::Newline).repeated())
1132 .then_ignore(just(Token::Then))
1133 .then_ignore(just(Token::Newline).repeated())
1134 .then(
1135 stmt.clone()
1136 .repeated()
1137 .collect::<Vec<_>>()
1138 .map(|stmts: Vec<Stmt>| {
1139 stmts
1140 .into_iter()
1141 .filter(|s| !matches!(s, Stmt::Empty))
1142 .collect::<Vec<_>>()
1143 }),
1144 );
1145
1146 let else_branch = just(Token::Else)
1148 .ignore_then(just(Token::Newline).repeated())
1149 .ignore_then(stmt.repeated().collect::<Vec<_>>())
1150 .map(|stmts: Vec<Stmt>| {
1151 stmts
1152 .into_iter()
1153 .filter(|s| !matches!(s, Stmt::Empty))
1154 .collect::<Vec<_>>()
1155 });
1156
1157 just(Token::If)
1158 .ignore_then(branch)
1159 .then(elif_branch.repeated().collect::<Vec<_>>())
1160 .then(else_branch.or_not())
1161 .then_ignore(just(Token::Fi))
1162 .map(|(((condition, then_branch), elif_branches), else_branch)| {
1163 build_if_chain(condition, then_branch, elif_branches, else_branch)
1165 })
1166 .labelled("if statement")
1167 .boxed()
1168}
1169
1170fn build_if_chain(
1177 condition: Expr,
1178 then_branch: Vec<Stmt>,
1179 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
1180 else_branch: Option<Vec<Stmt>>,
1181) -> IfStmt {
1182 if elif_branches.is_empty() {
1183 IfStmt {
1185 condition: Box::new(condition),
1186 then_branch,
1187 else_branch,
1188 }
1189 } else {
1190 let (elif_cond, elif_then) = elif_branches.remove(0);
1192 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
1193 IfStmt {
1194 condition: Box::new(condition),
1195 then_branch,
1196 else_branch: Some(vec![Stmt::If(nested_if)]),
1197 }
1198 }
1199}
1200
1201fn for_parser<'tokens, I, S>(
1203 stmt: S,
1204) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1205where
1206 I: ValueInput<'tokens, Token = Token, Span = Span>,
1207 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1208{
1209 just(Token::For)
1210 .ignore_then(ident_parser())
1211 .then_ignore(just(Token::In))
1212 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
1213 .then_ignore(just(Token::Semi).or_not())
1214 .then_ignore(just(Token::Newline).repeated())
1215 .then_ignore(just(Token::Do))
1216 .then_ignore(just(Token::Newline).repeated())
1217 .then(
1218 stmt.repeated()
1219 .collect::<Vec<_>>()
1220 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1221 )
1222 .then_ignore(just(Token::Done))
1223 .map(|((variable, items), body)| ForLoop {
1224 variable,
1225 items,
1226 body,
1227 })
1228 .labelled("for loop")
1229 .boxed()
1230}
1231
1232fn while_parser<'tokens, I, S>(
1234 stmt: S,
1235) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1236where
1237 I: ValueInput<'tokens, Token = Token, Span = Span>,
1238 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1239{
1240 just(Token::While)
1241 .ignore_then(condition_parser())
1242 .then_ignore(just(Token::Semi).or_not())
1243 .then_ignore(just(Token::Newline).repeated())
1244 .then_ignore(just(Token::Do))
1245 .then_ignore(just(Token::Newline).repeated())
1246 .then(
1247 stmt.repeated()
1248 .collect::<Vec<_>>()
1249 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1250 )
1251 .then_ignore(just(Token::Done))
1252 .map(|(condition, body)| WhileLoop {
1253 condition: Box::new(condition),
1254 body,
1255 })
1256 .labelled("while loop")
1257 .boxed()
1258}
1259
1260fn case_parser<'tokens, I, S>(
1267 stmt: S,
1268) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1269where
1270 I: ValueInput<'tokens, Token = Token, Span = Span>,
1271 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
1272{
1273 let pattern_part = choice((
1276 select! { Token::GlobWord(s) => s },
1277 select! { Token::Ident(s) => s },
1278 select! { Token::NumberIdent(s) => s },
1279 select! { Token::DottedIdent(s) => s },
1280 select! { Token::String(s) => s },
1281 select! { Token::SingleString(s) => s },
1282 select! { Token::Int(n) => n.to_string() },
1283 select! { Token::Star => "*".to_string() },
1284 select! { Token::Question => "?".to_string() },
1285 select! { Token::Dot => ".".to_string() },
1286 select! { Token::DotDot => "..".to_string() },
1287 select! { Token::Tilde => "~".to_string() },
1288 select! { Token::TildePath(s) => s },
1289 select! { Token::RelativePath(s) => s },
1290 select! { Token::DotSlashPath(s) => s },
1291 select! { Token::Path(p) => p },
1292 select! { Token::VarRef(v) => v },
1293 select! { Token::SimpleVarRef(v) => format!("${}", v) },
1294 just(Token::LBracket)
1296 .ignore_then(
1297 choice((
1298 select! { Token::Ident(s) => s },
1299 select! { Token::Int(n) => n.to_string() },
1300 just(Token::Colon).to(":".to_string()),
1301 just(Token::Bang).to("!".to_string()),
1303 select! { Token::ShortFlag(s) => format!("-{}", s) },
1305 ))
1306 .repeated()
1307 .at_least(1)
1308 .collect::<Vec<String>>()
1309 )
1310 .then_ignore(just(Token::RBracket))
1311 .map(|parts| format!("[{}]", parts.join(""))),
1312 just(Token::LBrace)
1314 .ignore_then(
1315 choice((
1316 select! { Token::Ident(s) => s },
1317 select! { Token::Int(n) => n.to_string() },
1318 ))
1319 .separated_by(just(Token::Comma))
1320 .at_least(1)
1321 .collect::<Vec<String>>()
1322 )
1323 .then_ignore(just(Token::RBrace))
1324 .map(|parts| format!("{{{}}}", parts.join(","))),
1325 ));
1326
1327 let pattern = pattern_part
1330 .repeated()
1331 .at_least(1)
1332 .collect::<Vec<String>>()
1333 .map(|parts| parts.join(""))
1334 .labelled("case pattern");
1335
1336 let patterns = pattern
1338 .separated_by(just(Token::Pipe))
1339 .at_least(1)
1340 .collect::<Vec<String>>()
1341 .labelled("case patterns");
1342
1343 let branch = just(Token::LParen)
1345 .or_not()
1346 .ignore_then(just(Token::Newline).repeated())
1347 .ignore_then(patterns)
1348 .then_ignore(just(Token::RParen))
1349 .then_ignore(just(Token::Newline).repeated())
1350 .then(
1351 stmt.clone()
1352 .repeated()
1353 .collect::<Vec<_>>()
1354 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
1355 )
1356 .then_ignore(just(Token::DoubleSemi))
1357 .then_ignore(just(Token::Newline).repeated())
1358 .map(|(patterns, body)| CaseBranch { patterns, body })
1359 .labelled("case branch");
1360
1361 just(Token::Case)
1362 .ignore_then(expr_parser())
1363 .then_ignore(just(Token::In))
1364 .then_ignore(just(Token::Newline).repeated())
1365 .then(branch.repeated().collect::<Vec<_>>())
1366 .then_ignore(just(Token::Esac))
1367 .map(|(expr, branches)| CaseStmt { expr, branches })
1368 .labelled("case statement")
1369 .boxed()
1370}
1371
1372fn pipeline_parser<'tokens, I>(
1374) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1375where
1376 I: ValueInput<'tokens, Token = Token, Span = Span>,
1377{
1378 command_parser()
1379 .separated_by(just(Token::Pipe))
1380 .at_least(1)
1381 .collect::<Vec<_>>()
1382 .then(just(Token::Amp).or_not())
1383 .map(|(commands, bg)| Pipeline {
1384 commands,
1385 background: bg.is_some(),
1386 })
1387 .labelled("pipeline")
1388 .boxed()
1389}
1390
1391fn command_parser<'tokens, I>(
1394) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1395where
1396 I: ValueInput<'tokens, Token = Token, Span = Span>,
1397{
1398 let command_name = choice((
1400 ident_parser(),
1401 path_parser(),
1402 select! { Token::DotSlashPath(s) => s },
1403 just(Token::True).to("true".to_string()),
1404 just(Token::False).to("false".to_string()),
1405 just(Token::Dot).to(".".to_string()),
1406 ));
1407
1408 command_name
1418 .then(args_list_parser())
1419 .then(redirect_parser().repeated().collect::<Vec<_>>())
1420 .map(|((name, args), redirects)| Command {
1421 name,
1422 args,
1423 redirects,
1424 })
1425 .labelled("command")
1426 .boxed()
1427}
1428
1429fn command_has_ambiguous_stdin(cmd: &Command) -> bool {
1433 cmd.redirects
1434 .iter()
1435 .filter(|r| {
1436 matches!(
1437 r.kind,
1438 RedirectKind::Stdin | RedirectKind::HereDoc | RedirectKind::HereString
1439 )
1440 })
1441 .count()
1442 > 1
1443}
1444
1445fn first_ambiguous_stdin(stmts: &[Stmt]) -> bool {
1449 stmts.iter().any(stmt_has_ambiguous_stdin)
1450}
1451
1452fn stmt_has_ambiguous_stdin(stmt: &Stmt) -> bool {
1453 match stmt {
1454 Stmt::Command(c) => command_has_ambiguous_stdin(c),
1455 Stmt::Pipeline(p) => p.commands.iter().any(command_has_ambiguous_stdin),
1456 Stmt::If(i) => {
1457 first_ambiguous_stdin(&i.then_branch)
1458 || i.else_branch
1459 .as_deref()
1460 .is_some_and(first_ambiguous_stdin)
1461 }
1462 Stmt::For(f) => first_ambiguous_stdin(&f.body),
1463 Stmt::While(w) => first_ambiguous_stdin(&w.body),
1464 Stmt::Case(c) => c.branches.iter().any(|b| first_ambiguous_stdin(&b.body)),
1465 Stmt::ToolDef(t) => first_ambiguous_stdin(&t.body),
1466 Stmt::AndChain { left, right } | Stmt::OrChain { left, right } => {
1467 stmt_has_ambiguous_stdin(left) || stmt_has_ambiguous_stdin(right)
1468 }
1469 Stmt::Assignment(_)
1470 | Stmt::Break(_)
1471 | Stmt::Continue(_)
1472 | Stmt::Return(_)
1473 | Stmt::Exit(_)
1474 | Stmt::Test(_)
1475 | Stmt::Empty => false,
1476 }
1477}
1478
1479fn args_list_parser<'tokens, I>(
1483) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1484where
1485 I: ValueInput<'tokens, Token = Token, Span = Span>,
1486{
1487 let pre_dash = arg_before_double_dash_parser()
1489 .repeated()
1490 .collect::<Vec<_>>();
1491
1492 let double_dash = select! {
1494 Token::DoubleDash => Arg::DoubleDash,
1495 };
1496
1497 let post_dash_arg = choice((
1499 select! {
1501 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
1502 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
1503 },
1504 primary_expr_parser().map(Arg::Positional),
1506 ));
1507
1508 let post_dash = post_dash_arg.repeated().collect::<Vec<_>>();
1509
1510 pre_dash
1512 .then(double_dash.then(post_dash).or_not())
1513 .map(|(mut args, maybe_dd)| {
1514 if let Some((dd, post)) = maybe_dd {
1515 args.push(dd);
1516 args.extend(post);
1517 }
1518 args
1519 })
1520}
1521
1522fn arg_before_double_dash_parser<'tokens, I>(
1524) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1525where
1526 I: ValueInput<'tokens, Token = Token, Span = Span>,
1527{
1528 let long_flag_with_value = select! {
1530 Token::LongFlag(name) => name,
1531 }
1532 .then_ignore(just(Token::Eq))
1533 .then(primary_expr_parser())
1534 .map(|(key, value)| Arg::Named { key, value });
1535
1536 let long_flag = select! {
1538 Token::LongFlag(name) => Arg::LongFlag(name),
1539 };
1540
1541 let short_flag = select! {
1543 Token::ShortFlag(name) => Arg::ShortFlag(name),
1544 };
1545
1546 let named = select! {
1552 Token::Ident(s) => s,
1553 }
1554 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1555 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1556 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1557 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1558 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1560 Err(Rich::custom(
1561 span,
1562 "shell assignment must not have spaces around '=' (use 'key=value' not 'key = value')",
1563 ))
1564 } else {
1565 Ok(Arg::WordAssign { key, value })
1566 }
1567 });
1568
1569 let positional = primary_expr_parser().map(Arg::Positional);
1571
1572 choice((
1575 long_flag_with_value,
1576 long_flag,
1577 short_flag,
1578 named,
1579 positional,
1580 ))
1581 .boxed()
1582}
1583
1584fn redirect_parser<'tokens, I>(
1586) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1587where
1588 I: ValueInput<'tokens, Token = Token, Span = Span>,
1589{
1590 let regular_redirect = select! {
1592 Token::GtGt => RedirectKind::StdoutAppend,
1593 Token::Gt => RedirectKind::StdoutOverwrite,
1594 Token::Lt => RedirectKind::Stdin,
1595 Token::Stderr => RedirectKind::Stderr,
1596 Token::Both => RedirectKind::Both,
1597 }
1598 .then(primary_expr_parser())
1599 .map(|(kind, target)| Redirect { kind, target });
1600
1601 let heredoc_redirect = just(Token::HereDocStart)
1609 .ignore_then(select! { Token::HereDoc(data) => data })
1610 .map(|data: HereDocData| {
1611 let target = if data.literal {
1612 let body = if data.strip_tabs {
1613 crate::interpreter::strip_leading_tabs(&data.content)
1614 } else {
1615 data.content
1616 };
1617 Expr::Literal(Value::String(body))
1618 } else {
1619 let parts = parse_interpolated_string_spanned(
1620 &data.content,
1621 data.body_start_offset,
1622 );
1623 if parts.len() == 1 && !data.strip_tabs {
1627 if let StringPart::Literal(text) = &parts[0].part {
1628 return Redirect {
1629 kind: RedirectKind::HereDoc,
1630 target: Expr::Literal(Value::String(text.clone())),
1631 };
1632 }
1633 }
1634 Expr::HereDocBody {
1635 parts,
1636 strip_tabs: data.strip_tabs,
1637 }
1638 };
1639 Redirect {
1640 kind: RedirectKind::HereDoc,
1641 target,
1642 }
1643 });
1644
1645 let herestring_redirect = just(Token::HereString)
1649 .ignore_then(primary_expr_parser())
1650 .map(|target| Redirect {
1651 kind: RedirectKind::HereString,
1652 target,
1653 });
1654
1655 let merge_stderr_redirect = just(Token::StderrToStdout)
1657 .map(|_| Redirect {
1658 kind: RedirectKind::MergeStderr,
1659 target: Expr::Literal(Value::Null),
1661 });
1662
1663 let merge_stdout_redirect = choice((
1665 just(Token::StdoutToStderr),
1666 just(Token::StdoutToStderr2),
1667 ))
1668 .map(|_| Redirect {
1669 kind: RedirectKind::MergeStdout,
1670 target: Expr::Literal(Value::Null),
1672 });
1673
1674 choice((
1675 heredoc_redirect,
1676 herestring_redirect,
1677 merge_stderr_redirect,
1678 merge_stdout_redirect,
1679 regular_redirect,
1680 ))
1681 .labelled("redirect")
1682 .boxed()
1683}
1684
1685fn test_expr_stmt_parser<'tokens, I>(
1695) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1696where
1697 I: ValueInput<'tokens, Token = Token, Span = Span>,
1698{
1699 let file_test_op = select! {
1701 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
1702 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
1703 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
1704 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
1705 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
1706 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
1707 };
1708
1709 let string_test_op = select! {
1711 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
1712 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
1713 };
1714
1715 let cmp_op = choice((
1718 just(Token::EqEq).to(TestCmpOp::Eq),
1719 just(Token::Eq).to(TestCmpOp::Eq),
1720 just(Token::NotEq).to(TestCmpOp::NotEq),
1721 just(Token::Match).to(TestCmpOp::Match),
1722 just(Token::NotMatch).to(TestCmpOp::NotMatch),
1723 just(Token::Gt).to(TestCmpOp::Gt),
1724 just(Token::Lt).to(TestCmpOp::Lt),
1725 just(Token::GtEq).to(TestCmpOp::GtEq),
1726 just(Token::LtEq).to(TestCmpOp::LtEq),
1727 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::NumEq },
1728 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NumNotEq },
1729 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::NumGt },
1730 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::NumLt },
1731 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::NumGtEq },
1732 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::NumLtEq },
1733 ));
1734
1735 let file_test = file_test_op
1737 .then(primary_expr_parser())
1738 .map(|(op, path)| TestExpr::FileTest {
1739 op,
1740 path: Box::new(path),
1741 });
1742
1743 let string_test = string_test_op
1745 .then(primary_expr_parser())
1746 .map(|(op, value)| TestExpr::StringTest {
1747 op,
1748 value: Box::new(value),
1749 });
1750
1751 let comparison = primary_expr_parser()
1753 .then(cmp_op)
1754 .then(primary_expr_parser())
1755 .map(|((left, op), right)| TestExpr::Comparison {
1756 left: Box::new(left),
1757 op,
1758 right: Box::new(right),
1759 });
1760
1761 let primary_test = choice((file_test, string_test, comparison));
1763
1764 let unary = recursive(|unary| {
1778 let not_expr = just(Token::Bang)
1779 .ignore_then(unary)
1780 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
1781 choice((not_expr, primary_test.clone()))
1782 });
1783
1784 let and_expr = unary.clone().foldl(
1786 just(Token::And).ignore_then(unary).repeated(),
1787 |left, right| TestExpr::And {
1788 left: Box::new(left),
1789 right: Box::new(right),
1790 },
1791 );
1792
1793 let compound_test = and_expr.clone().foldl(
1795 just(Token::Or).ignore_then(and_expr).repeated(),
1796 |left, right| TestExpr::Or {
1797 left: Box::new(left),
1798 right: Box::new(right),
1799 },
1800 );
1801
1802 just(Token::LBracket)
1805 .then(just(Token::LBracket))
1806 .ignore_then(compound_test)
1807 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
1808 .labelled("test expression")
1809 .boxed()
1810}
1811
1812fn condition_parser<'tokens, I>(
1827) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1828where
1829 I: ValueInput<'tokens, Token = Token, Span = Span>,
1830{
1831 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
1833
1834 let command_condition = command_parser().map(Expr::Command);
1837
1838 let base = choice((test_expr_condition, command_condition));
1840
1841 let and_expr = base.clone().foldl(
1844 just(Token::And).ignore_then(base).repeated(),
1845 |left, right| Expr::BinaryOp {
1846 left: Box::new(left),
1847 op: BinaryOp::And,
1848 right: Box::new(right),
1849 },
1850 );
1851
1852 and_expr
1854 .clone()
1855 .foldl(
1856 just(Token::Or).ignore_then(and_expr).repeated(),
1857 |left, right| Expr::BinaryOp {
1858 left: Box::new(left),
1859 op: BinaryOp::Or,
1860 right: Box::new(right),
1861 },
1862 )
1863 .labelled("condition")
1864 .boxed()
1865}
1866
1867fn expr_parser<'tokens, I>(
1869) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1870where
1871 I: ValueInput<'tokens, Token = Token, Span = Span>,
1872{
1873 primary_expr_parser()
1875}
1876
1877fn primary_expr_parser<'tokens, I>(
1881) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1882where
1883 I: ValueInput<'tokens, Token = Token, Span = Span>,
1884{
1885 let positional = select! {
1887 Token::Positional(n) => Expr::Positional(n),
1888 Token::AllArgs => Expr::AllArgs,
1889 Token::ArgCount => Expr::ArgCount,
1890 Token::VarLength(name) => Expr::VarLength(name),
1891 Token::LastExitCode => Expr::LastExitCode,
1892 Token::CurrentPid => Expr::CurrentPid,
1893 };
1894
1895 let arithmetic = select! {
1897 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
1898 };
1899
1900 let keyword_as_bareword = select! {
1903 Token::Done => "done",
1904 Token::Fi => "fi",
1905 Token::Then => "then",
1906 Token::Else => "else",
1907 Token::Elif => "elif",
1908 Token::In => "in",
1909 Token::Do => "do",
1910 Token::Esac => "esac",
1911 Token::Set => "set",
1916 }
1917 .map(|s| Expr::Literal(Value::String(s.to_string())));
1918
1919 let plus_minus_bare = select! {
1921 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
1922 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
1923 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
1924 };
1925
1926 let glob_pattern = select! {
1928 Token::GlobWord(s) => Expr::GlobPattern(s),
1929 Token::Star => Expr::GlobPattern("*".to_string()),
1930 Token::Question => Expr::GlobPattern("?".to_string()),
1931 };
1932
1933 recursive(|expr| {
1934 choice((
1935 positional,
1936 arithmetic,
1937 cmd_subst_parser(expr.clone()),
1938 var_expr_parser(),
1939 interpolated_string_parser(),
1940 literal_parser().map(Expr::Literal),
1941 glob_pattern,
1943 ident_parser().map(|s| Expr::Literal(Value::String(s))),
1945 path_parser().map(|s| Expr::Literal(Value::String(s))),
1947 select! {
1950 Token::Dot => Expr::Literal(Value::String(".".into())),
1956 Token::DotDot => Expr::Literal(Value::String("..".into())),
1957 Token::Tilde => Expr::Literal(Value::String("~".into())),
1958 Token::TildePath(s) => Expr::Literal(Value::String(s)),
1959 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
1960 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
1961 Token::NumberIdent(s) => Expr::Literal(Value::String(s)),
1963 Token::DottedIdent(s) => Expr::Literal(Value::String(s)),
1968 Token::JobSpec(s) => Expr::Literal(Value::String(s)),
1971 },
1972 plus_minus_bare,
1973 keyword_as_bareword,
1975 ))
1976 .labelled("expression")
1977 })
1978 .boxed()
1979}
1980
1981fn var_expr_parser<'tokens, I>(
1984) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1985where
1986 I: ValueInput<'tokens, Token = Token, Span = Span>,
1987{
1988 select! {
1989 Token::VarRef(raw) => parse_var_expr(&raw),
1990 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
1991 }
1992 .labelled("variable reference")
1993}
1994
1995fn cmd_subst_parser<'tokens, I, E>(
1999 expr: E,
2000) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2001where
2002 I: ValueInput<'tokens, Token = Token, Span = Span>,
2003 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone,
2004{
2005 let long_flag_with_value = select! {
2008 Token::LongFlag(name) => name,
2009 }
2010 .then_ignore(just(Token::Eq))
2011 .then(expr.clone())
2012 .map(|(key, value)| Arg::Named { key, value });
2013
2014 let long_flag = select! {
2016 Token::LongFlag(name) => Arg::LongFlag(name),
2017 };
2018
2019 let short_flag = select! {
2021 Token::ShortFlag(name) => Arg::ShortFlag(name),
2022 };
2023
2024 let named = ident_parser()
2026 .then_ignore(just(Token::Eq))
2027 .then(expr.clone())
2028 .map(|(key, value)| Arg::WordAssign { key, value });
2029
2030 let positional = expr.map(Arg::Positional);
2032
2033 let arg = choice((
2034 long_flag_with_value,
2035 long_flag,
2036 short_flag,
2037 named,
2038 positional,
2039 ));
2040
2041 let command_name = choice((
2043 ident_parser(),
2044 just(Token::True).to("true".to_string()),
2045 just(Token::False).to("false".to_string()),
2046 ));
2047
2048 let command = command_name
2050 .then(arg.repeated().collect::<Vec<_>>())
2051 .map(|(name, args)| Command {
2052 name,
2053 args,
2054 redirects: vec![],
2055 });
2056
2057 let pipeline = command
2059 .separated_by(just(Token::Pipe))
2060 .at_least(1)
2061 .collect::<Vec<_>>()
2062 .map(|commands| Pipeline {
2063 commands,
2064 background: false,
2065 });
2066
2067 just(Token::CmdSubstStart)
2068 .ignore_then(pipeline)
2069 .then_ignore(just(Token::RParen))
2070 .map(|pipeline| Expr::CommandSubst(Box::new(pipeline)))
2071 .labelled("command substitution")
2072}
2073
2074fn interpolated_string_parser<'tokens, I>(
2076) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2077where
2078 I: ValueInput<'tokens, Token = Token, Span = Span>,
2079{
2080 let double_quoted = select! {
2082 Token::String(s) => s,
2083 }
2084 .map(|s| {
2085 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
2087 let parts = parse_interpolated_string(&s);
2089 if parts.len() == 1
2090 && let StringPart::Literal(text) = &parts[0] {
2091 return Expr::Literal(Value::String(text.clone()));
2092 }
2093 Expr::Interpolated(parts)
2094 } else {
2095 Expr::Literal(Value::String(s))
2096 }
2097 });
2098
2099 let single_quoted = select! {
2101 Token::SingleString(s) => Expr::Literal(Value::String(s)),
2102 };
2103
2104 choice((single_quoted, double_quoted)).labelled("string")
2105}
2106
2107fn literal_parser<'tokens, I>(
2109) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2110where
2111 I: ValueInput<'tokens, Token = Token, Span = Span>,
2112{
2113 choice((
2114 select! {
2115 Token::True => Value::Bool(true),
2116 Token::False => Value::Bool(false),
2117 },
2118 select! {
2119 Token::Int(n) => Value::Int(n),
2120 Token::Float(f) => Value::Float(f),
2121 },
2122 ))
2123 .labelled("literal")
2124 .boxed()
2125}
2126
2127fn ident_parser<'tokens, I>(
2129) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2130where
2131 I: ValueInput<'tokens, Token = Token, Span = Span>,
2132{
2133 select! {
2134 Token::Ident(s) => s,
2135 }
2136 .labelled("identifier")
2137}
2138
2139fn path_parser<'tokens, I>(
2141) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
2142where
2143 I: ValueInput<'tokens, Token = Token, Span = Span>,
2144{
2145 select! {
2146 Token::Path(s) => s,
2147 }
2148 .labelled("path")
2149}
2150
2151#[cfg(test)]
2152mod tests {
2153 use super::*;
2154
2155 #[test]
2156 fn parse_empty() {
2157 let result = parse("");
2158 assert!(result.is_ok());
2159 assert_eq!(result.expect("ok").statements.len(), 0);
2160 }
2161
2162 #[test]
2163 fn parse_newlines_only() {
2164 let result = parse("\n\n\n");
2165 assert!(result.is_ok());
2166 }
2167
2168 #[test]
2169 fn parse_simple_command() {
2170 let result = parse("echo");
2171 assert!(result.is_ok());
2172 let program = result.expect("ok");
2173 assert_eq!(program.statements.len(), 1);
2174 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2175 }
2176
2177 #[test]
2178 fn parse_command_with_string_arg() {
2179 let result = parse(r#"echo "hello""#);
2180 assert!(result.is_ok());
2181 let program = result.expect("ok");
2182 match &program.statements[0] {
2183 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
2184 _ => panic!("expected Command"),
2185 }
2186 }
2187
2188 #[test]
2189 fn parse_assignment() {
2190 let result = parse("X=5");
2191 assert!(result.is_ok());
2192 let program = result.expect("ok");
2193 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2194 }
2195
2196 #[test]
2197 fn parse_pipeline() {
2198 let result = parse("a | b | c");
2199 assert!(result.is_ok());
2200 let program = result.expect("ok");
2201 match &program.statements[0] {
2202 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2203 _ => panic!("expected Pipeline"),
2204 }
2205 }
2206
2207 #[test]
2208 fn parse_background_job() {
2209 let result = parse("cmd &");
2210 assert!(result.is_ok());
2211 let program = result.expect("ok");
2212 match &program.statements[0] {
2213 Stmt::Pipeline(p) => assert!(p.background),
2214 _ => panic!("expected Pipeline with background"),
2215 }
2216 }
2217
2218 #[test]
2219 fn parse_if_simple() {
2220 let result = parse("if true; then echo; fi");
2221 assert!(result.is_ok());
2222 let program = result.expect("ok");
2223 assert!(matches!(&program.statements[0], Stmt::If(_)));
2224 }
2225
2226 #[test]
2227 fn parse_if_else() {
2228 let result = parse("if true; then echo; else echo; fi");
2229 assert!(result.is_ok());
2230 let program = result.expect("ok");
2231 match &program.statements[0] {
2232 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
2233 _ => panic!("expected If"),
2234 }
2235 }
2236
2237 #[test]
2238 fn parse_elif_simple() {
2239 let result = parse("if true; then echo a; elif false; then echo b; fi");
2240 assert!(result.is_ok(), "parse failed: {:?}", result);
2241 let program = result.expect("ok");
2242 match &program.statements[0] {
2243 Stmt::If(if_stmt) => {
2244 assert!(if_stmt.else_branch.is_some());
2246 let else_branch = if_stmt.else_branch.as_ref().unwrap();
2247 assert_eq!(else_branch.len(), 1);
2248 assert!(matches!(&else_branch[0], Stmt::If(_)));
2249 }
2250 _ => panic!("expected If"),
2251 }
2252 }
2253
2254 #[test]
2255 fn parse_elif_with_else() {
2256 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
2257 assert!(result.is_ok(), "parse failed: {:?}", result);
2258 let program = result.expect("ok");
2259 match &program.statements[0] {
2260 Stmt::If(outer_if) => {
2261 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
2263 assert_eq!(else_branch.len(), 1);
2264 match &else_branch[0] {
2265 Stmt::If(inner_if) => {
2266 assert!(inner_if.else_branch.is_some());
2268 }
2269 _ => panic!("expected nested If from elif"),
2270 }
2271 }
2272 _ => panic!("expected If"),
2273 }
2274 }
2275
2276 #[test]
2277 fn parse_multiple_elif() {
2278 let result = parse(
2280 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
2281 );
2282 assert!(result.is_ok(), "parse failed: {:?}", result);
2283 }
2284
2285 #[test]
2286 fn parse_for_loop() {
2287 let result = parse("for X in items; do echo; done");
2288 assert!(result.is_ok());
2289 let program = result.expect("ok");
2290 assert!(matches!(&program.statements[0], Stmt::For(_)));
2291 }
2292
2293 #[test]
2294 fn parse_brackets_not_array_literal() {
2295 let result = parse("cmd [1");
2297 let _ = result;
2300 }
2301
2302 #[test]
2303 fn parse_named_arg() {
2304 let result = parse("cmd foo=5");
2308 assert!(result.is_ok());
2309 let program = result.expect("ok");
2310 match &program.statements[0] {
2311 Stmt::Command(cmd) => {
2312 assert_eq!(cmd.args.len(), 1);
2313 assert!(matches!(&cmd.args[0], Arg::WordAssign { .. }));
2314 }
2315 _ => panic!("expected Command"),
2316 }
2317 }
2318
2319 #[test]
2320 fn parse_short_flag() {
2321 let result = parse("ls -l");
2322 assert!(result.is_ok());
2323 let program = result.expect("ok");
2324 match &program.statements[0] {
2325 Stmt::Command(cmd) => {
2326 assert_eq!(cmd.name, "ls");
2327 assert_eq!(cmd.args.len(), 1);
2328 match &cmd.args[0] {
2329 Arg::ShortFlag(name) => assert_eq!(name, "l"),
2330 _ => panic!("expected ShortFlag"),
2331 }
2332 }
2333 _ => panic!("expected Command"),
2334 }
2335 }
2336
2337 #[test]
2338 fn parse_long_flag() {
2339 let result = parse("git push --force");
2340 assert!(result.is_ok());
2341 let program = result.expect("ok");
2342 match &program.statements[0] {
2343 Stmt::Command(cmd) => {
2344 assert_eq!(cmd.name, "git");
2345 assert_eq!(cmd.args.len(), 2);
2346 match &cmd.args[0] {
2347 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
2348 _ => panic!("expected Positional push"),
2349 }
2350 match &cmd.args[1] {
2351 Arg::LongFlag(name) => assert_eq!(name, "force"),
2352 _ => panic!("expected LongFlag"),
2353 }
2354 }
2355 _ => panic!("expected Command"),
2356 }
2357 }
2358
2359 #[test]
2360 fn parse_long_flag_with_value() {
2361 let result = parse(r#"git commit --message="hello""#);
2362 assert!(result.is_ok());
2363 let program = result.expect("ok");
2364 match &program.statements[0] {
2365 Stmt::Command(cmd) => {
2366 assert_eq!(cmd.name, "git");
2367 assert_eq!(cmd.args.len(), 2);
2368 match &cmd.args[1] {
2369 Arg::Named { key, value } => {
2370 assert_eq!(key, "message");
2371 match value {
2372 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
2373 _ => panic!("expected String value"),
2374 }
2375 }
2376 _ => panic!("expected Named from --flag=value"),
2377 }
2378 }
2379 _ => panic!("expected Command"),
2380 }
2381 }
2382
2383 #[test]
2384 fn parse_mixed_flags_and_args() {
2385 let result = parse(r#"git commit -m "message" --amend"#);
2386 assert!(result.is_ok());
2387 let program = result.expect("ok");
2388 match &program.statements[0] {
2389 Stmt::Command(cmd) => {
2390 assert_eq!(cmd.name, "git");
2391 assert_eq!(cmd.args.len(), 4);
2392 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
2394 match &cmd.args[1] {
2396 Arg::ShortFlag(name) => assert_eq!(name, "m"),
2397 _ => panic!("expected ShortFlag -m"),
2398 }
2399 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
2401 match &cmd.args[3] {
2403 Arg::LongFlag(name) => assert_eq!(name, "amend"),
2404 _ => panic!("expected LongFlag --amend"),
2405 }
2406 }
2407 _ => panic!("expected Command"),
2408 }
2409 }
2410
2411 #[test]
2412 fn parse_redirect_stdout() {
2413 let result = parse("cmd > file");
2414 assert!(result.is_ok());
2415 let program = result.expect("ok");
2416 match &program.statements[0] {
2418 Stmt::Pipeline(p) => {
2419 assert_eq!(p.commands.len(), 1);
2420 let cmd = &p.commands[0];
2421 assert_eq!(cmd.redirects.len(), 1);
2422 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
2423 }
2424 _ => panic!("expected Pipeline"),
2425 }
2426 }
2427
2428 #[test]
2429 fn parse_var_ref() {
2430 let result = parse("echo ${VAR}");
2431 assert!(result.is_ok());
2432 let program = result.expect("ok");
2433 match &program.statements[0] {
2434 Stmt::Command(cmd) => {
2435 assert_eq!(cmd.args.len(), 1);
2436 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
2437 }
2438 _ => panic!("expected Command"),
2439 }
2440 }
2441
2442 #[test]
2443 fn parse_multiple_statements() {
2444 let result = parse("a\nb\nc");
2445 assert!(result.is_ok());
2446 let program = result.expect("ok");
2447 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2448 assert_eq!(non_empty.len(), 3);
2449 }
2450
2451 #[test]
2452 fn parse_semicolon_separated() {
2453 let result = parse("a; b; c");
2454 assert!(result.is_ok());
2455 let program = result.expect("ok");
2456 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
2457 assert_eq!(non_empty.len(), 3);
2458 }
2459
2460 #[test]
2461 fn parse_complex_pipeline() {
2462 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
2463 assert!(result.is_ok());
2464 let program = result.expect("ok");
2465 match &program.statements[0] {
2466 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
2467 _ => panic!("expected Pipeline"),
2468 }
2469 }
2470
2471 #[test]
2472 fn parse_json_as_string_arg() {
2473 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
2475 assert!(result.is_ok());
2476 }
2477
2478 #[test]
2479 fn parse_mixed_args() {
2480 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
2481 assert!(result.is_ok());
2482 let program = result.expect("ok");
2483 match &program.statements[0] {
2484 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
2485 _ => panic!("expected Command"),
2486 }
2487 }
2488
2489 #[test]
2490 fn error_unterminated_string() {
2491 let result = parse(r#"echo "hello"#);
2492 assert!(result.is_err());
2493 }
2494
2495 #[test]
2496 fn error_unterminated_var_ref() {
2497 let result = parse("echo ${VAR");
2498 assert!(result.is_err());
2499 }
2500
2501 #[test]
2502 fn error_missing_fi() {
2503 let result = parse("if true; then echo");
2504 assert!(result.is_err());
2505 }
2506
2507 #[test]
2508 fn error_missing_done() {
2509 let result = parse("for X in items; do echo");
2510 assert!(result.is_err());
2511 }
2512
2513 #[test]
2514 fn parse_nested_cmd_subst() {
2515 let result = parse("X=$(echo $(date))").unwrap();
2517 match &result.statements[0] {
2518 Stmt::Assignment(a) => {
2519 assert_eq!(a.name, "X");
2520 match &a.value {
2521 Expr::CommandSubst(outer) => {
2522 assert_eq!(outer.commands[0].name, "echo");
2523 match &outer.commands[0].args[0] {
2525 Arg::Positional(Expr::CommandSubst(inner)) => {
2526 assert_eq!(inner.commands[0].name, "date");
2527 }
2528 other => panic!("expected nested cmd subst, got {:?}", other),
2529 }
2530 }
2531 other => panic!("expected cmd subst, got {:?}", other),
2532 }
2533 }
2534 other => panic!("expected assignment, got {:?}", other),
2535 }
2536 }
2537
2538 #[test]
2539 fn parse_deeply_nested_cmd_subst() {
2540 let result = parse("X=$(a $(b $(c)))").unwrap();
2542 match &result.statements[0] {
2543 Stmt::Assignment(a) => match &a.value {
2544 Expr::CommandSubst(level1) => {
2545 assert_eq!(level1.commands[0].name, "a");
2546 match &level1.commands[0].args[0] {
2547 Arg::Positional(Expr::CommandSubst(level2)) => {
2548 assert_eq!(level2.commands[0].name, "b");
2549 match &level2.commands[0].args[0] {
2550 Arg::Positional(Expr::CommandSubst(level3)) => {
2551 assert_eq!(level3.commands[0].name, "c");
2552 }
2553 other => panic!("expected level3 cmd subst, got {:?}", other),
2554 }
2555 }
2556 other => panic!("expected level2 cmd subst, got {:?}", other),
2557 }
2558 }
2559 other => panic!("expected cmd subst, got {:?}", other),
2560 },
2561 other => panic!("expected assignment, got {:?}", other),
2562 }
2563 }
2564
2565 #[test]
2570 fn value_int_preserved() {
2571 let result = parse("X=42").unwrap();
2572 match &result.statements[0] {
2573 Stmt::Assignment(a) => {
2574 assert_eq!(a.name, "X");
2575 match &a.value {
2576 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2577 other => panic!("expected int literal, got {:?}", other),
2578 }
2579 }
2580 other => panic!("expected assignment, got {:?}", other),
2581 }
2582 }
2583
2584 #[test]
2585 fn value_negative_int_preserved() {
2586 let result = parse("X=-99").unwrap();
2587 match &result.statements[0] {
2588 Stmt::Assignment(a) => match &a.value {
2589 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
2590 other => panic!("expected int, got {:?}", other),
2591 },
2592 other => panic!("expected assignment, got {:?}", other),
2593 }
2594 }
2595
2596 #[test]
2597 fn value_float_preserved() {
2598 let result = parse("PI=3.14").unwrap();
2599 match &result.statements[0] {
2600 Stmt::Assignment(a) => match &a.value {
2601 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
2602 other => panic!("expected float, got {:?}", other),
2603 },
2604 other => panic!("expected assignment, got {:?}", other),
2605 }
2606 }
2607
2608 #[test]
2609 fn value_string_preserved() {
2610 let result = parse(r#"echo "hello world""#).unwrap();
2611 match &result.statements[0] {
2612 Stmt::Command(cmd) => {
2613 assert_eq!(cmd.name, "echo");
2614 match &cmd.args[0] {
2615 Arg::Positional(Expr::Literal(Value::String(s))) => {
2616 assert_eq!(s, "hello world");
2617 }
2618 other => panic!("expected string arg, got {:?}", other),
2619 }
2620 }
2621 other => panic!("expected command, got {:?}", other),
2622 }
2623 }
2624
2625 #[test]
2626 fn value_string_with_escapes_preserved() {
2627 let result = parse(r#"echo "line1\nline2""#).unwrap();
2628 match &result.statements[0] {
2629 Stmt::Command(cmd) => match &cmd.args[0] {
2630 Arg::Positional(Expr::Literal(Value::String(s))) => {
2631 assert_eq!(s, "line1\nline2");
2632 }
2633 other => panic!("expected string, got {:?}", other),
2634 },
2635 other => panic!("expected command, got {:?}", other),
2636 }
2637 }
2638
2639 #[test]
2640 fn value_command_name_preserved() {
2641 let result = parse("my-command").unwrap();
2642 match &result.statements[0] {
2643 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
2644 other => panic!("expected command, got {:?}", other),
2645 }
2646 }
2647
2648 #[test]
2649 fn value_assignment_name_preserved() {
2650 let result = parse("MY_VAR=1").unwrap();
2651 match &result.statements[0] {
2652 Stmt::Assignment(a) => assert_eq!(a.name, "MY_VAR"),
2653 other => panic!("expected assignment, got {:?}", other),
2654 }
2655 }
2656
2657 #[test]
2658 fn value_for_variable_preserved() {
2659 let result = parse("for ITEM in items; do echo; done").unwrap();
2660 match &result.statements[0] {
2661 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
2662 other => panic!("expected for, got {:?}", other),
2663 }
2664 }
2665
2666 #[test]
2667 fn value_varref_name_preserved() {
2668 let result = parse("echo ${MESSAGE}").unwrap();
2669 match &result.statements[0] {
2670 Stmt::Command(cmd) => match &cmd.args[0] {
2671 Arg::Positional(Expr::VarRef(path)) => {
2672 assert_eq!(path.segments.len(), 1);
2673 let VarSegment::Field(name) = &path.segments[0];
2674 assert_eq!(name, "MESSAGE");
2675 }
2676 other => panic!("expected varref, got {:?}", other),
2677 },
2678 other => panic!("expected command, got {:?}", other),
2679 }
2680 }
2681
2682 #[test]
2683 fn value_varref_field_access_preserved() {
2684 let result = parse("echo ${RESULT.data}").unwrap();
2685 match &result.statements[0] {
2686 Stmt::Command(cmd) => match &cmd.args[0] {
2687 Arg::Positional(Expr::VarRef(path)) => {
2688 assert_eq!(path.segments.len(), 2);
2689 let VarSegment::Field(a) = &path.segments[0];
2690 let VarSegment::Field(b) = &path.segments[1];
2691 assert_eq!(a, "RESULT");
2692 assert_eq!(b, "data");
2693 }
2694 other => panic!("expected varref, got {:?}", other),
2695 },
2696 other => panic!("expected command, got {:?}", other),
2697 }
2698 }
2699
2700 #[test]
2701 fn value_varref_index_ignored() {
2702 let result = parse("echo ${ITEMS[0]}").unwrap();
2704 match &result.statements[0] {
2705 Stmt::Command(cmd) => match &cmd.args[0] {
2706 Arg::Positional(Expr::VarRef(path)) => {
2707 assert_eq!(path.segments.len(), 1);
2709 let VarSegment::Field(name) = &path.segments[0];
2710 assert_eq!(name, "ITEMS");
2711 }
2712 other => panic!("expected varref, got {:?}", other),
2713 },
2714 other => panic!("expected command, got {:?}", other),
2715 }
2716 }
2717
2718 #[test]
2719 fn value_named_arg_preserved() {
2720 let result = parse("cmd count=42").unwrap();
2724 match &result.statements[0] {
2725 Stmt::Command(cmd) => {
2726 assert_eq!(cmd.name, "cmd");
2727 match &cmd.args[0] {
2728 Arg::WordAssign { key, value } => {
2729 assert_eq!(key, "count");
2730 match value {
2731 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2732 other => panic!("expected int, got {:?}", other),
2733 }
2734 }
2735 other => panic!("expected WordAssign arg, got {:?}", other),
2736 }
2737 }
2738 other => panic!("expected command, got {:?}", other),
2739 }
2740 }
2741
2742 #[test]
2743 fn value_function_def_name_preserved() {
2744 let result = parse("greet() { echo }").unwrap();
2745 match &result.statements[0] {
2746 Stmt::ToolDef(t) => {
2747 assert_eq!(t.name, "greet");
2748 assert!(t.params.is_empty());
2749 }
2750 other => panic!("expected function def, got {:?}", other),
2751 }
2752 }
2753
2754 #[test]
2759 fn parse_comparison_equals() {
2760 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
2762 match &result.statements[0] {
2763 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2764 Expr::Test(test) => match test.as_ref() {
2765 TestExpr::Comparison { left, op, right } => {
2766 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2767 assert_eq!(*op, TestCmpOp::Eq);
2768 match right.as_ref() {
2769 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
2770 other => panic!("expected int, got {:?}", other),
2771 }
2772 }
2773 other => panic!("expected comparison, got {:?}", other),
2774 },
2775 other => panic!("expected test expr, got {:?}", other),
2776 },
2777 other => panic!("expected if, got {:?}", other),
2778 }
2779 }
2780
2781 #[test]
2782 fn parse_comparison_not_equals() {
2783 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
2784 match &result.statements[0] {
2785 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2786 Expr::Test(test) => match test.as_ref() {
2787 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
2788 other => panic!("expected comparison, got {:?}", other),
2789 },
2790 other => panic!("expected test expr, got {:?}", other),
2791 },
2792 other => panic!("expected if, got {:?}", other),
2793 }
2794 }
2795
2796 #[test]
2797 fn parse_comparison_less_than() {
2798 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
2799 match &result.statements[0] {
2800 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2801 Expr::Test(test) => match test.as_ref() {
2802 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLt),
2803 other => panic!("expected comparison, got {:?}", other),
2804 },
2805 other => panic!("expected test expr, got {:?}", other),
2806 },
2807 other => panic!("expected if, got {:?}", other),
2808 }
2809 }
2810
2811 #[test]
2812 fn parse_comparison_greater_than() {
2813 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
2814 match &result.statements[0] {
2815 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2816 Expr::Test(test) => match test.as_ref() {
2817 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGt),
2818 other => panic!("expected comparison, got {:?}", other),
2819 },
2820 other => panic!("expected test expr, got {:?}", other),
2821 },
2822 other => panic!("expected if, got {:?}", other),
2823 }
2824 }
2825
2826 #[test]
2827 fn parse_comparison_less_equal() {
2828 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
2829 match &result.statements[0] {
2830 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2831 Expr::Test(test) => match test.as_ref() {
2832 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumLtEq),
2833 other => panic!("expected comparison, got {:?}", other),
2834 },
2835 other => panic!("expected test expr, got {:?}", other),
2836 },
2837 other => panic!("expected if, got {:?}", other),
2838 }
2839 }
2840
2841 #[test]
2842 fn parse_comparison_greater_equal() {
2843 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
2844 match &result.statements[0] {
2845 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2846 Expr::Test(test) => match test.as_ref() {
2847 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NumGtEq),
2848 other => panic!("expected comparison, got {:?}", other),
2849 },
2850 other => panic!("expected test expr, got {:?}", other),
2851 },
2852 other => panic!("expected if, got {:?}", other),
2853 }
2854 }
2855
2856 #[test]
2857 fn parse_regex_match() {
2858 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
2859 match &result.statements[0] {
2860 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2861 Expr::Test(test) => match test.as_ref() {
2862 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
2863 other => panic!("expected comparison, got {:?}", other),
2864 },
2865 other => panic!("expected test expr, got {:?}", other),
2866 },
2867 other => panic!("expected if, got {:?}", other),
2868 }
2869 }
2870
2871 #[test]
2872 fn parse_regex_not_match() {
2873 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
2874 match &result.statements[0] {
2875 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2876 Expr::Test(test) => match test.as_ref() {
2877 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
2878 other => panic!("expected comparison, got {:?}", other),
2879 },
2880 other => panic!("expected test expr, got {:?}", other),
2881 },
2882 other => panic!("expected if, got {:?}", other),
2883 }
2884 }
2885
2886 #[test]
2887 fn parse_string_interpolation() {
2888 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
2889 match &result.statements[0] {
2890 Stmt::Command(cmd) => match &cmd.args[0] {
2891 Arg::Positional(Expr::Interpolated(parts)) => {
2892 assert_eq!(parts.len(), 3);
2893 match &parts[0] {
2894 StringPart::Literal(s) => assert_eq!(s, "Hello "),
2895 other => panic!("expected literal, got {:?}", other),
2896 }
2897 match &parts[1] {
2898 StringPart::Var(path) => {
2899 assert_eq!(path.segments.len(), 1);
2900 let VarSegment::Field(name) = &path.segments[0];
2901 assert_eq!(name, "NAME");
2902 }
2903 other => panic!("expected var, got {:?}", other),
2904 }
2905 match &parts[2] {
2906 StringPart::Literal(s) => assert_eq!(s, "!"),
2907 other => panic!("expected literal, got {:?}", other),
2908 }
2909 }
2910 other => panic!("expected interpolated, got {:?}", other),
2911 },
2912 other => panic!("expected command, got {:?}", other),
2913 }
2914 }
2915
2916 #[test]
2917 fn parse_string_interpolation_multiple_vars() {
2918 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
2919 match &result.statements[0] {
2920 Stmt::Command(cmd) => match &cmd.args[0] {
2921 Arg::Positional(Expr::Interpolated(parts)) => {
2922 assert_eq!(parts.len(), 3);
2924 assert!(matches!(&parts[0], StringPart::Var(_)));
2925 assert!(matches!(&parts[1], StringPart::Literal(_)));
2926 assert!(matches!(&parts[2], StringPart::Var(_)));
2927 }
2928 other => panic!("expected interpolated, got {:?}", other),
2929 },
2930 other => panic!("expected command, got {:?}", other),
2931 }
2932 }
2933
2934 #[test]
2935 fn parse_empty_function_body() {
2936 let result = parse("empty() { }").unwrap();
2937 match &result.statements[0] {
2938 Stmt::ToolDef(t) => {
2939 assert_eq!(t.name, "empty");
2940 assert!(t.params.is_empty());
2941 assert!(t.body.is_empty());
2942 }
2943 other => panic!("expected function def, got {:?}", other),
2944 }
2945 }
2946
2947 #[test]
2948 fn parse_bash_style_function() {
2949 let result = parse("function greet { echo hello }").unwrap();
2950 match &result.statements[0] {
2951 Stmt::ToolDef(t) => {
2952 assert_eq!(t.name, "greet");
2953 assert!(t.params.is_empty());
2954 assert_eq!(t.body.len(), 1);
2955 }
2956 other => panic!("expected function def, got {:?}", other),
2957 }
2958 }
2959
2960 #[test]
2961 fn parse_comparison_string_values() {
2962 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
2963 match &result.statements[0] {
2964 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2965 Expr::Test(test) => match test.as_ref() {
2966 TestExpr::Comparison { left, op, right } => {
2967 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2968 assert_eq!(*op, TestCmpOp::Eq);
2969 match right.as_ref() {
2970 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
2971 other => panic!("expected string, got {:?}", other),
2972 }
2973 }
2974 other => panic!("expected comparison, got {:?}", other),
2975 },
2976 other => panic!("expected test expr, got {:?}", other),
2977 },
2978 other => panic!("expected if, got {:?}", other),
2979 }
2980 }
2981
2982 #[test]
2987 fn parse_cmd_subst_simple() {
2988 let result = parse("X=$(echo)").unwrap();
2989 match &result.statements[0] {
2990 Stmt::Assignment(a) => {
2991 assert_eq!(a.name, "X");
2992 match &a.value {
2993 Expr::CommandSubst(pipeline) => {
2994 assert_eq!(pipeline.commands.len(), 1);
2995 assert_eq!(pipeline.commands[0].name, "echo");
2996 }
2997 other => panic!("expected command subst, got {:?}", other),
2998 }
2999 }
3000 other => panic!("expected assignment, got {:?}", other),
3001 }
3002 }
3003
3004 #[test]
3005 fn parse_cmd_subst_with_args() {
3006 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
3007 match &result.statements[0] {
3008 Stmt::Assignment(a) => match &a.value {
3009 Expr::CommandSubst(pipeline) => {
3010 assert_eq!(pipeline.commands[0].name, "fetch");
3011 assert_eq!(pipeline.commands[0].args.len(), 1);
3012 match &pipeline.commands[0].args[0] {
3013 Arg::WordAssign { key, .. } => assert_eq!(key, "url"),
3014 other => panic!("expected WordAssign arg, got {:?}", other),
3015 }
3016 }
3017 other => panic!("expected command subst, got {:?}", other),
3018 },
3019 other => panic!("expected assignment, got {:?}", other),
3020 }
3021 }
3022
3023 #[test]
3024 fn parse_cmd_subst_pipeline() {
3025 let result = parse("X=$(cat file | grep pattern)").unwrap();
3026 match &result.statements[0] {
3027 Stmt::Assignment(a) => match &a.value {
3028 Expr::CommandSubst(pipeline) => {
3029 assert_eq!(pipeline.commands.len(), 2);
3030 assert_eq!(pipeline.commands[0].name, "cat");
3031 assert_eq!(pipeline.commands[1].name, "grep");
3032 }
3033 other => panic!("expected command subst, got {:?}", other),
3034 },
3035 other => panic!("expected assignment, got {:?}", other),
3036 }
3037 }
3038
3039 #[test]
3040 fn parse_cmd_subst_in_condition() {
3041 let result = parse("if kaish-validate; then echo; fi").unwrap();
3043 match &result.statements[0] {
3044 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3045 Expr::Command(cmd) => {
3046 assert_eq!(cmd.name, "kaish-validate");
3047 }
3048 other => panic!("expected command, got {:?}", other),
3049 },
3050 other => panic!("expected if, got {:?}", other),
3051 }
3052 }
3053
3054 #[test]
3055 fn parse_cmd_subst_in_command_arg() {
3056 let result = parse("echo $(whoami)").unwrap();
3057 match &result.statements[0] {
3058 Stmt::Command(cmd) => {
3059 assert_eq!(cmd.name, "echo");
3060 match &cmd.args[0] {
3061 Arg::Positional(Expr::CommandSubst(pipeline)) => {
3062 assert_eq!(pipeline.commands[0].name, "whoami");
3063 }
3064 other => panic!("expected command subst, got {:?}", other),
3065 }
3066 }
3067 other => panic!("expected command, got {:?}", other),
3068 }
3069 }
3070
3071 #[test]
3076 fn parse_condition_and() {
3077 let result = parse("if check-a && check-b; then echo; fi").unwrap();
3079 match &result.statements[0] {
3080 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3081 Expr::BinaryOp { left, op, right } => {
3082 assert_eq!(*op, BinaryOp::And);
3083 assert!(matches!(left.as_ref(), Expr::Command(_)));
3084 assert!(matches!(right.as_ref(), Expr::Command(_)));
3085 }
3086 other => panic!("expected binary op, got {:?}", other),
3087 },
3088 other => panic!("expected if, got {:?}", other),
3089 }
3090 }
3091
3092 #[test]
3093 fn parse_condition_or() {
3094 let result = parse("if try-a || try-b; then echo; fi").unwrap();
3095 match &result.statements[0] {
3096 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3097 Expr::BinaryOp { left, op, right } => {
3098 assert_eq!(*op, BinaryOp::Or);
3099 assert!(matches!(left.as_ref(), Expr::Command(_)));
3100 assert!(matches!(right.as_ref(), Expr::Command(_)));
3101 }
3102 other => panic!("expected binary op, got {:?}", other),
3103 },
3104 other => panic!("expected if, got {:?}", other),
3105 }
3106 }
3107
3108 #[test]
3109 fn parse_condition_and_or_precedence() {
3110 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
3112 match &result.statements[0] {
3113 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3114 Expr::BinaryOp { left, op, right } => {
3115 assert_eq!(*op, BinaryOp::Or);
3117 match left.as_ref() {
3119 Expr::BinaryOp { op: inner_op, .. } => {
3120 assert_eq!(*inner_op, BinaryOp::And);
3121 }
3122 other => panic!("expected binary op (&&), got {:?}", other),
3123 }
3124 assert!(matches!(right.as_ref(), Expr::Command(_)));
3126 }
3127 other => panic!("expected binary op, got {:?}", other),
3128 },
3129 other => panic!("expected if, got {:?}", other),
3130 }
3131 }
3132
3133 #[test]
3134 fn parse_condition_multiple_and() {
3135 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
3136 match &result.statements[0] {
3137 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3138 Expr::BinaryOp { left, op, .. } => {
3139 assert_eq!(*op, BinaryOp::And);
3140 match left.as_ref() {
3142 Expr::BinaryOp { op: inner_op, .. } => {
3143 assert_eq!(*inner_op, BinaryOp::And);
3144 }
3145 other => panic!("expected binary op, got {:?}", other),
3146 }
3147 }
3148 other => panic!("expected binary op, got {:?}", other),
3149 },
3150 other => panic!("expected if, got {:?}", other),
3151 }
3152 }
3153
3154 #[test]
3155 fn parse_condition_mixed_comparison_and_logical() {
3156 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
3158 match &result.statements[0] {
3159 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
3160 Expr::BinaryOp { left, op, right } => {
3161 assert_eq!(*op, BinaryOp::And);
3162 match left.as_ref() {
3164 Expr::Test(test) => match test.as_ref() {
3165 TestExpr::Comparison { op: left_op, .. } => {
3166 assert_eq!(*left_op, TestCmpOp::Eq);
3167 }
3168 other => panic!("expected comparison, got {:?}", other),
3169 },
3170 other => panic!("expected test, got {:?}", other),
3171 }
3172 match right.as_ref() {
3174 Expr::Test(test) => match test.as_ref() {
3175 TestExpr::Comparison { op: right_op, .. } => {
3176 assert_eq!(*right_op, TestCmpOp::NumGt);
3177 }
3178 other => panic!("expected comparison, got {:?}", other),
3179 },
3180 other => panic!("expected test, got {:?}", other),
3181 }
3182 }
3183 other => panic!("expected binary op, got {:?}", other),
3184 },
3185 other => panic!("expected if, got {:?}", other),
3186 }
3187 }
3188
3189 #[test]
3195 fn script_level1_linear() {
3196 let script = r#"
3197NAME="kaish"
3198VERSION=1
3199TIMEOUT=30
3200ITEMS="alpha beta gamma"
3201
3202echo "Starting ${NAME} v${VERSION}"
3203cat "README.md" | grep pattern="install" | head count=5
3204fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
3205echo "Items: ${ITEMS}"
3206"#;
3207 let result = parse(script).unwrap();
3208 let stmts: Vec<_> = result.statements.iter()
3209 .filter(|s| !matches!(s, Stmt::Empty))
3210 .collect();
3211
3212 assert_eq!(stmts.len(), 8);
3213 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(_))); }
3222
3223 #[test]
3225 fn script_level2_branching() {
3226 let script = r#"
3227RESULT=$(kaish-validate "input.json")
3228
3229if [[ ${RESULT.ok} == true ]]; then
3230 echo "Validation passed"
3231 process "input.json" > "output.json"
3232else
3233 echo "Validation failed: ${RESULT.err}"
3234fi
3235
3236if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
3237 echo "Count in valid range"
3238fi
3239
3240if check-network || check-cache; then
3241 fetch url=${URL}
3242fi
3243"#;
3244 let result = parse(script).unwrap();
3245 let stmts: Vec<_> = result.statements.iter()
3246 .filter(|s| !matches!(s, Stmt::Empty))
3247 .collect();
3248
3249 assert_eq!(stmts.len(), 4);
3250
3251 match stmts[0] {
3253 Stmt::Assignment(a) => {
3254 assert_eq!(a.name, "RESULT");
3255 assert!(matches!(&a.value, Expr::CommandSubst(_)));
3256 }
3257 other => panic!("expected assignment, got {:?}", other),
3258 }
3259
3260 match stmts[1] {
3262 Stmt::If(if_stmt) => {
3263 assert_eq!(if_stmt.then_branch.len(), 2);
3264 assert!(if_stmt.else_branch.is_some());
3265 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
3266 }
3267 other => panic!("expected if, got {:?}", other),
3268 }
3269
3270 match stmts[2] {
3272 Stmt::If(if_stmt) => {
3273 match if_stmt.condition.as_ref() {
3274 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3275 other => panic!("expected && condition, got {:?}", other),
3276 }
3277 }
3278 other => panic!("expected if, got {:?}", other),
3279 }
3280
3281 match stmts[3] {
3283 Stmt::If(if_stmt) => {
3284 match if_stmt.condition.as_ref() {
3285 Expr::BinaryOp { op, left, right } => {
3286 assert_eq!(*op, BinaryOp::Or);
3287 assert!(matches!(left.as_ref(), Expr::Command(_)));
3288 assert!(matches!(right.as_ref(), Expr::Command(_)));
3289 }
3290 other => panic!("expected || condition, got {:?}", other),
3291 }
3292 }
3293 other => panic!("expected if, got {:?}", other),
3294 }
3295 }
3296
3297 #[test]
3299 fn script_level3_loops_and_functions() {
3300 let script = r#"
3301greet() {
3302 echo "Hello, $1!"
3303}
3304
3305fetch_all() {
3306 for URL in $@; do
3307 fetch url=${URL}
3308 done
3309}
3310
3311USERS="alice bob charlie"
3312
3313for USER in ${USERS}; do
3314 greet ${USER}
3315 if [[ ${USER} == "bob" ]]; then
3316 echo "Found Bob!"
3317 fi
3318done
3319
3320long-running-task &
3321"#;
3322 let result = parse(script).unwrap();
3323 let stmts: Vec<_> = result.statements.iter()
3324 .filter(|s| !matches!(s, Stmt::Empty))
3325 .collect();
3326
3327 assert_eq!(stmts.len(), 5);
3328
3329 match stmts[0] {
3331 Stmt::ToolDef(t) => {
3332 assert_eq!(t.name, "greet");
3333 assert!(t.params.is_empty());
3334 }
3335 other => panic!("expected function def, got {:?}", other),
3336 }
3337
3338 match stmts[1] {
3340 Stmt::ToolDef(t) => {
3341 assert_eq!(t.name, "fetch_all");
3342 assert_eq!(t.body.len(), 1);
3343 assert!(matches!(&t.body[0], Stmt::For(_)));
3344 }
3345 other => panic!("expected function def, got {:?}", other),
3346 }
3347
3348 assert!(matches!(stmts[2], Stmt::Assignment(_)));
3350
3351 match stmts[3] {
3353 Stmt::For(f) => {
3354 assert_eq!(f.variable, "USER");
3355 assert_eq!(f.body.len(), 2);
3356 assert!(matches!(&f.body[0], Stmt::Command(_)));
3357 assert!(matches!(&f.body[1], Stmt::If(_)));
3358 }
3359 other => panic!("expected for loop, got {:?}", other),
3360 }
3361
3362 match stmts[4] {
3364 Stmt::Pipeline(p) => {
3365 assert!(p.background);
3366 assert_eq!(p.commands[0].name, "long-running-task");
3367 }
3368 other => panic!("expected pipeline (background), got {:?}", other),
3369 }
3370 }
3371
3372 #[test]
3374 fn script_level4_complex_nesting() {
3375 let script = r#"
3376RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
3377
3378if ping host=${HOST} && [[ ${RESULT} == true ]]; then
3379 for SERVER in "prod-1 prod-2"; do
3380 deploy target=${SERVER} port=8080
3381 if [[ $? -ne 0 ]]; then
3382 notify channel="ops" message="Deploy failed"
3383 fi
3384 done
3385fi
3386"#;
3387 let result = parse(script).unwrap();
3388 let stmts: Vec<_> = result.statements.iter()
3389 .filter(|s| !matches!(s, Stmt::Empty))
3390 .collect();
3391
3392 assert_eq!(stmts.len(), 2);
3393
3394 match stmts[0] {
3396 Stmt::Assignment(a) => {
3397 assert_eq!(a.name, "RESULT");
3398 match &a.value {
3399 Expr::CommandSubst(pipeline) => {
3400 assert_eq!(pipeline.commands.len(), 3);
3401 }
3402 other => panic!("expected command subst, got {:?}", other),
3403 }
3404 }
3405 other => panic!("expected assignment, got {:?}", other),
3406 }
3407
3408 match stmts[1] {
3410 Stmt::If(if_stmt) => {
3411 match if_stmt.condition.as_ref() {
3412 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
3413 other => panic!("expected && condition, got {:?}", other),
3414 }
3415 assert_eq!(if_stmt.then_branch.len(), 1);
3416 match &if_stmt.then_branch[0] {
3417 Stmt::For(f) => {
3418 assert_eq!(f.body.len(), 2);
3419 assert!(matches!(&f.body[1], Stmt::If(_)));
3420 }
3421 other => panic!("expected for in if body, got {:?}", other),
3422 }
3423 }
3424 other => panic!("expected if, got {:?}", other),
3425 }
3426 }
3427
3428 #[test]
3430 fn script_level5_edge_cases() {
3431 let script = r#"
3432echo ""
3433echo "quotes: \"nested\" here"
3434echo "escapes: \n\t\r\\"
3435echo "unicode: \u2764"
3436
3437X=-99999
3438Y=3.14159265358979
3439Z=-0.001
3440
3441cmd a=1 b="two" c=true d=false e=null
3442
3443if true; then
3444 if false; then
3445 echo "inner"
3446 else
3447 echo "else"
3448 fi
3449fi
3450
3451for I in "a b c"; do
3452 echo ${I}
3453done
3454
3455no_params() {
3456 echo "no params"
3457}
3458
3459function all_args {
3460 echo "args: $@"
3461}
3462
3463a | b | c | d | e &
3464cmd 2> "errors.log"
3465cmd &> "all.log"
3466cmd >> "append.log"
3467cmd < "input.txt"
3468"#;
3469 let result = parse(script).unwrap();
3470 let stmts: Vec<_> = result.statements.iter()
3471 .filter(|s| !matches!(s, Stmt::Empty))
3472 .collect();
3473
3474 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
3476
3477 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
3479 assert!(bg_stmt.is_some(), "expected background pipeline");
3480
3481 match bg_stmt.unwrap() {
3482 Stmt::Pipeline(p) => {
3483 assert_eq!(p.commands.len(), 5);
3484 assert!(p.background);
3485 }
3486 _ => unreachable!(),
3487 }
3488 }
3489
3490 #[test]
3495 fn parse_keyword_as_variable_rejected() {
3496 let result = parse(r#"if="value""#);
3499 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
3500
3501 let result = parse("while=true");
3502 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
3503
3504 let result = parse(r#"then="next""#);
3505 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
3506 }
3507
3508 #[test]
3509 fn parse_set_command_with_flag() {
3510 let result = parse("set -e");
3511 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
3512 let program = result.unwrap();
3513 match &program.statements[0] {
3514 Stmt::Command(cmd) => {
3515 assert_eq!(cmd.name, "set");
3516 assert_eq!(cmd.args.len(), 1);
3517 match &cmd.args[0] {
3518 Arg::ShortFlag(f) => assert_eq!(f, "e"),
3519 other => panic!("expected ShortFlag, got {:?}", other),
3520 }
3521 }
3522 other => panic!("expected Command, got {:?}", other),
3523 }
3524 }
3525
3526 #[test]
3527 fn parse_set_command_no_args() {
3528 let result = parse("set");
3529 assert!(result.is_ok(), "failed to parse set: {:?}", result);
3530 let program = result.unwrap();
3531 match &program.statements[0] {
3532 Stmt::Command(cmd) => {
3533 assert_eq!(cmd.name, "set");
3534 assert_eq!(cmd.args.len(), 0);
3535 }
3536 other => panic!("expected Command, got {:?}", other),
3537 }
3538 }
3539
3540 #[test]
3541 fn parse_set_assignment_vs_command() {
3542 let result = parse("X=5");
3544 assert!(result.is_ok());
3545 let program = result.unwrap();
3546 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
3547
3548 let result = parse("set -e");
3550 assert!(result.is_ok());
3551 let program = result.unwrap();
3552 assert!(matches!(&program.statements[0], Stmt::Command(_)));
3553 }
3554
3555 #[test]
3556 fn parse_true_as_command() {
3557 let result = parse("true");
3558 assert!(result.is_ok());
3559 let program = result.unwrap();
3560 match &program.statements[0] {
3561 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
3562 other => panic!("expected Command(true), got {:?}", other),
3563 }
3564 }
3565
3566 #[test]
3567 fn parse_false_as_command() {
3568 let result = parse("false");
3569 assert!(result.is_ok());
3570 let program = result.unwrap();
3571 match &program.statements[0] {
3572 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
3573 other => panic!("expected Command(false), got {:?}", other),
3574 }
3575 }
3576
3577 #[test]
3578 fn parse_dot_as_source_alias() {
3579 let result = parse(". script.kai");
3580 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
3581 let program = result.unwrap();
3582 match &program.statements[0] {
3583 Stmt::Command(cmd) => {
3584 assert_eq!(cmd.name, ".");
3585 assert_eq!(cmd.args.len(), 1);
3586 }
3587 other => panic!("expected Command(.), got {:?}", other),
3588 }
3589 }
3590
3591 #[test]
3592 fn parse_source_command() {
3593 let result = parse("source utils.kai");
3594 assert!(result.is_ok(), "failed to parse source: {:?}", result);
3595 let program = result.unwrap();
3596 match &program.statements[0] {
3597 Stmt::Command(cmd) => {
3598 assert_eq!(cmd.name, "source");
3599 assert_eq!(cmd.args.len(), 1);
3600 }
3601 other => panic!("expected Command(source), got {:?}", other),
3602 }
3603 }
3604
3605 #[test]
3606 fn parse_test_expr_file_test() {
3607 let result = parse(r#"[[ -f "/path/file" ]]"#);
3609 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
3610 }
3611
3612 #[test]
3613 fn parse_test_expr_comparison() {
3614 let result = parse(r#"[[ $X == "value" ]]"#);
3615 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
3616 }
3617
3618 #[test]
3619 fn parse_test_expr_single_eq() {
3620 let result = parse(r#"[[ $X = "value" ]]"#);
3622 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
3623 let program = result.unwrap();
3624 match &program.statements[0] {
3625 Stmt::Test(TestExpr::Comparison { op, .. }) => {
3626 assert_eq!(op, &TestCmpOp::Eq);
3627 }
3628 other => panic!("expected Test(Comparison), got {:?}", other),
3629 }
3630 }
3631
3632 #[test]
3633 fn parse_while_loop() {
3634 let result = parse("while true; do echo; done");
3635 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
3636 let program = result.unwrap();
3637 assert!(matches!(&program.statements[0], Stmt::While(_)));
3638 }
3639
3640 #[test]
3641 fn parse_break_with_level() {
3642 let result = parse("break 2");
3643 assert!(result.is_ok());
3644 let program = result.unwrap();
3645 match &program.statements[0] {
3646 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
3647 other => panic!("expected Break(2), got {:?}", other),
3648 }
3649 }
3650
3651 #[test]
3652 fn parse_continue_with_level() {
3653 let result = parse("continue 3");
3654 assert!(result.is_ok());
3655 let program = result.unwrap();
3656 match &program.statements[0] {
3657 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
3658 other => panic!("expected Continue(3), got {:?}", other),
3659 }
3660 }
3661
3662 #[test]
3663 fn parse_exit_with_code() {
3664 let result = parse("exit 1");
3665 assert!(result.is_ok());
3666 let program = result.unwrap();
3667 match &program.statements[0] {
3668 Stmt::Exit(Some(expr)) => {
3669 match expr.as_ref() {
3670 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
3671 other => panic!("expected Int(1), got {:?}", other),
3672 }
3673 }
3674 other => panic!("expected Exit(1), got {:?}", other),
3675 }
3676 }
3677
3678 #[test]
3685 fn spanned_literal_only_records_byte_range() {
3686 let parts = parse_interpolated_string_spanned("hello world", 100);
3687 assert_eq!(parts.len(), 1);
3688 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello world"));
3689 assert_eq!(parts[0].offset, 100, "base_offset must propagate to literals");
3690 assert_eq!(parts[0].len, 11);
3691 }
3692
3693 #[test]
3694 fn spanned_braced_var_at_zero() {
3695 let parts = parse_interpolated_string_spanned("${X}", 50);
3696 assert_eq!(parts.len(), 1);
3697 assert!(matches!(&parts[0].part, StringPart::Var(_)));
3698 assert_eq!(parts[0].offset, 50);
3699 assert_eq!(parts[0].len, 4); }
3701
3702 #[test]
3703 fn spanned_simple_var_then_literal() {
3704 let parts = parse_interpolated_string_spanned("$X end", 10);
3705 assert_eq!(parts.len(), 2);
3706 assert!(matches!(&parts[0].part, StringPart::Var(_)));
3707 assert_eq!(parts[0].offset, 10);
3708 assert_eq!(parts[0].len, 2); assert!(matches!(&parts[1].part, StringPart::Literal(s) if s == " end"));
3710 assert_eq!(parts[1].offset, 12);
3711 assert_eq!(parts[1].len, 4);
3712 }
3713
3714 #[test]
3715 fn spanned_mixed_literal_var_literal() {
3716 let parts = parse_interpolated_string_spanned("hi ${X} bye", 0);
3717 assert_eq!(parts.len(), 3);
3718 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hi "));
3720 assert_eq!(parts[0].offset, 0);
3721 assert_eq!(parts[0].len, 3);
3722 assert!(matches!(&parts[1].part, StringPart::Var(_)));
3724 assert_eq!(parts[1].offset, 3);
3725 assert_eq!(parts[1].len, 4);
3726 assert!(matches!(&parts[2].part, StringPart::Literal(s) if s == " bye"));
3728 assert_eq!(parts[2].offset, 7);
3729 assert_eq!(parts[2].len, 4);
3730 }
3731
3732 #[test]
3733 fn spanned_positional_param() {
3734 let parts = parse_interpolated_string_spanned("$1 done", 0);
3735 assert_eq!(parts.len(), 2);
3736 assert!(matches!(&parts[0].part, StringPart::Positional(1)));
3737 assert_eq!(parts[0].offset, 0);
3738 assert_eq!(parts[0].len, 2); }
3740
3741 #[test]
3742 fn spanned_special_dollar_dollar() {
3743 let parts = parse_interpolated_string_spanned("$$", 5);
3744 assert_eq!(parts.len(), 1);
3745 assert!(matches!(&parts[0].part, StringPart::CurrentPid));
3746 assert_eq!(parts[0].offset, 5);
3747 assert_eq!(parts[0].len, 2);
3748 }
3749
3750 #[test]
3751 fn spanned_arithmetic_marker_recognised() {
3752 let parts = parse_interpolated_string_spanned("${__ARITH:1+2__}", 0);
3756 assert_eq!(parts.len(), 1);
3757 assert!(matches!(&parts[0].part, StringPart::Arithmetic(e) if e == "1+2"));
3758 }
3759
3760 #[test]
3761 fn spanned_default_separator_yields_var_with_default() {
3762 let parts = parse_interpolated_string_spanned("${X:-fallback}", 0);
3763 assert_eq!(parts.len(), 1);
3764 assert!(matches!(&parts[0].part, StringPart::VarWithDefault { .. }));
3765 assert_eq!(parts[0].offset, 0);
3766 assert_eq!(parts[0].len, 14); }
3768
3769 #[test]
3770 fn spanned_no_dollar_runs_one_literal() {
3771 let parts = parse_interpolated_string_spanned("plain text only", 7);
3772 assert_eq!(parts.len(), 1);
3773 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "plain text only"));
3774 assert_eq!(parts[0].offset, 7);
3775 assert_eq!(parts[0].len, 15);
3776 }
3777
3778 #[test]
3779 fn spanned_matches_unspanned_part_count() {
3780 let cases = [
3783 "hello",
3784 "$X",
3785 "${X}",
3786 "${X:-d}",
3787 "hi $A and $B",
3788 "$0 $1 $2",
3789 "$$ $? $#",
3790 ];
3791 for s in &cases {
3792 let unspanned = parse_interpolated_string(s);
3793 let spanned = parse_interpolated_string_spanned(s, 0);
3794 assert_eq!(
3795 unspanned.len(),
3796 spanned.len(),
3797 "part count differs for {:?}",
3798 s
3799 );
3800 }
3801 }
3802
3803 #[test]
3804 fn spanned_multibyte_utf8_before_var_uses_byte_offsets() {
3805 let parts = parse_interpolated_string_spanned("🚀 ${X}", 0);
3810 assert_eq!(parts.len(), 2);
3811
3812 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "🚀 "));
3813 assert_eq!(parts[0].offset, 0);
3814 assert_eq!(parts[0].len, 5, "literal len must be bytes, not chars");
3815
3816 assert!(matches!(&parts[1].part, StringPart::Var(_)));
3817 assert_eq!(parts[1].offset, 5, "var offset must be bytes, not chars");
3818 assert_eq!(parts[1].len, 4);
3819 }
3820
3821 #[test]
3822 fn spanned_multibyte_utf8_pure_literal_is_byte_length() {
3823 let parts = parse_interpolated_string_spanned("hello 世界 world", 0);
3826 assert_eq!(parts.len(), 1);
3827 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "hello 世界 world"));
3828 assert_eq!(parts[0].offset, 0);
3829 assert_eq!(parts[0].len, 18);
3830 }
3831
3832 #[test]
3833 fn spanned_escape_dollar_consumes_two_bytes_emits_one_char() {
3834 let parts = parse_interpolated_string_spanned("\\$", 0);
3837 assert_eq!(parts.len(), 1);
3838 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "$"));
3839 assert_eq!(parts[0].offset, 0);
3840 assert_eq!(parts[0].len, 2, "len is source byte length, not rendered length");
3841 }
3842
3843 #[test]
3844 fn spanned_escape_backslash_collapses_pair_to_one() {
3845 let parts = parse_interpolated_string_spanned("\\\\", 0);
3846 assert_eq!(parts.len(), 1);
3847 assert!(matches!(&parts[0].part, StringPart::Literal(s) if s == "\\"));
3848 assert_eq!(parts[0].len, 2);
3849 }
3850}