1use crate::ast::{
7 Arg, Assignment, BinaryOp, CaseBranch, CaseStmt, Command, Expr, FileTestOp, ForLoop, IfStmt,
8 Pipeline, Program, Redirect, RedirectKind, Stmt, StringPart, StringTestOp, TestCmpOp, TestExpr,
9 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];
41 let default = parse_interpolated_string(default_str);
42 return Expr::VarWithDefault { name, default };
43 }
44
45 Expr::VarRef(parse_varpath(raw))
47}
48
49fn find_default_separator(raw: &str) -> Option<usize> {
51 let bytes = raw.as_bytes();
52 let mut depth = 0;
53 let mut i = 0;
54
55 while i < bytes.len() {
56 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
57 depth += 1;
58 i += 2;
59 continue;
60 }
61 if bytes[i] == b'}' && depth > 0 {
62 depth -= 1;
63 i += 1;
64 continue;
65 }
66 if depth == 1 && i + 1 < bytes.len() && bytes[i] == b':' && bytes[i + 1] == b'-' {
68 return Some(i);
69 }
70 i += 1;
71 }
72 None
73}
74
75fn find_default_separator_in_content(content: &str) -> Option<usize> {
77 let bytes = content.as_bytes();
78 let mut depth = 0;
79 let mut i = 0;
80
81 while i < bytes.len() {
82 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
83 depth += 1;
84 i += 2;
85 continue;
86 }
87 if bytes[i] == b'}' && depth > 0 {
88 depth -= 1;
89 i += 1;
90 continue;
91 }
92 if depth == 0 && i + 1 < bytes.len() && bytes[i] == b':' && bytes[i + 1] == b'-' {
94 return Some(i);
95 }
96 i += 1;
97 }
98 None
99}
100
101fn parse_varpath(raw: &str) -> VarPath {
105 let segments_strs = lexer::parse_var_ref(raw).unwrap_or_default();
106 let segments = segments_strs
107 .into_iter()
108 .filter(|s| !s.starts_with('[')) .map(VarSegment::Field)
110 .collect();
111 VarPath { segments }
112}
113
114fn stmt_to_pipeline(stmt: Stmt) -> Option<Pipeline> {
117 match stmt {
118 Stmt::Pipeline(p) => Some(p),
119 Stmt::Command(cmd) => Some(Pipeline {
120 commands: vec![cmd],
121 background: false,
122 }),
123 _ => None,
124 }
125}
126
127fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
128 let s = s.replace("__KAISH_ESCAPED_DOLLAR__", "\x00DOLLAR\x00");
131
132 let mut parts = Vec::new();
133 let mut current_text = String::new();
134 let mut chars = s.chars().peekable();
135
136 while let Some(ch) = chars.next() {
137 if ch == '\x00' {
138 let mut marker = String::new();
140 while let Some(&c) = chars.peek() {
141 if c == '\x00' {
142 chars.next(); break;
144 }
145 if let Some(c) = chars.next() {
146 marker.push(c);
147 }
148 }
149 if marker == "DOLLAR" {
150 current_text.push('$');
151 }
152 } else if ch == '$' {
153 if chars.peek() == Some(&'(') {
155 if !current_text.is_empty() {
157 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
158 }
159
160 chars.next();
162
163 let mut cmd_content = String::new();
165 let mut paren_depth = 1;
166 for c in chars.by_ref() {
167 if c == '(' {
168 paren_depth += 1;
169 cmd_content.push(c);
170 } else if c == ')' {
171 paren_depth -= 1;
172 if paren_depth == 0 {
173 break;
174 }
175 cmd_content.push(c);
176 } else {
177 cmd_content.push(c);
178 }
179 }
180
181 if let Ok(program) = parse(&cmd_content) {
184 if let Some(stmt) = program.statements.first() {
186 if let Some(pipeline) = stmt_to_pipeline(stmt.clone()) {
187 parts.push(StringPart::CommandSubst(pipeline));
188 } else {
189 current_text.push_str("$(");
191 current_text.push_str(&cmd_content);
192 current_text.push(')');
193 }
194 }
195 } else {
196 current_text.push_str("$(");
198 current_text.push_str(&cmd_content);
199 current_text.push(')');
200 }
201 } else if chars.peek() == Some(&'{') {
202 if !current_text.is_empty() {
204 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
205 }
206
207 chars.next();
209
210 let mut var_content = String::new();
212 let mut depth = 1;
213 for c in chars.by_ref() {
214 if c == '{' && var_content.ends_with('$') {
215 depth += 1;
216 var_content.push(c);
217 } else if c == '}' {
218 depth -= 1;
219 if depth == 0 {
220 break;
221 }
222 var_content.push(c);
223 } else {
224 var_content.push(c);
225 }
226 }
227
228 let part = if let Some(name) = var_content.strip_prefix('#') {
230 StringPart::VarLength(name.to_string())
232 } else if var_content.starts_with("__ARITH:") && var_content.ends_with("__") {
233 let expr = var_content
235 .strip_prefix("__ARITH:")
236 .and_then(|s| s.strip_suffix("__"))
237 .unwrap_or("");
238 StringPart::Arithmetic(expr.to_string())
239 } else if let Some(colon_idx) = find_default_separator_in_content(&var_content) {
240 let name = var_content[..colon_idx].to_string();
242 let default_str = &var_content[colon_idx + 2..];
243 let default = parse_interpolated_string(default_str);
244 StringPart::VarWithDefault { name, default }
245 } else {
246 StringPart::Var(parse_varpath(&format!("${{{}}}", var_content)))
248 };
249 parts.push(part);
250 } else if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
251 if !current_text.is_empty() {
253 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
254 }
255 if let Some(digit) = chars.next() {
256 let n = digit.to_digit(10).unwrap_or(0) as usize;
257 parts.push(StringPart::Positional(n));
258 }
259 } else if chars.peek() == Some(&'@') {
260 if !current_text.is_empty() {
262 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
263 }
264 chars.next(); parts.push(StringPart::AllArgs);
266 } else if chars.peek() == Some(&'#') {
267 if !current_text.is_empty() {
269 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
270 }
271 chars.next(); parts.push(StringPart::ArgCount);
273 } else if chars.peek() == Some(&'?') {
274 if !current_text.is_empty() {
276 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
277 }
278 chars.next(); parts.push(StringPart::LastExitCode);
280 } else if chars.peek() == Some(&'$') {
281 if !current_text.is_empty() {
283 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
284 }
285 chars.next(); parts.push(StringPart::CurrentPid);
287 } else if chars.peek().map(|c| c.is_ascii_alphabetic() || *c == '_').unwrap_or(false) {
288 if !current_text.is_empty() {
290 parts.push(StringPart::Literal(std::mem::take(&mut current_text)));
291 }
292
293 let mut var_name = String::new();
295 while let Some(&c) = chars.peek() {
296 if c.is_ascii_alphanumeric() || c == '_' {
297 if let Some(c) = chars.next() {
298 var_name.push(c);
299 }
300 } else {
301 break;
302 }
303 }
304
305 parts.push(StringPart::Var(VarPath::simple(var_name)));
306 } else {
307 current_text.push(ch);
309 }
310 } else {
311 current_text.push(ch);
312 }
313 }
314
315 if !current_text.is_empty() {
316 parts.push(StringPart::Literal(current_text));
317 }
318
319 parts
320}
321
322#[derive(Debug, Clone)]
324pub struct ParseError {
325 pub span: Span,
326 pub message: String,
327}
328
329impl std::fmt::Display for ParseError {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 write!(f, "{} at {:?}", self.message, self.span)
332 }
333}
334
335impl std::error::Error for ParseError {}
336
337pub fn parse(source: &str) -> Result<Program, Vec<ParseError>> {
339 let tokens = lexer::tokenize(source).map_err(|errs| {
341 errs.into_iter()
342 .map(|e| ParseError {
343 span: (e.span.start..e.span.end).into(),
344 message: format!("lexer error: {}", e.token),
345 })
346 .collect::<Vec<_>>()
347 })?;
348
349 let tokens: Vec<(Token, Span)> = tokens
351 .into_iter()
352 .map(|spanned| (spanned.token, (spanned.span.start..spanned.span.end).into()))
353 .collect();
354
355 let end_span: Span = (source.len()..source.len()).into();
357
358 let parser = program_parser();
360 let result = parser.parse(tokens.as_slice().map(end_span, |(t, s)| (t, s)));
361
362 result.into_result().map_err(|errs| {
363 errs.into_iter()
364 .map(|e| ParseError {
365 span: *e.span(),
366 message: e.to_string(),
367 })
368 .collect()
369 })
370}
371
372pub fn parse_statement(source: &str) -> Result<Stmt, Vec<ParseError>> {
374 let program = parse(source)?;
375 program
376 .statements
377 .into_iter()
378 .find(|s| !matches!(s, Stmt::Empty))
379 .ok_or_else(|| {
380 vec![ParseError {
381 span: (0..source.len()).into(),
382 message: "empty input".to_string(),
383 }]
384 })
385}
386
387fn program_parser<'tokens, 'src: 'tokens, I>(
393) -> impl Parser<'tokens, I, Program, extra::Err<Rich<'tokens, Token, Span>>>
394where
395 I: ValueInput<'tokens, Token = Token, Span = Span>,
396{
397 statement_parser()
398 .repeated()
399 .collect::<Vec<_>>()
400 .map(|statements| Program { statements })
401}
402
403fn statement_parser<'tokens, I>(
406) -> impl Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
407where
408 I: ValueInput<'tokens, Token = Token, Span = Span>,
409{
410 recursive(|stmt| {
411 let terminator = choice((just(Token::Newline), just(Token::Semi))).repeated();
412
413 let break_stmt = just(Token::Break)
415 .ignore_then(
416 select! { Token::Int(n) => n as usize }.or_not()
417 )
418 .map(Stmt::Break);
419
420 let continue_stmt = just(Token::Continue)
422 .ignore_then(
423 select! { Token::Int(n) => n as usize }.or_not()
424 )
425 .map(Stmt::Continue);
426
427 let return_stmt = just(Token::Return)
429 .ignore_then(primary_expr_parser().or_not())
430 .map(|e| Stmt::Return(e.map(Box::new)));
431
432 let exit_stmt = just(Token::Exit)
434 .ignore_then(primary_expr_parser().or_not())
435 .map(|e| Stmt::Exit(e.map(Box::new)));
436
437 let set_flag_arg = choice((
446 select! { Token::ShortFlag(f) => Arg::ShortFlag(f) },
447 select! { Token::LongFlag(f) => Arg::LongFlag(f) },
448 select! { Token::PlusFlag(f) => Arg::Positional(Expr::Literal(Value::String(format!("+{}", f)))) },
450 ));
451
452 let set_with_flags = just(Token::Set)
454 .then(set_flag_arg)
455 .then(
456 choice((
457 set_flag_arg,
458 ident_parser().map(|name| Arg::Positional(Expr::Literal(Value::String(name)))),
460 ))
461 .repeated()
462 .collect::<Vec<_>>(),
463 )
464 .map(|((_, first_arg), mut rest_args)| {
465 let mut args = vec![first_arg];
466 args.append(&mut rest_args);
467 Stmt::Command(Command {
468 name: "set".to_string(),
469 args,
470 redirects: vec![],
471 })
472 });
473
474 let set_no_args = just(Token::Set)
477 .then(
478 choice((
479 just(Token::Newline).to(()),
480 just(Token::Semi).to(()),
481 just(Token::And).to(()),
482 just(Token::Or).to(()),
483 end(),
484 ))
485 .rewind(),
486 )
487 .map(|_| Stmt::Command(Command {
488 name: "set".to_string(),
489 args: vec![],
490 redirects: vec![],
491 }));
492
493 let set_command = set_with_flags.or(set_no_args);
497
498 let base_statement = choice((
500 just(Token::Newline).to(Stmt::Empty),
501 set_command,
502 assignment_parser().map(Stmt::Assignment),
503 posix_function_parser(stmt.clone()).map(Stmt::ToolDef), bash_function_parser(stmt.clone()).map(Stmt::ToolDef), if_parser(stmt.clone()).map(Stmt::If),
507 for_parser(stmt.clone()).map(Stmt::For),
508 while_parser(stmt.clone()).map(Stmt::While),
509 case_parser(stmt.clone()).map(Stmt::Case),
510 break_stmt,
511 continue_stmt,
512 return_stmt,
513 exit_stmt,
514 test_expr_stmt_parser().map(Stmt::Test),
515 pipeline_parser().map(|p| {
517 if p.commands.len() == 1 && !p.background {
519 if p.commands[0].redirects.is_empty() {
521 match p.commands.into_iter().next() {
523 Some(cmd) => Stmt::Command(cmd),
524 None => Stmt::Empty, }
526 } else {
527 Stmt::Pipeline(p)
528 }
529 } else {
530 Stmt::Pipeline(p)
531 }
532 }),
533 ))
534 .boxed();
535
536 let and_chain = base_statement
540 .clone()
541 .foldl(
542 just(Token::And).ignore_then(base_statement).repeated(),
543 |left, right| Stmt::AndChain {
544 left: Box::new(left),
545 right: Box::new(right),
546 },
547 );
548
549 and_chain
550 .clone()
551 .foldl(
552 just(Token::Or).ignore_then(and_chain).repeated(),
553 |left, right| Stmt::OrChain {
554 left: Box::new(left),
555 right: Box::new(right),
556 },
557 )
558 .then_ignore(terminator)
559 })
560}
561
562fn assignment_parser<'tokens, I>(
564) -> impl Parser<'tokens, I, Assignment, extra::Err<Rich<'tokens, Token, Span>>> + Clone
565where
566 I: ValueInput<'tokens, Token = Token, Span = Span>,
567{
568 let local_assignment = just(Token::Local)
570 .ignore_then(ident_parser())
571 .then_ignore(just(Token::Eq))
572 .then(expr_parser())
573 .map(|(name, value)| Assignment {
574 name,
575 value,
576 local: true,
577 });
578
579 let bash_assignment = ident_parser()
582 .then_ignore(just(Token::Eq))
583 .then(expr_parser())
584 .map(|(name, value)| Assignment {
585 name,
586 value,
587 local: false,
588 });
589
590 choice((local_assignment, bash_assignment))
591 .labelled("assignment")
592 .boxed()
593}
594
595fn posix_function_parser<'tokens, I, S>(
599 stmt: S,
600) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
601where
602 I: ValueInput<'tokens, Token = Token, Span = Span>,
603 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
604{
605 ident_parser()
606 .then_ignore(just(Token::LParen))
607 .then_ignore(just(Token::RParen))
608 .then_ignore(just(Token::LBrace))
609 .then_ignore(just(Token::Newline).repeated())
610 .then(
611 stmt.repeated()
612 .collect::<Vec<_>>()
613 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
614 )
615 .then_ignore(just(Token::Newline).repeated())
616 .then_ignore(just(Token::RBrace))
617 .map(|(name, body)| ToolDef { name, params: vec![], body })
618 .labelled("POSIX function")
619 .boxed()
620}
621
622fn bash_function_parser<'tokens, I, S>(
626 stmt: S,
627) -> impl Parser<'tokens, I, ToolDef, extra::Err<Rich<'tokens, Token, Span>>> + Clone
628where
629 I: ValueInput<'tokens, Token = Token, Span = Span>,
630 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
631{
632 just(Token::Function)
633 .ignore_then(ident_parser())
634 .then_ignore(just(Token::LBrace))
635 .then_ignore(just(Token::Newline).repeated())
636 .then(
637 stmt.repeated()
638 .collect::<Vec<_>>()
639 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
640 )
641 .then_ignore(just(Token::Newline).repeated())
642 .then_ignore(just(Token::RBrace))
643 .map(|(name, body)| ToolDef { name, params: vec![], body })
644 .labelled("bash function")
645 .boxed()
646}
647
648fn if_parser<'tokens, I, S>(
655 stmt: S,
656) -> impl Parser<'tokens, I, IfStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
657where
658 I: ValueInput<'tokens, Token = Token, Span = Span>,
659 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
660{
661 let branch = condition_parser()
663 .then_ignore(just(Token::Semi).or_not())
664 .then_ignore(just(Token::Newline).repeated())
665 .then_ignore(just(Token::Then))
666 .then_ignore(just(Token::Newline).repeated())
667 .then(
668 stmt.clone()
669 .repeated()
670 .collect::<Vec<_>>()
671 .map(|stmts: Vec<Stmt>| {
672 stmts
673 .into_iter()
674 .filter(|s| !matches!(s, Stmt::Empty))
675 .collect::<Vec<_>>()
676 }),
677 );
678
679 let elif_branch = just(Token::Elif)
681 .ignore_then(condition_parser())
682 .then_ignore(just(Token::Semi).or_not())
683 .then_ignore(just(Token::Newline).repeated())
684 .then_ignore(just(Token::Then))
685 .then_ignore(just(Token::Newline).repeated())
686 .then(
687 stmt.clone()
688 .repeated()
689 .collect::<Vec<_>>()
690 .map(|stmts: Vec<Stmt>| {
691 stmts
692 .into_iter()
693 .filter(|s| !matches!(s, Stmt::Empty))
694 .collect::<Vec<_>>()
695 }),
696 );
697
698 let else_branch = just(Token::Else)
700 .ignore_then(just(Token::Newline).repeated())
701 .ignore_then(stmt.repeated().collect::<Vec<_>>())
702 .map(|stmts: Vec<Stmt>| {
703 stmts
704 .into_iter()
705 .filter(|s| !matches!(s, Stmt::Empty))
706 .collect::<Vec<_>>()
707 });
708
709 just(Token::If)
710 .ignore_then(branch)
711 .then(elif_branch.repeated().collect::<Vec<_>>())
712 .then(else_branch.or_not())
713 .then_ignore(just(Token::Fi))
714 .map(|(((condition, then_branch), elif_branches), else_branch)| {
715 build_if_chain(condition, then_branch, elif_branches, else_branch)
717 })
718 .labelled("if statement")
719 .boxed()
720}
721
722fn build_if_chain(
729 condition: Expr,
730 then_branch: Vec<Stmt>,
731 mut elif_branches: Vec<(Expr, Vec<Stmt>)>,
732 else_branch: Option<Vec<Stmt>>,
733) -> IfStmt {
734 if elif_branches.is_empty() {
735 IfStmt {
737 condition: Box::new(condition),
738 then_branch,
739 else_branch,
740 }
741 } else {
742 let (elif_cond, elif_then) = elif_branches.remove(0);
744 let nested_if = build_if_chain(elif_cond, elif_then, elif_branches, else_branch);
745 IfStmt {
746 condition: Box::new(condition),
747 then_branch,
748 else_branch: Some(vec![Stmt::If(nested_if)]),
749 }
750 }
751}
752
753fn for_parser<'tokens, I, S>(
755 stmt: S,
756) -> impl Parser<'tokens, I, ForLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
757where
758 I: ValueInput<'tokens, Token = Token, Span = Span>,
759 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
760{
761 just(Token::For)
762 .ignore_then(ident_parser())
763 .then_ignore(just(Token::In))
764 .then(expr_parser().repeated().at_least(1).collect::<Vec<_>>())
765 .then_ignore(just(Token::Semi).or_not())
766 .then_ignore(just(Token::Newline).repeated())
767 .then_ignore(just(Token::Do))
768 .then_ignore(just(Token::Newline).repeated())
769 .then(
770 stmt.repeated()
771 .collect::<Vec<_>>()
772 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
773 )
774 .then_ignore(just(Token::Done))
775 .map(|((variable, items), body)| ForLoop {
776 variable,
777 items,
778 body,
779 })
780 .labelled("for loop")
781 .boxed()
782}
783
784fn while_parser<'tokens, I, S>(
786 stmt: S,
787) -> impl Parser<'tokens, I, WhileLoop, extra::Err<Rich<'tokens, Token, Span>>> + Clone
788where
789 I: ValueInput<'tokens, Token = Token, Span = Span>,
790 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
791{
792 just(Token::While)
793 .ignore_then(condition_parser())
794 .then_ignore(just(Token::Semi).or_not())
795 .then_ignore(just(Token::Newline).repeated())
796 .then_ignore(just(Token::Do))
797 .then_ignore(just(Token::Newline).repeated())
798 .then(
799 stmt.repeated()
800 .collect::<Vec<_>>()
801 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
802 )
803 .then_ignore(just(Token::Done))
804 .map(|(condition, body)| WhileLoop {
805 condition: Box::new(condition),
806 body,
807 })
808 .labelled("while loop")
809 .boxed()
810}
811
812fn case_parser<'tokens, I, S>(
819 stmt: S,
820) -> impl Parser<'tokens, I, CaseStmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone
821where
822 I: ValueInput<'tokens, Token = Token, Span = Span>,
823 S: Parser<'tokens, I, Stmt, extra::Err<Rich<'tokens, Token, Span>>> + Clone + 'tokens,
824{
825 let pattern_part = choice((
828 select! { Token::GlobWord(s) => s },
829 select! { Token::Ident(s) => s },
830 select! { Token::String(s) => s },
831 select! { Token::SingleString(s) => s },
832 select! { Token::Int(n) => n.to_string() },
833 select! { Token::Star => "*".to_string() },
834 select! { Token::Question => "?".to_string() },
835 select! { Token::Dot => ".".to_string() },
836 select! { Token::DotDot => "..".to_string() },
837 select! { Token::Tilde => "~".to_string() },
838 select! { Token::TildePath(s) => s },
839 select! { Token::RelativePath(s) => s },
840 select! { Token::DotSlashPath(s) => s },
841 select! { Token::Path(p) => p },
842 select! { Token::VarRef(v) => v },
843 select! { Token::SimpleVarRef(v) => format!("${}", v) },
844 just(Token::LBracket)
846 .ignore_then(
847 choice((
848 select! { Token::Ident(s) => s },
849 select! { Token::Int(n) => n.to_string() },
850 just(Token::Colon).to(":".to_string()),
851 just(Token::Bang).to("!".to_string()),
853 select! { Token::ShortFlag(s) => format!("-{}", s) },
855 ))
856 .repeated()
857 .at_least(1)
858 .collect::<Vec<String>>()
859 )
860 .then_ignore(just(Token::RBracket))
861 .map(|parts| format!("[{}]", parts.join(""))),
862 just(Token::LBrace)
864 .ignore_then(
865 choice((
866 select! { Token::Ident(s) => s },
867 select! { Token::Int(n) => n.to_string() },
868 ))
869 .separated_by(just(Token::Comma))
870 .at_least(1)
871 .collect::<Vec<String>>()
872 )
873 .then_ignore(just(Token::RBrace))
874 .map(|parts| format!("{{{}}}", parts.join(","))),
875 ));
876
877 let pattern = pattern_part
880 .repeated()
881 .at_least(1)
882 .collect::<Vec<String>>()
883 .map(|parts| parts.join(""))
884 .labelled("case pattern");
885
886 let patterns = pattern
888 .separated_by(just(Token::Pipe))
889 .at_least(1)
890 .collect::<Vec<String>>()
891 .labelled("case patterns");
892
893 let branch = just(Token::LParen)
895 .or_not()
896 .ignore_then(just(Token::Newline).repeated())
897 .ignore_then(patterns)
898 .then_ignore(just(Token::RParen))
899 .then_ignore(just(Token::Newline).repeated())
900 .then(
901 stmt.clone()
902 .repeated()
903 .collect::<Vec<_>>()
904 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
905 )
906 .then_ignore(just(Token::DoubleSemi))
907 .then_ignore(just(Token::Newline).repeated())
908 .map(|(patterns, body)| CaseBranch { patterns, body })
909 .labelled("case branch");
910
911 just(Token::Case)
912 .ignore_then(expr_parser())
913 .then_ignore(just(Token::In))
914 .then_ignore(just(Token::Newline).repeated())
915 .then(branch.repeated().collect::<Vec<_>>())
916 .then_ignore(just(Token::Esac))
917 .map(|(expr, branches)| CaseStmt { expr, branches })
918 .labelled("case statement")
919 .boxed()
920}
921
922fn pipeline_parser<'tokens, I>(
924) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
925where
926 I: ValueInput<'tokens, Token = Token, Span = Span>,
927{
928 command_parser()
929 .separated_by(just(Token::Pipe))
930 .at_least(1)
931 .collect::<Vec<_>>()
932 .then(just(Token::Amp).or_not())
933 .map(|(commands, bg)| Pipeline {
934 commands,
935 background: bg.is_some(),
936 })
937 .labelled("pipeline")
938 .boxed()
939}
940
941fn command_parser<'tokens, I>(
944) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
945where
946 I: ValueInput<'tokens, Token = Token, Span = Span>,
947{
948 let command_name = choice((
950 ident_parser(),
951 path_parser(),
952 select! { Token::DotSlashPath(s) => s },
953 just(Token::True).to("true".to_string()),
954 just(Token::False).to("false".to_string()),
955 just(Token::Dot).to(".".to_string()),
956 ));
957
958 command_name
959 .then(args_list_parser())
960 .then(redirect_parser().repeated().collect::<Vec<_>>())
961 .map(|((name, args), redirects)| Command {
962 name,
963 args,
964 redirects,
965 })
966 .labelled("command")
967 .boxed()
968}
969
970fn args_list_parser<'tokens, I>(
974) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
975where
976 I: ValueInput<'tokens, Token = Token, Span = Span>,
977{
978 let pre_dash = arg_before_double_dash_parser()
980 .repeated()
981 .collect::<Vec<_>>();
982
983 let double_dash = select! {
985 Token::DoubleDash => Arg::DoubleDash,
986 };
987
988 let post_dash_arg = choice((
990 select! {
992 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
993 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
994 },
995 primary_expr_parser().map(Arg::Positional),
997 ));
998
999 let post_dash = post_dash_arg.repeated().collect::<Vec<_>>();
1000
1001 pre_dash
1003 .then(double_dash.then(post_dash).or_not())
1004 .map(|(mut args, maybe_dd)| {
1005 if let Some((dd, post)) = maybe_dd {
1006 args.push(dd);
1007 args.extend(post);
1008 }
1009 args
1010 })
1011}
1012
1013fn arg_before_double_dash_parser<'tokens, I>(
1015) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1016where
1017 I: ValueInput<'tokens, Token = Token, Span = Span>,
1018{
1019 let long_flag_with_value = select! {
1021 Token::LongFlag(name) => name,
1022 }
1023 .then_ignore(just(Token::Eq))
1024 .then(primary_expr_parser())
1025 .map(|(key, value)| Arg::Named { key, value });
1026
1027 let long_flag = select! {
1029 Token::LongFlag(name) => Arg::LongFlag(name),
1030 };
1031
1032 let short_flag = select! {
1034 Token::ShortFlag(name) => Arg::ShortFlag(name),
1035 };
1036
1037 let named = select! {
1040 Token::Ident(s) => s,
1041 }
1042 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1043 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1044 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1045 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1046 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1048 Err(Rich::custom(
1049 span,
1050 "named argument must not have spaces around '=' (use 'key=value' not 'key = value')",
1051 ))
1052 } else {
1053 Ok(Arg::Named { key, value })
1054 }
1055 });
1056
1057 let positional = primary_expr_parser().map(Arg::Positional);
1059
1060 choice((
1063 long_flag_with_value,
1064 long_flag,
1065 short_flag,
1066 named,
1067 positional,
1068 ))
1069 .boxed()
1070}
1071
1072fn redirect_parser<'tokens, I>(
1074) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1075where
1076 I: ValueInput<'tokens, Token = Token, Span = Span>,
1077{
1078 let regular_redirect = select! {
1080 Token::GtGt => RedirectKind::StdoutAppend,
1081 Token::Gt => RedirectKind::StdoutOverwrite,
1082 Token::Lt => RedirectKind::Stdin,
1083 Token::Stderr => RedirectKind::Stderr,
1084 Token::Both => RedirectKind::Both,
1085 }
1086 .then(primary_expr_parser())
1087 .map(|(kind, target)| Redirect { kind, target });
1088
1089 let heredoc_redirect = just(Token::HereDocStart)
1093 .ignore_then(select! { Token::HereDoc(data) => data })
1094 .map(|data: HereDocData| {
1095 let target = if data.literal {
1096 Expr::Literal(Value::String(data.content))
1097 } else {
1098 let parts = parse_interpolated_string(&data.content);
1099 if parts.len() == 1 {
1101 if let StringPart::Literal(text) = &parts[0] {
1102 return Redirect {
1103 kind: RedirectKind::HereDoc,
1104 target: Expr::Literal(Value::String(text.clone())),
1105 };
1106 }
1107 }
1108 Expr::Interpolated(parts)
1109 };
1110 Redirect {
1111 kind: RedirectKind::HereDoc,
1112 target,
1113 }
1114 });
1115
1116 let merge_stderr_redirect = just(Token::StderrToStdout)
1118 .map(|_| Redirect {
1119 kind: RedirectKind::MergeStderr,
1120 target: Expr::Literal(Value::Null),
1122 });
1123
1124 let merge_stdout_redirect = choice((
1126 just(Token::StdoutToStderr),
1127 just(Token::StdoutToStderr2),
1128 ))
1129 .map(|_| Redirect {
1130 kind: RedirectKind::MergeStdout,
1131 target: Expr::Literal(Value::Null),
1133 });
1134
1135 choice((heredoc_redirect, merge_stderr_redirect, merge_stdout_redirect, regular_redirect))
1136 .labelled("redirect")
1137 .boxed()
1138}
1139
1140fn test_expr_stmt_parser<'tokens, I>(
1150) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1151where
1152 I: ValueInput<'tokens, Token = Token, Span = Span>,
1153{
1154 let file_test_op = select! {
1156 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
1157 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
1158 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
1159 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
1160 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
1161 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
1162 };
1163
1164 let string_test_op = select! {
1166 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
1167 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
1168 };
1169
1170 let cmp_op = choice((
1173 just(Token::EqEq).to(TestCmpOp::Eq),
1174 just(Token::Eq).to(TestCmpOp::Eq),
1175 just(Token::NotEq).to(TestCmpOp::NotEq),
1176 just(Token::Match).to(TestCmpOp::Match),
1177 just(Token::NotMatch).to(TestCmpOp::NotMatch),
1178 just(Token::Gt).to(TestCmpOp::Gt),
1179 just(Token::Lt).to(TestCmpOp::Lt),
1180 just(Token::GtEq).to(TestCmpOp::GtEq),
1181 just(Token::LtEq).to(TestCmpOp::LtEq),
1182 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::Eq },
1183 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NotEq },
1184 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::Gt },
1185 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::Lt },
1186 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::GtEq },
1187 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::LtEq },
1188 ));
1189
1190 let file_test = file_test_op
1192 .then(primary_expr_parser())
1193 .map(|(op, path)| TestExpr::FileTest {
1194 op,
1195 path: Box::new(path),
1196 });
1197
1198 let string_test = string_test_op
1200 .then(primary_expr_parser())
1201 .map(|(op, value)| TestExpr::StringTest {
1202 op,
1203 value: Box::new(value),
1204 });
1205
1206 let comparison = primary_expr_parser()
1208 .then(cmp_op)
1209 .then(primary_expr_parser())
1210 .map(|((left, op), right)| TestExpr::Comparison {
1211 left: Box::new(left),
1212 op,
1213 right: Box::new(right),
1214 });
1215
1216 let primary_test = choice((file_test, string_test, comparison));
1218
1219 let compound_test = recursive(|compound| {
1230 let not_expr = just(Token::Bang)
1232 .ignore_then(compound.clone())
1233 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
1234
1235 let unary = choice((not_expr, primary_test.clone()));
1237
1238 let and_expr = unary.clone().foldl(
1240 just(Token::And).ignore_then(unary).repeated(),
1241 |left, right| TestExpr::And {
1242 left: Box::new(left),
1243 right: Box::new(right),
1244 },
1245 );
1246
1247 and_expr.clone().foldl(
1249 just(Token::Or).ignore_then(and_expr).repeated(),
1250 |left, right| TestExpr::Or {
1251 left: Box::new(left),
1252 right: Box::new(right),
1253 },
1254 )
1255 });
1256
1257 just(Token::LBracket)
1260 .then(just(Token::LBracket))
1261 .ignore_then(compound_test)
1262 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
1263 .labelled("test expression")
1264 .boxed()
1265}
1266
1267fn condition_parser<'tokens, I>(
1282) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1283where
1284 I: ValueInput<'tokens, Token = Token, Span = Span>,
1285{
1286 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
1288
1289 let command_condition = command_parser().map(Expr::Command);
1292
1293 let base = choice((test_expr_condition, command_condition));
1295
1296 let and_expr = base.clone().foldl(
1299 just(Token::And).ignore_then(base).repeated(),
1300 |left, right| Expr::BinaryOp {
1301 left: Box::new(left),
1302 op: BinaryOp::And,
1303 right: Box::new(right),
1304 },
1305 );
1306
1307 and_expr
1309 .clone()
1310 .foldl(
1311 just(Token::Or).ignore_then(and_expr).repeated(),
1312 |left, right| Expr::BinaryOp {
1313 left: Box::new(left),
1314 op: BinaryOp::Or,
1315 right: Box::new(right),
1316 },
1317 )
1318 .labelled("condition")
1319 .boxed()
1320}
1321
1322fn expr_parser<'tokens, I>(
1324) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1325where
1326 I: ValueInput<'tokens, Token = Token, Span = Span>,
1327{
1328 primary_expr_parser()
1330}
1331
1332fn primary_expr_parser<'tokens, I>(
1336) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1337where
1338 I: ValueInput<'tokens, Token = Token, Span = Span>,
1339{
1340 let positional = select! {
1342 Token::Positional(n) => Expr::Positional(n),
1343 Token::AllArgs => Expr::AllArgs,
1344 Token::ArgCount => Expr::ArgCount,
1345 Token::VarLength(name) => Expr::VarLength(name),
1346 Token::LastExitCode => Expr::LastExitCode,
1347 Token::CurrentPid => Expr::CurrentPid,
1348 };
1349
1350 let arithmetic = select! {
1352 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
1353 };
1354
1355 let keyword_as_bareword = select! {
1358 Token::Done => "done",
1359 Token::Fi => "fi",
1360 Token::Then => "then",
1361 Token::Else => "else",
1362 Token::Elif => "elif",
1363 Token::In => "in",
1364 Token::Do => "do",
1365 Token::Esac => "esac",
1366 }
1367 .map(|s| Expr::Literal(Value::String(s.to_string())));
1368
1369 let plus_minus_bare = select! {
1371 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
1372 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
1373 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
1374 };
1375
1376 let glob_pattern = select! {
1378 Token::GlobWord(s) => Expr::GlobPattern(s),
1379 Token::Star => Expr::GlobPattern("*".to_string()),
1380 Token::Question => Expr::GlobPattern("?".to_string()),
1381 };
1382
1383 recursive(|expr| {
1384 choice((
1385 positional,
1386 arithmetic,
1387 cmd_subst_parser(expr.clone()),
1388 var_expr_parser(),
1389 interpolated_string_parser(),
1390 literal_parser().map(Expr::Literal),
1391 glob_pattern,
1393 ident_parser().map(|s| Expr::Literal(Value::String(s))),
1395 path_parser().map(|s| Expr::Literal(Value::String(s))),
1397 select! {
1400 Token::DotDot => Expr::Literal(Value::String("..".into())),
1401 Token::Tilde => Expr::Literal(Value::String("~".into())),
1402 Token::TildePath(s) => Expr::Literal(Value::String(s)),
1403 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
1404 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
1405 },
1406 plus_minus_bare,
1407 keyword_as_bareword,
1409 ))
1410 .labelled("expression")
1411 })
1412 .boxed()
1413}
1414
1415fn var_expr_parser<'tokens, I>(
1418) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1419where
1420 I: ValueInput<'tokens, Token = Token, Span = Span>,
1421{
1422 select! {
1423 Token::VarRef(raw) => parse_var_expr(&raw),
1424 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
1425 }
1426 .labelled("variable reference")
1427}
1428
1429fn cmd_subst_parser<'tokens, I, E>(
1433 expr: E,
1434) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1435where
1436 I: ValueInput<'tokens, Token = Token, Span = Span>,
1437 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone,
1438{
1439 let long_flag_with_value = select! {
1442 Token::LongFlag(name) => name,
1443 }
1444 .then_ignore(just(Token::Eq))
1445 .then(expr.clone())
1446 .map(|(key, value)| Arg::Named { key, value });
1447
1448 let long_flag = select! {
1450 Token::LongFlag(name) => Arg::LongFlag(name),
1451 };
1452
1453 let short_flag = select! {
1455 Token::ShortFlag(name) => Arg::ShortFlag(name),
1456 };
1457
1458 let named = ident_parser()
1460 .then_ignore(just(Token::Eq))
1461 .then(expr.clone())
1462 .map(|(key, value)| Arg::Named { key, value });
1463
1464 let positional = expr.map(Arg::Positional);
1466
1467 let arg = choice((
1468 long_flag_with_value,
1469 long_flag,
1470 short_flag,
1471 named,
1472 positional,
1473 ));
1474
1475 let command_name = choice((
1477 ident_parser(),
1478 just(Token::True).to("true".to_string()),
1479 just(Token::False).to("false".to_string()),
1480 ));
1481
1482 let command = command_name
1484 .then(arg.repeated().collect::<Vec<_>>())
1485 .map(|(name, args)| Command {
1486 name,
1487 args,
1488 redirects: vec![],
1489 });
1490
1491 let pipeline = command
1493 .separated_by(just(Token::Pipe))
1494 .at_least(1)
1495 .collect::<Vec<_>>()
1496 .map(|commands| Pipeline {
1497 commands,
1498 background: false,
1499 });
1500
1501 just(Token::CmdSubstStart)
1502 .ignore_then(pipeline)
1503 .then_ignore(just(Token::RParen))
1504 .map(|pipeline| Expr::CommandSubst(Box::new(pipeline)))
1505 .labelled("command substitution")
1506}
1507
1508fn interpolated_string_parser<'tokens, I>(
1510) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1511where
1512 I: ValueInput<'tokens, Token = Token, Span = Span>,
1513{
1514 let double_quoted = select! {
1516 Token::String(s) => s,
1517 }
1518 .map(|s| {
1519 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
1521 let parts = parse_interpolated_string(&s);
1523 if parts.len() == 1
1524 && let StringPart::Literal(text) = &parts[0] {
1525 return Expr::Literal(Value::String(text.clone()));
1526 }
1527 Expr::Interpolated(parts)
1528 } else {
1529 Expr::Literal(Value::String(s))
1530 }
1531 });
1532
1533 let single_quoted = select! {
1535 Token::SingleString(s) => Expr::Literal(Value::String(s)),
1536 };
1537
1538 choice((single_quoted, double_quoted)).labelled("string")
1539}
1540
1541fn literal_parser<'tokens, I>(
1543) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1544where
1545 I: ValueInput<'tokens, Token = Token, Span = Span>,
1546{
1547 choice((
1548 select! {
1549 Token::True => Value::Bool(true),
1550 Token::False => Value::Bool(false),
1551 },
1552 select! {
1553 Token::Int(n) => Value::Int(n),
1554 Token::Float(f) => Value::Float(f),
1555 },
1556 ))
1557 .labelled("literal")
1558 .boxed()
1559}
1560
1561fn ident_parser<'tokens, I>(
1563) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1564where
1565 I: ValueInput<'tokens, Token = Token, Span = Span>,
1566{
1567 select! {
1568 Token::Ident(s) => s,
1569 }
1570 .labelled("identifier")
1571}
1572
1573fn path_parser<'tokens, I>(
1575) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1576where
1577 I: ValueInput<'tokens, Token = Token, Span = Span>,
1578{
1579 select! {
1580 Token::Path(s) => s,
1581 }
1582 .labelled("path")
1583}
1584
1585#[cfg(test)]
1586mod tests {
1587 use super::*;
1588
1589 #[test]
1590 fn parse_empty() {
1591 let result = parse("");
1592 assert!(result.is_ok());
1593 assert_eq!(result.expect("ok").statements.len(), 0);
1594 }
1595
1596 #[test]
1597 fn parse_newlines_only() {
1598 let result = parse("\n\n\n");
1599 assert!(result.is_ok());
1600 }
1601
1602 #[test]
1603 fn parse_simple_command() {
1604 let result = parse("echo");
1605 assert!(result.is_ok());
1606 let program = result.expect("ok");
1607 assert_eq!(program.statements.len(), 1);
1608 assert!(matches!(&program.statements[0], Stmt::Command(_)));
1609 }
1610
1611 #[test]
1612 fn parse_command_with_string_arg() {
1613 let result = parse(r#"echo "hello""#);
1614 assert!(result.is_ok());
1615 let program = result.expect("ok");
1616 match &program.statements[0] {
1617 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
1618 _ => panic!("expected Command"),
1619 }
1620 }
1621
1622 #[test]
1623 fn parse_assignment() {
1624 let result = parse("X=5");
1625 assert!(result.is_ok());
1626 let program = result.expect("ok");
1627 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
1628 }
1629
1630 #[test]
1631 fn parse_pipeline() {
1632 let result = parse("a | b | c");
1633 assert!(result.is_ok());
1634 let program = result.expect("ok");
1635 match &program.statements[0] {
1636 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
1637 _ => panic!("expected Pipeline"),
1638 }
1639 }
1640
1641 #[test]
1642 fn parse_background_job() {
1643 let result = parse("cmd &");
1644 assert!(result.is_ok());
1645 let program = result.expect("ok");
1646 match &program.statements[0] {
1647 Stmt::Pipeline(p) => assert!(p.background),
1648 _ => panic!("expected Pipeline with background"),
1649 }
1650 }
1651
1652 #[test]
1653 fn parse_if_simple() {
1654 let result = parse("if true; then echo; fi");
1655 assert!(result.is_ok());
1656 let program = result.expect("ok");
1657 assert!(matches!(&program.statements[0], Stmt::If(_)));
1658 }
1659
1660 #[test]
1661 fn parse_if_else() {
1662 let result = parse("if true; then echo; else echo; fi");
1663 assert!(result.is_ok());
1664 let program = result.expect("ok");
1665 match &program.statements[0] {
1666 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
1667 _ => panic!("expected If"),
1668 }
1669 }
1670
1671 #[test]
1672 fn parse_elif_simple() {
1673 let result = parse("if true; then echo a; elif false; then echo b; fi");
1674 assert!(result.is_ok(), "parse failed: {:?}", result);
1675 let program = result.expect("ok");
1676 match &program.statements[0] {
1677 Stmt::If(if_stmt) => {
1678 assert!(if_stmt.else_branch.is_some());
1680 let else_branch = if_stmt.else_branch.as_ref().unwrap();
1681 assert_eq!(else_branch.len(), 1);
1682 assert!(matches!(&else_branch[0], Stmt::If(_)));
1683 }
1684 _ => panic!("expected If"),
1685 }
1686 }
1687
1688 #[test]
1689 fn parse_elif_with_else() {
1690 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
1691 assert!(result.is_ok(), "parse failed: {:?}", result);
1692 let program = result.expect("ok");
1693 match &program.statements[0] {
1694 Stmt::If(outer_if) => {
1695 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
1697 assert_eq!(else_branch.len(), 1);
1698 match &else_branch[0] {
1699 Stmt::If(inner_if) => {
1700 assert!(inner_if.else_branch.is_some());
1702 }
1703 _ => panic!("expected nested If from elif"),
1704 }
1705 }
1706 _ => panic!("expected If"),
1707 }
1708 }
1709
1710 #[test]
1711 fn parse_multiple_elif() {
1712 let result = parse(
1714 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
1715 );
1716 assert!(result.is_ok(), "parse failed: {:?}", result);
1717 }
1718
1719 #[test]
1720 fn parse_for_loop() {
1721 let result = parse("for X in items; do echo; done");
1722 assert!(result.is_ok());
1723 let program = result.expect("ok");
1724 assert!(matches!(&program.statements[0], Stmt::For(_)));
1725 }
1726
1727 #[test]
1728 fn parse_brackets_not_array_literal() {
1729 let result = parse("cmd [1");
1731 let _ = result;
1734 }
1735
1736 #[test]
1737 fn parse_named_arg() {
1738 let result = parse("cmd foo=5");
1739 assert!(result.is_ok());
1740 let program = result.expect("ok");
1741 match &program.statements[0] {
1742 Stmt::Command(cmd) => {
1743 assert_eq!(cmd.args.len(), 1);
1744 assert!(matches!(&cmd.args[0], Arg::Named { .. }));
1745 }
1746 _ => panic!("expected Command"),
1747 }
1748 }
1749
1750 #[test]
1751 fn parse_short_flag() {
1752 let result = parse("ls -l");
1753 assert!(result.is_ok());
1754 let program = result.expect("ok");
1755 match &program.statements[0] {
1756 Stmt::Command(cmd) => {
1757 assert_eq!(cmd.name, "ls");
1758 assert_eq!(cmd.args.len(), 1);
1759 match &cmd.args[0] {
1760 Arg::ShortFlag(name) => assert_eq!(name, "l"),
1761 _ => panic!("expected ShortFlag"),
1762 }
1763 }
1764 _ => panic!("expected Command"),
1765 }
1766 }
1767
1768 #[test]
1769 fn parse_long_flag() {
1770 let result = parse("git push --force");
1771 assert!(result.is_ok());
1772 let program = result.expect("ok");
1773 match &program.statements[0] {
1774 Stmt::Command(cmd) => {
1775 assert_eq!(cmd.name, "git");
1776 assert_eq!(cmd.args.len(), 2);
1777 match &cmd.args[0] {
1778 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
1779 _ => panic!("expected Positional push"),
1780 }
1781 match &cmd.args[1] {
1782 Arg::LongFlag(name) => assert_eq!(name, "force"),
1783 _ => panic!("expected LongFlag"),
1784 }
1785 }
1786 _ => panic!("expected Command"),
1787 }
1788 }
1789
1790 #[test]
1791 fn parse_long_flag_with_value() {
1792 let result = parse(r#"git commit --message="hello""#);
1793 assert!(result.is_ok());
1794 let program = result.expect("ok");
1795 match &program.statements[0] {
1796 Stmt::Command(cmd) => {
1797 assert_eq!(cmd.name, "git");
1798 assert_eq!(cmd.args.len(), 2);
1799 match &cmd.args[1] {
1800 Arg::Named { key, value } => {
1801 assert_eq!(key, "message");
1802 match value {
1803 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
1804 _ => panic!("expected String value"),
1805 }
1806 }
1807 _ => panic!("expected Named from --flag=value"),
1808 }
1809 }
1810 _ => panic!("expected Command"),
1811 }
1812 }
1813
1814 #[test]
1815 fn parse_mixed_flags_and_args() {
1816 let result = parse(r#"git commit -m "message" --amend"#);
1817 assert!(result.is_ok());
1818 let program = result.expect("ok");
1819 match &program.statements[0] {
1820 Stmt::Command(cmd) => {
1821 assert_eq!(cmd.name, "git");
1822 assert_eq!(cmd.args.len(), 4);
1823 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
1825 match &cmd.args[1] {
1827 Arg::ShortFlag(name) => assert_eq!(name, "m"),
1828 _ => panic!("expected ShortFlag -m"),
1829 }
1830 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
1832 match &cmd.args[3] {
1834 Arg::LongFlag(name) => assert_eq!(name, "amend"),
1835 _ => panic!("expected LongFlag --amend"),
1836 }
1837 }
1838 _ => panic!("expected Command"),
1839 }
1840 }
1841
1842 #[test]
1843 fn parse_redirect_stdout() {
1844 let result = parse("cmd > file");
1845 assert!(result.is_ok());
1846 let program = result.expect("ok");
1847 match &program.statements[0] {
1849 Stmt::Pipeline(p) => {
1850 assert_eq!(p.commands.len(), 1);
1851 let cmd = &p.commands[0];
1852 assert_eq!(cmd.redirects.len(), 1);
1853 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
1854 }
1855 _ => panic!("expected Pipeline"),
1856 }
1857 }
1858
1859 #[test]
1860 fn parse_var_ref() {
1861 let result = parse("echo ${VAR}");
1862 assert!(result.is_ok());
1863 let program = result.expect("ok");
1864 match &program.statements[0] {
1865 Stmt::Command(cmd) => {
1866 assert_eq!(cmd.args.len(), 1);
1867 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
1868 }
1869 _ => panic!("expected Command"),
1870 }
1871 }
1872
1873 #[test]
1874 fn parse_multiple_statements() {
1875 let result = parse("a\nb\nc");
1876 assert!(result.is_ok());
1877 let program = result.expect("ok");
1878 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
1879 assert_eq!(non_empty.len(), 3);
1880 }
1881
1882 #[test]
1883 fn parse_semicolon_separated() {
1884 let result = parse("a; b; c");
1885 assert!(result.is_ok());
1886 let program = result.expect("ok");
1887 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
1888 assert_eq!(non_empty.len(), 3);
1889 }
1890
1891 #[test]
1892 fn parse_complex_pipeline() {
1893 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
1894 assert!(result.is_ok());
1895 let program = result.expect("ok");
1896 match &program.statements[0] {
1897 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
1898 _ => panic!("expected Pipeline"),
1899 }
1900 }
1901
1902 #[test]
1903 fn parse_json_as_string_arg() {
1904 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
1906 assert!(result.is_ok());
1907 }
1908
1909 #[test]
1910 fn parse_mixed_args() {
1911 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
1912 assert!(result.is_ok());
1913 let program = result.expect("ok");
1914 match &program.statements[0] {
1915 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
1916 _ => panic!("expected Command"),
1917 }
1918 }
1919
1920 #[test]
1921 fn error_unterminated_string() {
1922 let result = parse(r#"echo "hello"#);
1923 assert!(result.is_err());
1924 }
1925
1926 #[test]
1927 fn error_unterminated_var_ref() {
1928 let result = parse("echo ${VAR");
1929 assert!(result.is_err());
1930 }
1931
1932 #[test]
1933 fn error_missing_fi() {
1934 let result = parse("if true; then echo");
1935 assert!(result.is_err());
1936 }
1937
1938 #[test]
1939 fn error_missing_done() {
1940 let result = parse("for X in items; do echo");
1941 assert!(result.is_err());
1942 }
1943
1944 #[test]
1945 fn parse_nested_cmd_subst() {
1946 let result = parse("X=$(echo $(date))").unwrap();
1948 match &result.statements[0] {
1949 Stmt::Assignment(a) => {
1950 assert_eq!(a.name, "X");
1951 match &a.value {
1952 Expr::CommandSubst(outer) => {
1953 assert_eq!(outer.commands[0].name, "echo");
1954 match &outer.commands[0].args[0] {
1956 Arg::Positional(Expr::CommandSubst(inner)) => {
1957 assert_eq!(inner.commands[0].name, "date");
1958 }
1959 other => panic!("expected nested cmd subst, got {:?}", other),
1960 }
1961 }
1962 other => panic!("expected cmd subst, got {:?}", other),
1963 }
1964 }
1965 other => panic!("expected assignment, got {:?}", other),
1966 }
1967 }
1968
1969 #[test]
1970 fn parse_deeply_nested_cmd_subst() {
1971 let result = parse("X=$(a $(b $(c)))").unwrap();
1973 match &result.statements[0] {
1974 Stmt::Assignment(a) => match &a.value {
1975 Expr::CommandSubst(level1) => {
1976 assert_eq!(level1.commands[0].name, "a");
1977 match &level1.commands[0].args[0] {
1978 Arg::Positional(Expr::CommandSubst(level2)) => {
1979 assert_eq!(level2.commands[0].name, "b");
1980 match &level2.commands[0].args[0] {
1981 Arg::Positional(Expr::CommandSubst(level3)) => {
1982 assert_eq!(level3.commands[0].name, "c");
1983 }
1984 other => panic!("expected level3 cmd subst, got {:?}", other),
1985 }
1986 }
1987 other => panic!("expected level2 cmd subst, got {:?}", other),
1988 }
1989 }
1990 other => panic!("expected cmd subst, got {:?}", other),
1991 },
1992 other => panic!("expected assignment, got {:?}", other),
1993 }
1994 }
1995
1996 #[test]
2001 fn value_int_preserved() {
2002 let result = parse("X=42").unwrap();
2003 match &result.statements[0] {
2004 Stmt::Assignment(a) => {
2005 assert_eq!(a.name, "X");
2006 match &a.value {
2007 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2008 other => panic!("expected int literal, got {:?}", other),
2009 }
2010 }
2011 other => panic!("expected assignment, got {:?}", other),
2012 }
2013 }
2014
2015 #[test]
2016 fn value_negative_int_preserved() {
2017 let result = parse("X=-99").unwrap();
2018 match &result.statements[0] {
2019 Stmt::Assignment(a) => match &a.value {
2020 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
2021 other => panic!("expected int, got {:?}", other),
2022 },
2023 other => panic!("expected assignment, got {:?}", other),
2024 }
2025 }
2026
2027 #[test]
2028 fn value_float_preserved() {
2029 let result = parse("PI=3.14").unwrap();
2030 match &result.statements[0] {
2031 Stmt::Assignment(a) => match &a.value {
2032 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
2033 other => panic!("expected float, got {:?}", other),
2034 },
2035 other => panic!("expected assignment, got {:?}", other),
2036 }
2037 }
2038
2039 #[test]
2040 fn value_string_preserved() {
2041 let result = parse(r#"echo "hello world""#).unwrap();
2042 match &result.statements[0] {
2043 Stmt::Command(cmd) => {
2044 assert_eq!(cmd.name, "echo");
2045 match &cmd.args[0] {
2046 Arg::Positional(Expr::Literal(Value::String(s))) => {
2047 assert_eq!(s, "hello world");
2048 }
2049 other => panic!("expected string arg, got {:?}", other),
2050 }
2051 }
2052 other => panic!("expected command, got {:?}", other),
2053 }
2054 }
2055
2056 #[test]
2057 fn value_string_with_escapes_preserved() {
2058 let result = parse(r#"echo "line1\nline2""#).unwrap();
2059 match &result.statements[0] {
2060 Stmt::Command(cmd) => match &cmd.args[0] {
2061 Arg::Positional(Expr::Literal(Value::String(s))) => {
2062 assert_eq!(s, "line1\nline2");
2063 }
2064 other => panic!("expected string, got {:?}", other),
2065 },
2066 other => panic!("expected command, got {:?}", other),
2067 }
2068 }
2069
2070 #[test]
2071 fn value_command_name_preserved() {
2072 let result = parse("my-command").unwrap();
2073 match &result.statements[0] {
2074 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
2075 other => panic!("expected command, got {:?}", other),
2076 }
2077 }
2078
2079 #[test]
2080 fn value_assignment_name_preserved() {
2081 let result = parse("MY_VAR=1").unwrap();
2082 match &result.statements[0] {
2083 Stmt::Assignment(a) => assert_eq!(a.name, "MY_VAR"),
2084 other => panic!("expected assignment, got {:?}", other),
2085 }
2086 }
2087
2088 #[test]
2089 fn value_for_variable_preserved() {
2090 let result = parse("for ITEM in items; do echo; done").unwrap();
2091 match &result.statements[0] {
2092 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
2093 other => panic!("expected for, got {:?}", other),
2094 }
2095 }
2096
2097 #[test]
2098 fn value_varref_name_preserved() {
2099 let result = parse("echo ${MESSAGE}").unwrap();
2100 match &result.statements[0] {
2101 Stmt::Command(cmd) => match &cmd.args[0] {
2102 Arg::Positional(Expr::VarRef(path)) => {
2103 assert_eq!(path.segments.len(), 1);
2104 let VarSegment::Field(name) = &path.segments[0];
2105 assert_eq!(name, "MESSAGE");
2106 }
2107 other => panic!("expected varref, got {:?}", other),
2108 },
2109 other => panic!("expected command, got {:?}", other),
2110 }
2111 }
2112
2113 #[test]
2114 fn value_varref_field_access_preserved() {
2115 let result = parse("echo ${RESULT.data}").unwrap();
2116 match &result.statements[0] {
2117 Stmt::Command(cmd) => match &cmd.args[0] {
2118 Arg::Positional(Expr::VarRef(path)) => {
2119 assert_eq!(path.segments.len(), 2);
2120 let VarSegment::Field(a) = &path.segments[0];
2121 let VarSegment::Field(b) = &path.segments[1];
2122 assert_eq!(a, "RESULT");
2123 assert_eq!(b, "data");
2124 }
2125 other => panic!("expected varref, got {:?}", other),
2126 },
2127 other => panic!("expected command, got {:?}", other),
2128 }
2129 }
2130
2131 #[test]
2132 fn value_varref_index_ignored() {
2133 let result = parse("echo ${ITEMS[0]}").unwrap();
2135 match &result.statements[0] {
2136 Stmt::Command(cmd) => match &cmd.args[0] {
2137 Arg::Positional(Expr::VarRef(path)) => {
2138 assert_eq!(path.segments.len(), 1);
2140 let VarSegment::Field(name) = &path.segments[0];
2141 assert_eq!(name, "ITEMS");
2142 }
2143 other => panic!("expected varref, got {:?}", other),
2144 },
2145 other => panic!("expected command, got {:?}", other),
2146 }
2147 }
2148
2149 #[test]
2150 fn value_last_result_ref_preserved() {
2151 let result = parse("echo ${?.ok}").unwrap();
2152 match &result.statements[0] {
2153 Stmt::Command(cmd) => match &cmd.args[0] {
2154 Arg::Positional(Expr::VarRef(path)) => {
2155 assert_eq!(path.segments.len(), 2);
2156 let VarSegment::Field(name) = &path.segments[0];
2157 assert_eq!(name, "?");
2158 }
2159 other => panic!("expected varref, got {:?}", other),
2160 },
2161 other => panic!("expected command, got {:?}", other),
2162 }
2163 }
2164
2165 #[test]
2166 fn value_named_arg_preserved() {
2167 let result = parse("cmd count=42").unwrap();
2168 match &result.statements[0] {
2169 Stmt::Command(cmd) => {
2170 assert_eq!(cmd.name, "cmd");
2171 match &cmd.args[0] {
2172 Arg::Named { key, value } => {
2173 assert_eq!(key, "count");
2174 match value {
2175 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2176 other => panic!("expected int, got {:?}", other),
2177 }
2178 }
2179 other => panic!("expected named arg, got {:?}", other),
2180 }
2181 }
2182 other => panic!("expected command, got {:?}", other),
2183 }
2184 }
2185
2186 #[test]
2187 fn value_function_def_name_preserved() {
2188 let result = parse("greet() { echo }").unwrap();
2189 match &result.statements[0] {
2190 Stmt::ToolDef(t) => {
2191 assert_eq!(t.name, "greet");
2192 assert!(t.params.is_empty());
2193 }
2194 other => panic!("expected function def, got {:?}", other),
2195 }
2196 }
2197
2198 #[test]
2203 fn parse_comparison_equals() {
2204 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
2206 match &result.statements[0] {
2207 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2208 Expr::Test(test) => match test.as_ref() {
2209 TestExpr::Comparison { left, op, right } => {
2210 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2211 assert_eq!(*op, TestCmpOp::Eq);
2212 match right.as_ref() {
2213 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
2214 other => panic!("expected int, got {:?}", other),
2215 }
2216 }
2217 other => panic!("expected comparison, got {:?}", other),
2218 },
2219 other => panic!("expected test expr, got {:?}", other),
2220 },
2221 other => panic!("expected if, got {:?}", other),
2222 }
2223 }
2224
2225 #[test]
2226 fn parse_comparison_not_equals() {
2227 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
2228 match &result.statements[0] {
2229 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2230 Expr::Test(test) => match test.as_ref() {
2231 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
2232 other => panic!("expected comparison, got {:?}", other),
2233 },
2234 other => panic!("expected test expr, got {:?}", other),
2235 },
2236 other => panic!("expected if, got {:?}", other),
2237 }
2238 }
2239
2240 #[test]
2241 fn parse_comparison_less_than() {
2242 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
2243 match &result.statements[0] {
2244 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2245 Expr::Test(test) => match test.as_ref() {
2246 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Lt),
2247 other => panic!("expected comparison, got {:?}", other),
2248 },
2249 other => panic!("expected test expr, got {:?}", other),
2250 },
2251 other => panic!("expected if, got {:?}", other),
2252 }
2253 }
2254
2255 #[test]
2256 fn parse_comparison_greater_than() {
2257 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
2258 match &result.statements[0] {
2259 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2260 Expr::Test(test) => match test.as_ref() {
2261 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Gt),
2262 other => panic!("expected comparison, got {:?}", other),
2263 },
2264 other => panic!("expected test expr, got {:?}", other),
2265 },
2266 other => panic!("expected if, got {:?}", other),
2267 }
2268 }
2269
2270 #[test]
2271 fn parse_comparison_less_equal() {
2272 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
2273 match &result.statements[0] {
2274 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2275 Expr::Test(test) => match test.as_ref() {
2276 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::LtEq),
2277 other => panic!("expected comparison, got {:?}", other),
2278 },
2279 other => panic!("expected test expr, got {:?}", other),
2280 },
2281 other => panic!("expected if, got {:?}", other),
2282 }
2283 }
2284
2285 #[test]
2286 fn parse_comparison_greater_equal() {
2287 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
2288 match &result.statements[0] {
2289 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2290 Expr::Test(test) => match test.as_ref() {
2291 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::GtEq),
2292 other => panic!("expected comparison, got {:?}", other),
2293 },
2294 other => panic!("expected test expr, got {:?}", other),
2295 },
2296 other => panic!("expected if, got {:?}", other),
2297 }
2298 }
2299
2300 #[test]
2301 fn parse_regex_match() {
2302 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
2303 match &result.statements[0] {
2304 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2305 Expr::Test(test) => match test.as_ref() {
2306 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
2307 other => panic!("expected comparison, got {:?}", other),
2308 },
2309 other => panic!("expected test expr, got {:?}", other),
2310 },
2311 other => panic!("expected if, got {:?}", other),
2312 }
2313 }
2314
2315 #[test]
2316 fn parse_regex_not_match() {
2317 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
2318 match &result.statements[0] {
2319 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2320 Expr::Test(test) => match test.as_ref() {
2321 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
2322 other => panic!("expected comparison, got {:?}", other),
2323 },
2324 other => panic!("expected test expr, got {:?}", other),
2325 },
2326 other => panic!("expected if, got {:?}", other),
2327 }
2328 }
2329
2330 #[test]
2331 fn parse_string_interpolation() {
2332 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
2333 match &result.statements[0] {
2334 Stmt::Command(cmd) => match &cmd.args[0] {
2335 Arg::Positional(Expr::Interpolated(parts)) => {
2336 assert_eq!(parts.len(), 3);
2337 match &parts[0] {
2338 StringPart::Literal(s) => assert_eq!(s, "Hello "),
2339 other => panic!("expected literal, got {:?}", other),
2340 }
2341 match &parts[1] {
2342 StringPart::Var(path) => {
2343 assert_eq!(path.segments.len(), 1);
2344 let VarSegment::Field(name) = &path.segments[0];
2345 assert_eq!(name, "NAME");
2346 }
2347 other => panic!("expected var, got {:?}", other),
2348 }
2349 match &parts[2] {
2350 StringPart::Literal(s) => assert_eq!(s, "!"),
2351 other => panic!("expected literal, got {:?}", other),
2352 }
2353 }
2354 other => panic!("expected interpolated, got {:?}", other),
2355 },
2356 other => panic!("expected command, got {:?}", other),
2357 }
2358 }
2359
2360 #[test]
2361 fn parse_string_interpolation_multiple_vars() {
2362 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
2363 match &result.statements[0] {
2364 Stmt::Command(cmd) => match &cmd.args[0] {
2365 Arg::Positional(Expr::Interpolated(parts)) => {
2366 assert_eq!(parts.len(), 3);
2368 assert!(matches!(&parts[0], StringPart::Var(_)));
2369 assert!(matches!(&parts[1], StringPart::Literal(_)));
2370 assert!(matches!(&parts[2], StringPart::Var(_)));
2371 }
2372 other => panic!("expected interpolated, got {:?}", other),
2373 },
2374 other => panic!("expected command, got {:?}", other),
2375 }
2376 }
2377
2378 #[test]
2379 fn parse_empty_function_body() {
2380 let result = parse("empty() { }").unwrap();
2381 match &result.statements[0] {
2382 Stmt::ToolDef(t) => {
2383 assert_eq!(t.name, "empty");
2384 assert!(t.params.is_empty());
2385 assert!(t.body.is_empty());
2386 }
2387 other => panic!("expected function def, got {:?}", other),
2388 }
2389 }
2390
2391 #[test]
2392 fn parse_bash_style_function() {
2393 let result = parse("function greet { echo hello }").unwrap();
2394 match &result.statements[0] {
2395 Stmt::ToolDef(t) => {
2396 assert_eq!(t.name, "greet");
2397 assert!(t.params.is_empty());
2398 assert_eq!(t.body.len(), 1);
2399 }
2400 other => panic!("expected function def, got {:?}", other),
2401 }
2402 }
2403
2404 #[test]
2405 fn parse_comparison_string_values() {
2406 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
2407 match &result.statements[0] {
2408 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2409 Expr::Test(test) => match test.as_ref() {
2410 TestExpr::Comparison { left, op, right } => {
2411 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2412 assert_eq!(*op, TestCmpOp::Eq);
2413 match right.as_ref() {
2414 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
2415 other => panic!("expected string, got {:?}", other),
2416 }
2417 }
2418 other => panic!("expected comparison, got {:?}", other),
2419 },
2420 other => panic!("expected test expr, got {:?}", other),
2421 },
2422 other => panic!("expected if, got {:?}", other),
2423 }
2424 }
2425
2426 #[test]
2431 fn parse_cmd_subst_simple() {
2432 let result = parse("X=$(echo)").unwrap();
2433 match &result.statements[0] {
2434 Stmt::Assignment(a) => {
2435 assert_eq!(a.name, "X");
2436 match &a.value {
2437 Expr::CommandSubst(pipeline) => {
2438 assert_eq!(pipeline.commands.len(), 1);
2439 assert_eq!(pipeline.commands[0].name, "echo");
2440 }
2441 other => panic!("expected command subst, got {:?}", other),
2442 }
2443 }
2444 other => panic!("expected assignment, got {:?}", other),
2445 }
2446 }
2447
2448 #[test]
2449 fn parse_cmd_subst_with_args() {
2450 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
2451 match &result.statements[0] {
2452 Stmt::Assignment(a) => match &a.value {
2453 Expr::CommandSubst(pipeline) => {
2454 assert_eq!(pipeline.commands[0].name, "fetch");
2455 assert_eq!(pipeline.commands[0].args.len(), 1);
2456 match &pipeline.commands[0].args[0] {
2457 Arg::Named { key, .. } => assert_eq!(key, "url"),
2458 other => panic!("expected named arg, got {:?}", other),
2459 }
2460 }
2461 other => panic!("expected command subst, got {:?}", other),
2462 },
2463 other => panic!("expected assignment, got {:?}", other),
2464 }
2465 }
2466
2467 #[test]
2468 fn parse_cmd_subst_pipeline() {
2469 let result = parse("X=$(cat file | grep pattern)").unwrap();
2470 match &result.statements[0] {
2471 Stmt::Assignment(a) => match &a.value {
2472 Expr::CommandSubst(pipeline) => {
2473 assert_eq!(pipeline.commands.len(), 2);
2474 assert_eq!(pipeline.commands[0].name, "cat");
2475 assert_eq!(pipeline.commands[1].name, "grep");
2476 }
2477 other => panic!("expected command subst, got {:?}", other),
2478 },
2479 other => panic!("expected assignment, got {:?}", other),
2480 }
2481 }
2482
2483 #[test]
2484 fn parse_cmd_subst_in_condition() {
2485 let result = parse("if kaish-validate; then echo; fi").unwrap();
2487 match &result.statements[0] {
2488 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2489 Expr::Command(cmd) => {
2490 assert_eq!(cmd.name, "kaish-validate");
2491 }
2492 other => panic!("expected command, got {:?}", other),
2493 },
2494 other => panic!("expected if, got {:?}", other),
2495 }
2496 }
2497
2498 #[test]
2499 fn parse_cmd_subst_in_command_arg() {
2500 let result = parse("echo $(whoami)").unwrap();
2501 match &result.statements[0] {
2502 Stmt::Command(cmd) => {
2503 assert_eq!(cmd.name, "echo");
2504 match &cmd.args[0] {
2505 Arg::Positional(Expr::CommandSubst(pipeline)) => {
2506 assert_eq!(pipeline.commands[0].name, "whoami");
2507 }
2508 other => panic!("expected command subst, got {:?}", other),
2509 }
2510 }
2511 other => panic!("expected command, got {:?}", other),
2512 }
2513 }
2514
2515 #[test]
2520 fn parse_condition_and() {
2521 let result = parse("if check-a && check-b; then echo; fi").unwrap();
2523 match &result.statements[0] {
2524 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2525 Expr::BinaryOp { left, op, right } => {
2526 assert_eq!(*op, BinaryOp::And);
2527 assert!(matches!(left.as_ref(), Expr::Command(_)));
2528 assert!(matches!(right.as_ref(), Expr::Command(_)));
2529 }
2530 other => panic!("expected binary op, got {:?}", other),
2531 },
2532 other => panic!("expected if, got {:?}", other),
2533 }
2534 }
2535
2536 #[test]
2537 fn parse_condition_or() {
2538 let result = parse("if try-a || try-b; then echo; fi").unwrap();
2539 match &result.statements[0] {
2540 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2541 Expr::BinaryOp { left, op, right } => {
2542 assert_eq!(*op, BinaryOp::Or);
2543 assert!(matches!(left.as_ref(), Expr::Command(_)));
2544 assert!(matches!(right.as_ref(), Expr::Command(_)));
2545 }
2546 other => panic!("expected binary op, got {:?}", other),
2547 },
2548 other => panic!("expected if, got {:?}", other),
2549 }
2550 }
2551
2552 #[test]
2553 fn parse_condition_and_or_precedence() {
2554 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
2556 match &result.statements[0] {
2557 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2558 Expr::BinaryOp { left, op, right } => {
2559 assert_eq!(*op, BinaryOp::Or);
2561 match left.as_ref() {
2563 Expr::BinaryOp { op: inner_op, .. } => {
2564 assert_eq!(*inner_op, BinaryOp::And);
2565 }
2566 other => panic!("expected binary op (&&), got {:?}", other),
2567 }
2568 assert!(matches!(right.as_ref(), Expr::Command(_)));
2570 }
2571 other => panic!("expected binary op, got {:?}", other),
2572 },
2573 other => panic!("expected if, got {:?}", other),
2574 }
2575 }
2576
2577 #[test]
2578 fn parse_condition_multiple_and() {
2579 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
2580 match &result.statements[0] {
2581 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2582 Expr::BinaryOp { left, op, .. } => {
2583 assert_eq!(*op, BinaryOp::And);
2584 match left.as_ref() {
2586 Expr::BinaryOp { op: inner_op, .. } => {
2587 assert_eq!(*inner_op, BinaryOp::And);
2588 }
2589 other => panic!("expected binary op, got {:?}", other),
2590 }
2591 }
2592 other => panic!("expected binary op, got {:?}", other),
2593 },
2594 other => panic!("expected if, got {:?}", other),
2595 }
2596 }
2597
2598 #[test]
2599 fn parse_condition_mixed_comparison_and_logical() {
2600 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
2602 match &result.statements[0] {
2603 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2604 Expr::BinaryOp { left, op, right } => {
2605 assert_eq!(*op, BinaryOp::And);
2606 match left.as_ref() {
2608 Expr::Test(test) => match test.as_ref() {
2609 TestExpr::Comparison { op: left_op, .. } => {
2610 assert_eq!(*left_op, TestCmpOp::Eq);
2611 }
2612 other => panic!("expected comparison, got {:?}", other),
2613 },
2614 other => panic!("expected test, got {:?}", other),
2615 }
2616 match right.as_ref() {
2618 Expr::Test(test) => match test.as_ref() {
2619 TestExpr::Comparison { op: right_op, .. } => {
2620 assert_eq!(*right_op, TestCmpOp::Gt);
2621 }
2622 other => panic!("expected comparison, got {:?}", other),
2623 },
2624 other => panic!("expected test, got {:?}", other),
2625 }
2626 }
2627 other => panic!("expected binary op, got {:?}", other),
2628 },
2629 other => panic!("expected if, got {:?}", other),
2630 }
2631 }
2632
2633 #[test]
2639 fn script_level1_linear() {
2640 let script = r#"
2641NAME="kaish"
2642VERSION=1
2643TIMEOUT=30
2644ITEMS="alpha beta gamma"
2645
2646echo "Starting ${NAME} v${VERSION}"
2647cat "README.md" | grep pattern="install" | head count=5
2648fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
2649echo "Items: ${ITEMS}"
2650"#;
2651 let result = parse(script).unwrap();
2652 let stmts: Vec<_> = result.statements.iter()
2653 .filter(|s| !matches!(s, Stmt::Empty))
2654 .collect();
2655
2656 assert_eq!(stmts.len(), 8);
2657 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(_))); }
2666
2667 #[test]
2669 fn script_level2_branching() {
2670 let script = r#"
2671RESULT=$(kaish-validate "input.json")
2672
2673if [[ ${RESULT.ok} == true ]]; then
2674 echo "Validation passed"
2675 process "input.json" > "output.json"
2676else
2677 echo "Validation failed: ${RESULT.err}"
2678fi
2679
2680if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
2681 echo "Count in valid range"
2682fi
2683
2684if check-network || check-cache; then
2685 fetch url=${URL}
2686fi
2687"#;
2688 let result = parse(script).unwrap();
2689 let stmts: Vec<_> = result.statements.iter()
2690 .filter(|s| !matches!(s, Stmt::Empty))
2691 .collect();
2692
2693 assert_eq!(stmts.len(), 4);
2694
2695 match stmts[0] {
2697 Stmt::Assignment(a) => {
2698 assert_eq!(a.name, "RESULT");
2699 assert!(matches!(&a.value, Expr::CommandSubst(_)));
2700 }
2701 other => panic!("expected assignment, got {:?}", other),
2702 }
2703
2704 match stmts[1] {
2706 Stmt::If(if_stmt) => {
2707 assert_eq!(if_stmt.then_branch.len(), 2);
2708 assert!(if_stmt.else_branch.is_some());
2709 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
2710 }
2711 other => panic!("expected if, got {:?}", other),
2712 }
2713
2714 match stmts[2] {
2716 Stmt::If(if_stmt) => {
2717 match if_stmt.condition.as_ref() {
2718 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
2719 other => panic!("expected && condition, got {:?}", other),
2720 }
2721 }
2722 other => panic!("expected if, got {:?}", other),
2723 }
2724
2725 match stmts[3] {
2727 Stmt::If(if_stmt) => {
2728 match if_stmt.condition.as_ref() {
2729 Expr::BinaryOp { op, left, right } => {
2730 assert_eq!(*op, BinaryOp::Or);
2731 assert!(matches!(left.as_ref(), Expr::Command(_)));
2732 assert!(matches!(right.as_ref(), Expr::Command(_)));
2733 }
2734 other => panic!("expected || condition, got {:?}", other),
2735 }
2736 }
2737 other => panic!("expected if, got {:?}", other),
2738 }
2739 }
2740
2741 #[test]
2743 fn script_level3_loops_and_functions() {
2744 let script = r#"
2745greet() {
2746 echo "Hello, $1!"
2747}
2748
2749fetch_all() {
2750 for URL in $@; do
2751 fetch url=${URL}
2752 done
2753}
2754
2755USERS="alice bob charlie"
2756
2757for USER in ${USERS}; do
2758 greet ${USER}
2759 if [[ ${USER} == "bob" ]]; then
2760 echo "Found Bob!"
2761 fi
2762done
2763
2764long-running-task &
2765"#;
2766 let result = parse(script).unwrap();
2767 let stmts: Vec<_> = result.statements.iter()
2768 .filter(|s| !matches!(s, Stmt::Empty))
2769 .collect();
2770
2771 assert_eq!(stmts.len(), 5);
2772
2773 match stmts[0] {
2775 Stmt::ToolDef(t) => {
2776 assert_eq!(t.name, "greet");
2777 assert!(t.params.is_empty());
2778 }
2779 other => panic!("expected function def, got {:?}", other),
2780 }
2781
2782 match stmts[1] {
2784 Stmt::ToolDef(t) => {
2785 assert_eq!(t.name, "fetch_all");
2786 assert_eq!(t.body.len(), 1);
2787 assert!(matches!(&t.body[0], Stmt::For(_)));
2788 }
2789 other => panic!("expected function def, got {:?}", other),
2790 }
2791
2792 assert!(matches!(stmts[2], Stmt::Assignment(_)));
2794
2795 match stmts[3] {
2797 Stmt::For(f) => {
2798 assert_eq!(f.variable, "USER");
2799 assert_eq!(f.body.len(), 2);
2800 assert!(matches!(&f.body[0], Stmt::Command(_)));
2801 assert!(matches!(&f.body[1], Stmt::If(_)));
2802 }
2803 other => panic!("expected for loop, got {:?}", other),
2804 }
2805
2806 match stmts[4] {
2808 Stmt::Pipeline(p) => {
2809 assert!(p.background);
2810 assert_eq!(p.commands[0].name, "long-running-task");
2811 }
2812 other => panic!("expected pipeline (background), got {:?}", other),
2813 }
2814 }
2815
2816 #[test]
2818 fn script_level4_complex_nesting() {
2819 let script = r#"
2820RESULT=$(cat "config.json" | jq query=".servers" | kaish-validate schema="server-schema.json")
2821
2822if ping host=${HOST} && [[ ${RESULT} == true ]]; then
2823 for SERVER in "prod-1 prod-2"; do
2824 deploy target=${SERVER} port=8080
2825 if [[ ${?.code} != 0 ]]; then
2826 notify channel="ops" message="Deploy failed"
2827 fi
2828 done
2829fi
2830"#;
2831 let result = parse(script).unwrap();
2832 let stmts: Vec<_> = result.statements.iter()
2833 .filter(|s| !matches!(s, Stmt::Empty))
2834 .collect();
2835
2836 assert_eq!(stmts.len(), 2);
2837
2838 match stmts[0] {
2840 Stmt::Assignment(a) => {
2841 assert_eq!(a.name, "RESULT");
2842 match &a.value {
2843 Expr::CommandSubst(pipeline) => {
2844 assert_eq!(pipeline.commands.len(), 3);
2845 }
2846 other => panic!("expected command subst, got {:?}", other),
2847 }
2848 }
2849 other => panic!("expected assignment, got {:?}", other),
2850 }
2851
2852 match stmts[1] {
2854 Stmt::If(if_stmt) => {
2855 match if_stmt.condition.as_ref() {
2856 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
2857 other => panic!("expected && condition, got {:?}", other),
2858 }
2859 assert_eq!(if_stmt.then_branch.len(), 1);
2860 match &if_stmt.then_branch[0] {
2861 Stmt::For(f) => {
2862 assert_eq!(f.body.len(), 2);
2863 assert!(matches!(&f.body[1], Stmt::If(_)));
2864 }
2865 other => panic!("expected for in if body, got {:?}", other),
2866 }
2867 }
2868 other => panic!("expected if, got {:?}", other),
2869 }
2870 }
2871
2872 #[test]
2874 fn script_level5_edge_cases() {
2875 let script = r#"
2876echo ""
2877echo "quotes: \"nested\" here"
2878echo "escapes: \n\t\r\\"
2879echo "unicode: \u2764"
2880
2881X=-99999
2882Y=3.14159265358979
2883Z=-0.001
2884
2885cmd a=1 b="two" c=true d=false e=null
2886
2887if true; then
2888 if false; then
2889 echo "inner"
2890 else
2891 echo "else"
2892 fi
2893fi
2894
2895for I in "a b c"; do
2896 echo ${I}
2897done
2898
2899no_params() {
2900 echo "no params"
2901}
2902
2903function all_args {
2904 echo "args: $@"
2905}
2906
2907a | b | c | d | e &
2908cmd 2> "errors.log"
2909cmd &> "all.log"
2910cmd >> "append.log"
2911cmd < "input.txt"
2912"#;
2913 let result = parse(script).unwrap();
2914 let stmts: Vec<_> = result.statements.iter()
2915 .filter(|s| !matches!(s, Stmt::Empty))
2916 .collect();
2917
2918 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
2920
2921 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
2923 assert!(bg_stmt.is_some(), "expected background pipeline");
2924
2925 match bg_stmt.unwrap() {
2926 Stmt::Pipeline(p) => {
2927 assert_eq!(p.commands.len(), 5);
2928 assert!(p.background);
2929 }
2930 _ => unreachable!(),
2931 }
2932 }
2933
2934 #[test]
2939 fn parse_keyword_as_variable_rejected() {
2940 let result = parse(r#"if="value""#);
2943 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
2944
2945 let result = parse("while=true");
2946 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
2947
2948 let result = parse(r#"then="next""#);
2949 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
2950 }
2951
2952 #[test]
2953 fn parse_set_command_with_flag() {
2954 let result = parse("set -e");
2955 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
2956 let program = result.unwrap();
2957 match &program.statements[0] {
2958 Stmt::Command(cmd) => {
2959 assert_eq!(cmd.name, "set");
2960 assert_eq!(cmd.args.len(), 1);
2961 match &cmd.args[0] {
2962 Arg::ShortFlag(f) => assert_eq!(f, "e"),
2963 other => panic!("expected ShortFlag, got {:?}", other),
2964 }
2965 }
2966 other => panic!("expected Command, got {:?}", other),
2967 }
2968 }
2969
2970 #[test]
2971 fn parse_set_command_no_args() {
2972 let result = parse("set");
2973 assert!(result.is_ok(), "failed to parse set: {:?}", result);
2974 let program = result.unwrap();
2975 match &program.statements[0] {
2976 Stmt::Command(cmd) => {
2977 assert_eq!(cmd.name, "set");
2978 assert_eq!(cmd.args.len(), 0);
2979 }
2980 other => panic!("expected Command, got {:?}", other),
2981 }
2982 }
2983
2984 #[test]
2985 fn parse_set_assignment_vs_command() {
2986 let result = parse("X=5");
2988 assert!(result.is_ok());
2989 let program = result.unwrap();
2990 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2991
2992 let result = parse("set -e");
2994 assert!(result.is_ok());
2995 let program = result.unwrap();
2996 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2997 }
2998
2999 #[test]
3000 fn parse_true_as_command() {
3001 let result = parse("true");
3002 assert!(result.is_ok());
3003 let program = result.unwrap();
3004 match &program.statements[0] {
3005 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
3006 other => panic!("expected Command(true), got {:?}", other),
3007 }
3008 }
3009
3010 #[test]
3011 fn parse_false_as_command() {
3012 let result = parse("false");
3013 assert!(result.is_ok());
3014 let program = result.unwrap();
3015 match &program.statements[0] {
3016 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
3017 other => panic!("expected Command(false), got {:?}", other),
3018 }
3019 }
3020
3021 #[test]
3022 fn parse_dot_as_source_alias() {
3023 let result = parse(". script.kai");
3024 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
3025 let program = result.unwrap();
3026 match &program.statements[0] {
3027 Stmt::Command(cmd) => {
3028 assert_eq!(cmd.name, ".");
3029 assert_eq!(cmd.args.len(), 1);
3030 }
3031 other => panic!("expected Command(.), got {:?}", other),
3032 }
3033 }
3034
3035 #[test]
3036 fn parse_source_command() {
3037 let result = parse("source utils.kai");
3038 assert!(result.is_ok(), "failed to parse source: {:?}", result);
3039 let program = result.unwrap();
3040 match &program.statements[0] {
3041 Stmt::Command(cmd) => {
3042 assert_eq!(cmd.name, "source");
3043 assert_eq!(cmd.args.len(), 1);
3044 }
3045 other => panic!("expected Command(source), got {:?}", other),
3046 }
3047 }
3048
3049 #[test]
3050 fn parse_test_expr_file_test() {
3051 let result = parse(r#"[[ -f "/path/file" ]]"#);
3053 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
3054 }
3055
3056 #[test]
3057 fn parse_test_expr_comparison() {
3058 let result = parse(r#"[[ $X == "value" ]]"#);
3059 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
3060 }
3061
3062 #[test]
3063 fn parse_test_expr_single_eq() {
3064 let result = parse(r#"[[ $X = "value" ]]"#);
3066 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
3067 let program = result.unwrap();
3068 match &program.statements[0] {
3069 Stmt::Test(TestExpr::Comparison { op, .. }) => {
3070 assert_eq!(op, &TestCmpOp::Eq);
3071 }
3072 other => panic!("expected Test(Comparison), got {:?}", other),
3073 }
3074 }
3075
3076 #[test]
3077 fn parse_while_loop() {
3078 let result = parse("while true; do echo; done");
3079 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
3080 let program = result.unwrap();
3081 assert!(matches!(&program.statements[0], Stmt::While(_)));
3082 }
3083
3084 #[test]
3085 fn parse_break_with_level() {
3086 let result = parse("break 2");
3087 assert!(result.is_ok());
3088 let program = result.unwrap();
3089 match &program.statements[0] {
3090 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
3091 other => panic!("expected Break(2), got {:?}", other),
3092 }
3093 }
3094
3095 #[test]
3096 fn parse_continue_with_level() {
3097 let result = parse("continue 3");
3098 assert!(result.is_ok());
3099 let program = result.unwrap();
3100 match &program.statements[0] {
3101 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
3102 other => panic!("expected Continue(3), got {:?}", other),
3103 }
3104 }
3105
3106 #[test]
3107 fn parse_exit_with_code() {
3108 let result = parse("exit 1");
3109 assert!(result.is_ok());
3110 let program = result.unwrap();
3111 match &program.statements[0] {
3112 Stmt::Exit(Some(expr)) => {
3113 match expr.as_ref() {
3114 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
3115 other => panic!("expected Int(1), got {:?}", other),
3116 }
3117 }
3118 other => panic!("expected Exit(1), got {:?}", other),
3119 }
3120 }
3121}