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::Ident(s) => s },
829 select! { Token::String(s) => s },
830 select! { Token::SingleString(s) => s },
831 select! { Token::Int(n) => n.to_string() },
832 select! { Token::Star => "*".to_string() },
833 select! { Token::Question => "?".to_string() },
834 select! { Token::Dot => ".".to_string() },
835 select! { Token::DotDot => "..".to_string() },
836 select! { Token::Tilde => "~".to_string() },
837 select! { Token::TildePath(s) => s },
838 select! { Token::RelativePath(s) => s },
839 select! { Token::DotSlashPath(s) => s },
840 select! { Token::Path(p) => p },
841 select! { Token::VarRef(v) => v },
842 select! { Token::SimpleVarRef(v) => format!("${}", v) },
843 just(Token::LBracket)
845 .ignore_then(
846 choice((
847 select! { Token::Ident(s) => s },
848 select! { Token::Int(n) => n.to_string() },
849 just(Token::Colon).to(":".to_string()),
850 just(Token::Bang).to("!".to_string()),
852 select! { Token::ShortFlag(s) => format!("-{}", s) },
854 ))
855 .repeated()
856 .at_least(1)
857 .collect::<Vec<String>>()
858 )
859 .then_ignore(just(Token::RBracket))
860 .map(|parts| format!("[{}]", parts.join(""))),
861 just(Token::LBrace)
863 .ignore_then(
864 choice((
865 select! { Token::Ident(s) => s },
866 select! { Token::Int(n) => n.to_string() },
867 ))
868 .separated_by(just(Token::Comma))
869 .at_least(1)
870 .collect::<Vec<String>>()
871 )
872 .then_ignore(just(Token::RBrace))
873 .map(|parts| format!("{{{}}}", parts.join(","))),
874 ));
875
876 let pattern = pattern_part
879 .repeated()
880 .at_least(1)
881 .collect::<Vec<String>>()
882 .map(|parts| parts.join(""))
883 .labelled("case pattern");
884
885 let patterns = pattern
887 .separated_by(just(Token::Pipe))
888 .at_least(1)
889 .collect::<Vec<String>>()
890 .labelled("case patterns");
891
892 let branch = just(Token::LParen)
894 .or_not()
895 .ignore_then(just(Token::Newline).repeated())
896 .ignore_then(patterns)
897 .then_ignore(just(Token::RParen))
898 .then_ignore(just(Token::Newline).repeated())
899 .then(
900 stmt.clone()
901 .repeated()
902 .collect::<Vec<_>>()
903 .map(|stmts| stmts.into_iter().filter(|s| !matches!(s, Stmt::Empty)).collect()),
904 )
905 .then_ignore(just(Token::DoubleSemi))
906 .then_ignore(just(Token::Newline).repeated())
907 .map(|(patterns, body)| CaseBranch { patterns, body })
908 .labelled("case branch");
909
910 just(Token::Case)
911 .ignore_then(expr_parser())
912 .then_ignore(just(Token::In))
913 .then_ignore(just(Token::Newline).repeated())
914 .then(branch.repeated().collect::<Vec<_>>())
915 .then_ignore(just(Token::Esac))
916 .map(|(expr, branches)| CaseStmt { expr, branches })
917 .labelled("case statement")
918 .boxed()
919}
920
921fn pipeline_parser<'tokens, I>(
923) -> impl Parser<'tokens, I, Pipeline, extra::Err<Rich<'tokens, Token, Span>>> + Clone
924where
925 I: ValueInput<'tokens, Token = Token, Span = Span>,
926{
927 command_parser()
928 .separated_by(just(Token::Pipe))
929 .at_least(1)
930 .collect::<Vec<_>>()
931 .then(just(Token::Amp).or_not())
932 .map(|(commands, bg)| Pipeline {
933 commands,
934 background: bg.is_some(),
935 })
936 .labelled("pipeline")
937 .boxed()
938}
939
940fn command_parser<'tokens, I>(
943) -> impl Parser<'tokens, I, Command, extra::Err<Rich<'tokens, Token, Span>>> + Clone
944where
945 I: ValueInput<'tokens, Token = Token, Span = Span>,
946{
947 let command_name = choice((
949 ident_parser(),
950 path_parser(),
951 select! { Token::DotSlashPath(s) => s },
952 just(Token::True).to("true".to_string()),
953 just(Token::False).to("false".to_string()),
954 just(Token::Dot).to(".".to_string()),
955 ));
956
957 command_name
958 .then(args_list_parser())
959 .then(redirect_parser().repeated().collect::<Vec<_>>())
960 .map(|((name, args), redirects)| Command {
961 name,
962 args,
963 redirects,
964 })
965 .labelled("command")
966 .boxed()
967}
968
969fn args_list_parser<'tokens, I>(
973) -> impl Parser<'tokens, I, Vec<Arg>, extra::Err<Rich<'tokens, Token, Span>>> + Clone
974where
975 I: ValueInput<'tokens, Token = Token, Span = Span>,
976{
977 let pre_dash = arg_before_double_dash_parser()
979 .repeated()
980 .collect::<Vec<_>>();
981
982 let double_dash = select! {
984 Token::DoubleDash => Arg::DoubleDash,
985 };
986
987 let post_dash_arg = choice((
989 select! {
991 Token::ShortFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("-{}", name)))),
992 Token::LongFlag(name) => Arg::Positional(Expr::Literal(Value::String(format!("--{}", name)))),
993 },
994 primary_expr_parser().map(Arg::Positional),
996 ));
997
998 let post_dash = post_dash_arg.repeated().collect::<Vec<_>>();
999
1000 pre_dash
1002 .then(double_dash.then(post_dash).or_not())
1003 .map(|(mut args, maybe_dd)| {
1004 if let Some((dd, post)) = maybe_dd {
1005 args.push(dd);
1006 args.extend(post);
1007 }
1008 args
1009 })
1010}
1011
1012fn arg_before_double_dash_parser<'tokens, I>(
1014) -> impl Parser<'tokens, I, Arg, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1015where
1016 I: ValueInput<'tokens, Token = Token, Span = Span>,
1017{
1018 let long_flag_with_value = select! {
1020 Token::LongFlag(name) => name,
1021 }
1022 .then_ignore(just(Token::Eq))
1023 .then(primary_expr_parser())
1024 .map(|(key, value)| Arg::Named { key, value });
1025
1026 let long_flag = select! {
1028 Token::LongFlag(name) => Arg::LongFlag(name),
1029 };
1030
1031 let short_flag = select! {
1033 Token::ShortFlag(name) => Arg::ShortFlag(name),
1034 };
1035
1036 let named = select! {
1039 Token::Ident(s) => s,
1040 }
1041 .map_with(|s, e| -> (String, Span) { (s, e.span()) })
1042 .then(just(Token::Eq).map_with(|_, e| -> Span { e.span() }))
1043 .then(primary_expr_parser().map_with(|expr, e| -> (Expr, Span) { (expr, e.span()) }))
1044 .try_map(|(((key, key_span), eq_span), (value, value_span)): (((String, Span), Span), (Expr, Span)), span| {
1045 if key_span.end != eq_span.start || eq_span.end != value_span.start {
1047 Err(Rich::custom(
1048 span,
1049 "named argument must not have spaces around '=' (use 'key=value' not 'key = value')",
1050 ))
1051 } else {
1052 Ok(Arg::Named { key, value })
1053 }
1054 });
1055
1056 let positional = primary_expr_parser().map(Arg::Positional);
1058
1059 choice((
1062 long_flag_with_value,
1063 long_flag,
1064 short_flag,
1065 named,
1066 positional,
1067 ))
1068 .boxed()
1069}
1070
1071fn redirect_parser<'tokens, I>(
1073) -> impl Parser<'tokens, I, Redirect, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1074where
1075 I: ValueInput<'tokens, Token = Token, Span = Span>,
1076{
1077 let regular_redirect = select! {
1079 Token::GtGt => RedirectKind::StdoutAppend,
1080 Token::Gt => RedirectKind::StdoutOverwrite,
1081 Token::Lt => RedirectKind::Stdin,
1082 Token::Stderr => RedirectKind::Stderr,
1083 Token::Both => RedirectKind::Both,
1084 }
1085 .then(primary_expr_parser())
1086 .map(|(kind, target)| Redirect { kind, target });
1087
1088 let heredoc_redirect = just(Token::HereDocStart)
1092 .ignore_then(select! { Token::HereDoc(data) => data })
1093 .map(|data: HereDocData| {
1094 let target = if data.literal {
1095 Expr::Literal(Value::String(data.content))
1096 } else {
1097 let parts = parse_interpolated_string(&data.content);
1098 if parts.len() == 1 {
1100 if let StringPart::Literal(text) = &parts[0] {
1101 return Redirect {
1102 kind: RedirectKind::HereDoc,
1103 target: Expr::Literal(Value::String(text.clone())),
1104 };
1105 }
1106 }
1107 Expr::Interpolated(parts)
1108 };
1109 Redirect {
1110 kind: RedirectKind::HereDoc,
1111 target,
1112 }
1113 });
1114
1115 let merge_stderr_redirect = just(Token::StderrToStdout)
1117 .map(|_| Redirect {
1118 kind: RedirectKind::MergeStderr,
1119 target: Expr::Literal(Value::Null),
1121 });
1122
1123 let merge_stdout_redirect = choice((
1125 just(Token::StdoutToStderr),
1126 just(Token::StdoutToStderr2),
1127 ))
1128 .map(|_| Redirect {
1129 kind: RedirectKind::MergeStdout,
1130 target: Expr::Literal(Value::Null),
1132 });
1133
1134 choice((heredoc_redirect, merge_stderr_redirect, merge_stdout_redirect, regular_redirect))
1135 .labelled("redirect")
1136 .boxed()
1137}
1138
1139fn test_expr_stmt_parser<'tokens, I>(
1149) -> impl Parser<'tokens, I, TestExpr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1150where
1151 I: ValueInput<'tokens, Token = Token, Span = Span>,
1152{
1153 let file_test_op = select! {
1155 Token::ShortFlag(s) if s == "e" => FileTestOp::Exists,
1156 Token::ShortFlag(s) if s == "f" => FileTestOp::IsFile,
1157 Token::ShortFlag(s) if s == "d" => FileTestOp::IsDir,
1158 Token::ShortFlag(s) if s == "r" => FileTestOp::Readable,
1159 Token::ShortFlag(s) if s == "w" => FileTestOp::Writable,
1160 Token::ShortFlag(s) if s == "x" => FileTestOp::Executable,
1161 };
1162
1163 let string_test_op = select! {
1165 Token::ShortFlag(s) if s == "z" => StringTestOp::IsEmpty,
1166 Token::ShortFlag(s) if s == "n" => StringTestOp::IsNonEmpty,
1167 };
1168
1169 let cmp_op = choice((
1172 just(Token::EqEq).to(TestCmpOp::Eq),
1173 just(Token::Eq).to(TestCmpOp::Eq),
1174 just(Token::NotEq).to(TestCmpOp::NotEq),
1175 just(Token::Match).to(TestCmpOp::Match),
1176 just(Token::NotMatch).to(TestCmpOp::NotMatch),
1177 just(Token::Gt).to(TestCmpOp::Gt),
1178 just(Token::Lt).to(TestCmpOp::Lt),
1179 just(Token::GtEq).to(TestCmpOp::GtEq),
1180 just(Token::LtEq).to(TestCmpOp::LtEq),
1181 select! { Token::ShortFlag(s) if s == "eq" => TestCmpOp::Eq },
1182 select! { Token::ShortFlag(s) if s == "ne" => TestCmpOp::NotEq },
1183 select! { Token::ShortFlag(s) if s == "gt" => TestCmpOp::Gt },
1184 select! { Token::ShortFlag(s) if s == "lt" => TestCmpOp::Lt },
1185 select! { Token::ShortFlag(s) if s == "ge" => TestCmpOp::GtEq },
1186 select! { Token::ShortFlag(s) if s == "le" => TestCmpOp::LtEq },
1187 ));
1188
1189 let file_test = file_test_op
1191 .then(primary_expr_parser())
1192 .map(|(op, path)| TestExpr::FileTest {
1193 op,
1194 path: Box::new(path),
1195 });
1196
1197 let string_test = string_test_op
1199 .then(primary_expr_parser())
1200 .map(|(op, value)| TestExpr::StringTest {
1201 op,
1202 value: Box::new(value),
1203 });
1204
1205 let comparison = primary_expr_parser()
1207 .then(cmp_op)
1208 .then(primary_expr_parser())
1209 .map(|((left, op), right)| TestExpr::Comparison {
1210 left: Box::new(left),
1211 op,
1212 right: Box::new(right),
1213 });
1214
1215 let primary_test = choice((file_test, string_test, comparison));
1217
1218 let compound_test = recursive(|compound| {
1229 let not_expr = just(Token::Bang)
1231 .ignore_then(compound.clone())
1232 .map(|expr| TestExpr::Not { expr: Box::new(expr) });
1233
1234 let unary = choice((not_expr, primary_test.clone()));
1236
1237 let and_expr = unary.clone().foldl(
1239 just(Token::And).ignore_then(unary).repeated(),
1240 |left, right| TestExpr::And {
1241 left: Box::new(left),
1242 right: Box::new(right),
1243 },
1244 );
1245
1246 and_expr.clone().foldl(
1248 just(Token::Or).ignore_then(and_expr).repeated(),
1249 |left, right| TestExpr::Or {
1250 left: Box::new(left),
1251 right: Box::new(right),
1252 },
1253 )
1254 });
1255
1256 just(Token::LBracket)
1259 .then(just(Token::LBracket))
1260 .ignore_then(compound_test)
1261 .then_ignore(just(Token::RBracket).then(just(Token::RBracket)))
1262 .labelled("test expression")
1263 .boxed()
1264}
1265
1266fn condition_parser<'tokens, I>(
1281) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1282where
1283 I: ValueInput<'tokens, Token = Token, Span = Span>,
1284{
1285 let test_expr_condition = test_expr_stmt_parser().map(|test| Expr::Test(Box::new(test)));
1287
1288 let command_condition = command_parser().map(Expr::Command);
1291
1292 let base = choice((test_expr_condition, command_condition));
1294
1295 let and_expr = base.clone().foldl(
1298 just(Token::And).ignore_then(base).repeated(),
1299 |left, right| Expr::BinaryOp {
1300 left: Box::new(left),
1301 op: BinaryOp::And,
1302 right: Box::new(right),
1303 },
1304 );
1305
1306 and_expr
1308 .clone()
1309 .foldl(
1310 just(Token::Or).ignore_then(and_expr).repeated(),
1311 |left, right| Expr::BinaryOp {
1312 left: Box::new(left),
1313 op: BinaryOp::Or,
1314 right: Box::new(right),
1315 },
1316 )
1317 .labelled("condition")
1318 .boxed()
1319}
1320
1321fn expr_parser<'tokens, I>(
1323) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1324where
1325 I: ValueInput<'tokens, Token = Token, Span = Span>,
1326{
1327 primary_expr_parser()
1329}
1330
1331fn primary_expr_parser<'tokens, I>(
1335) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1336where
1337 I: ValueInput<'tokens, Token = Token, Span = Span>,
1338{
1339 let positional = select! {
1341 Token::Positional(n) => Expr::Positional(n),
1342 Token::AllArgs => Expr::AllArgs,
1343 Token::ArgCount => Expr::ArgCount,
1344 Token::VarLength(name) => Expr::VarLength(name),
1345 Token::LastExitCode => Expr::LastExitCode,
1346 Token::CurrentPid => Expr::CurrentPid,
1347 };
1348
1349 let arithmetic = select! {
1351 Token::Arithmetic(expr_str) => Expr::Arithmetic(expr_str),
1352 };
1353
1354 let keyword_as_bareword = select! {
1357 Token::Done => "done",
1358 Token::Fi => "fi",
1359 Token::Then => "then",
1360 Token::Else => "else",
1361 Token::Elif => "elif",
1362 Token::In => "in",
1363 Token::Do => "do",
1364 Token::Esac => "esac",
1365 }
1366 .map(|s| Expr::Literal(Value::String(s.to_string())));
1367
1368 let plus_minus_bare = select! {
1370 Token::PlusBare(s) => Expr::Literal(Value::String(s)),
1371 Token::MinusBare(s) => Expr::Literal(Value::String(s)),
1372 Token::MinusAlone => Expr::Literal(Value::String("-".to_string())),
1373 };
1374
1375 recursive(|expr| {
1376 choice((
1377 positional,
1378 arithmetic,
1379 cmd_subst_parser(expr.clone()),
1380 var_expr_parser(),
1381 interpolated_string_parser(),
1382 literal_parser().map(Expr::Literal),
1383 ident_parser().map(|s| Expr::Literal(Value::String(s))),
1385 path_parser().map(|s| Expr::Literal(Value::String(s))),
1387 select! {
1390 Token::DotDot => Expr::Literal(Value::String("..".into())),
1391 Token::Tilde => Expr::Literal(Value::String("~".into())),
1392 Token::TildePath(s) => Expr::Literal(Value::String(s)),
1393 Token::RelativePath(s) => Expr::Literal(Value::String(s)),
1394 Token::DotSlashPath(s) => Expr::Literal(Value::String(s)),
1395 },
1396 plus_minus_bare,
1397 keyword_as_bareword,
1399 ))
1400 .labelled("expression")
1401 })
1402 .boxed()
1403}
1404
1405fn var_expr_parser<'tokens, I>(
1408) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1409where
1410 I: ValueInput<'tokens, Token = Token, Span = Span>,
1411{
1412 select! {
1413 Token::VarRef(raw) => parse_var_expr(&raw),
1414 Token::SimpleVarRef(name) => Expr::VarRef(VarPath::simple(name)),
1415 }
1416 .labelled("variable reference")
1417}
1418
1419fn cmd_subst_parser<'tokens, I, E>(
1423 expr: E,
1424) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1425where
1426 I: ValueInput<'tokens, Token = Token, Span = Span>,
1427 E: Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone,
1428{
1429 let long_flag_with_value = select! {
1432 Token::LongFlag(name) => name,
1433 }
1434 .then_ignore(just(Token::Eq))
1435 .then(expr.clone())
1436 .map(|(key, value)| Arg::Named { key, value });
1437
1438 let long_flag = select! {
1440 Token::LongFlag(name) => Arg::LongFlag(name),
1441 };
1442
1443 let short_flag = select! {
1445 Token::ShortFlag(name) => Arg::ShortFlag(name),
1446 };
1447
1448 let named = ident_parser()
1450 .then_ignore(just(Token::Eq))
1451 .then(expr.clone())
1452 .map(|(key, value)| Arg::Named { key, value });
1453
1454 let positional = expr.map(Arg::Positional);
1456
1457 let arg = choice((
1458 long_flag_with_value,
1459 long_flag,
1460 short_flag,
1461 named,
1462 positional,
1463 ));
1464
1465 let command_name = choice((
1467 ident_parser(),
1468 just(Token::True).to("true".to_string()),
1469 just(Token::False).to("false".to_string()),
1470 ));
1471
1472 let command = command_name
1474 .then(arg.repeated().collect::<Vec<_>>())
1475 .map(|(name, args)| Command {
1476 name,
1477 args,
1478 redirects: vec![],
1479 });
1480
1481 let pipeline = command
1483 .separated_by(just(Token::Pipe))
1484 .at_least(1)
1485 .collect::<Vec<_>>()
1486 .map(|commands| Pipeline {
1487 commands,
1488 background: false,
1489 });
1490
1491 just(Token::CmdSubstStart)
1492 .ignore_then(pipeline)
1493 .then_ignore(just(Token::RParen))
1494 .map(|pipeline| Expr::CommandSubst(Box::new(pipeline)))
1495 .labelled("command substitution")
1496}
1497
1498fn interpolated_string_parser<'tokens, I>(
1500) -> impl Parser<'tokens, I, Expr, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1501where
1502 I: ValueInput<'tokens, Token = Token, Span = Span>,
1503{
1504 let double_quoted = select! {
1506 Token::String(s) => s,
1507 }
1508 .map(|s| {
1509 if s.contains('$') || s.contains("__KAISH_ESCAPED_DOLLAR__") {
1511 let parts = parse_interpolated_string(&s);
1513 if parts.len() == 1
1514 && let StringPart::Literal(text) = &parts[0] {
1515 return Expr::Literal(Value::String(text.clone()));
1516 }
1517 Expr::Interpolated(parts)
1518 } else {
1519 Expr::Literal(Value::String(s))
1520 }
1521 });
1522
1523 let single_quoted = select! {
1525 Token::SingleString(s) => Expr::Literal(Value::String(s)),
1526 };
1527
1528 choice((single_quoted, double_quoted)).labelled("string")
1529}
1530
1531fn literal_parser<'tokens, I>(
1533) -> impl Parser<'tokens, I, Value, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1534where
1535 I: ValueInput<'tokens, Token = Token, Span = Span>,
1536{
1537 choice((
1538 select! {
1539 Token::True => Value::Bool(true),
1540 Token::False => Value::Bool(false),
1541 },
1542 select! {
1543 Token::Int(n) => Value::Int(n),
1544 Token::Float(f) => Value::Float(f),
1545 },
1546 ))
1547 .labelled("literal")
1548 .boxed()
1549}
1550
1551fn ident_parser<'tokens, I>(
1553) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1554where
1555 I: ValueInput<'tokens, Token = Token, Span = Span>,
1556{
1557 select! {
1558 Token::Ident(s) => s,
1559 }
1560 .labelled("identifier")
1561}
1562
1563fn path_parser<'tokens, I>(
1565) -> impl Parser<'tokens, I, String, extra::Err<Rich<'tokens, Token, Span>>> + Clone
1566where
1567 I: ValueInput<'tokens, Token = Token, Span = Span>,
1568{
1569 select! {
1570 Token::Path(s) => s,
1571 }
1572 .labelled("path")
1573}
1574
1575#[cfg(test)]
1576mod tests {
1577 use super::*;
1578
1579 #[test]
1580 fn parse_empty() {
1581 let result = parse("");
1582 assert!(result.is_ok());
1583 assert_eq!(result.expect("ok").statements.len(), 0);
1584 }
1585
1586 #[test]
1587 fn parse_newlines_only() {
1588 let result = parse("\n\n\n");
1589 assert!(result.is_ok());
1590 }
1591
1592 #[test]
1593 fn parse_simple_command() {
1594 let result = parse("echo");
1595 assert!(result.is_ok());
1596 let program = result.expect("ok");
1597 assert_eq!(program.statements.len(), 1);
1598 assert!(matches!(&program.statements[0], Stmt::Command(_)));
1599 }
1600
1601 #[test]
1602 fn parse_command_with_string_arg() {
1603 let result = parse(r#"echo "hello""#);
1604 assert!(result.is_ok());
1605 let program = result.expect("ok");
1606 match &program.statements[0] {
1607 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 1),
1608 _ => panic!("expected Command"),
1609 }
1610 }
1611
1612 #[test]
1613 fn parse_assignment() {
1614 let result = parse("X=5");
1615 assert!(result.is_ok());
1616 let program = result.expect("ok");
1617 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
1618 }
1619
1620 #[test]
1621 fn parse_pipeline() {
1622 let result = parse("a | b | c");
1623 assert!(result.is_ok());
1624 let program = result.expect("ok");
1625 match &program.statements[0] {
1626 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
1627 _ => panic!("expected Pipeline"),
1628 }
1629 }
1630
1631 #[test]
1632 fn parse_background_job() {
1633 let result = parse("cmd &");
1634 assert!(result.is_ok());
1635 let program = result.expect("ok");
1636 match &program.statements[0] {
1637 Stmt::Pipeline(p) => assert!(p.background),
1638 _ => panic!("expected Pipeline with background"),
1639 }
1640 }
1641
1642 #[test]
1643 fn parse_if_simple() {
1644 let result = parse("if true; then echo; fi");
1645 assert!(result.is_ok());
1646 let program = result.expect("ok");
1647 assert!(matches!(&program.statements[0], Stmt::If(_)));
1648 }
1649
1650 #[test]
1651 fn parse_if_else() {
1652 let result = parse("if true; then echo; else echo; fi");
1653 assert!(result.is_ok());
1654 let program = result.expect("ok");
1655 match &program.statements[0] {
1656 Stmt::If(if_stmt) => assert!(if_stmt.else_branch.is_some()),
1657 _ => panic!("expected If"),
1658 }
1659 }
1660
1661 #[test]
1662 fn parse_elif_simple() {
1663 let result = parse("if true; then echo a; elif false; then echo b; fi");
1664 assert!(result.is_ok(), "parse failed: {:?}", result);
1665 let program = result.expect("ok");
1666 match &program.statements[0] {
1667 Stmt::If(if_stmt) => {
1668 assert!(if_stmt.else_branch.is_some());
1670 let else_branch = if_stmt.else_branch.as_ref().unwrap();
1671 assert_eq!(else_branch.len(), 1);
1672 assert!(matches!(&else_branch[0], Stmt::If(_)));
1673 }
1674 _ => panic!("expected If"),
1675 }
1676 }
1677
1678 #[test]
1679 fn parse_elif_with_else() {
1680 let result = parse("if true; then echo a; elif false; then echo b; else echo c; fi");
1681 assert!(result.is_ok(), "parse failed: {:?}", result);
1682 let program = result.expect("ok");
1683 match &program.statements[0] {
1684 Stmt::If(outer_if) => {
1685 let else_branch = outer_if.else_branch.as_ref().expect("outer else");
1687 assert_eq!(else_branch.len(), 1);
1688 match &else_branch[0] {
1689 Stmt::If(inner_if) => {
1690 assert!(inner_if.else_branch.is_some());
1692 }
1693 _ => panic!("expected nested If from elif"),
1694 }
1695 }
1696 _ => panic!("expected If"),
1697 }
1698 }
1699
1700 #[test]
1701 fn parse_multiple_elif() {
1702 let result = parse(
1704 "if [[ ${X} == 1 ]]; then echo one; elif [[ ${X} == 2 ]]; then echo two; elif [[ ${X} == 3 ]]; then echo three; else echo other; fi",
1705 );
1706 assert!(result.is_ok(), "parse failed: {:?}", result);
1707 }
1708
1709 #[test]
1710 fn parse_for_loop() {
1711 let result = parse("for X in items; do echo; done");
1712 assert!(result.is_ok());
1713 let program = result.expect("ok");
1714 assert!(matches!(&program.statements[0], Stmt::For(_)));
1715 }
1716
1717 #[test]
1718 fn parse_brackets_not_array_literal() {
1719 let result = parse("cmd [1");
1721 let _ = result;
1724 }
1725
1726 #[test]
1727 fn parse_named_arg() {
1728 let result = parse("cmd foo=5");
1729 assert!(result.is_ok());
1730 let program = result.expect("ok");
1731 match &program.statements[0] {
1732 Stmt::Command(cmd) => {
1733 assert_eq!(cmd.args.len(), 1);
1734 assert!(matches!(&cmd.args[0], Arg::Named { .. }));
1735 }
1736 _ => panic!("expected Command"),
1737 }
1738 }
1739
1740 #[test]
1741 fn parse_short_flag() {
1742 let result = parse("ls -l");
1743 assert!(result.is_ok());
1744 let program = result.expect("ok");
1745 match &program.statements[0] {
1746 Stmt::Command(cmd) => {
1747 assert_eq!(cmd.name, "ls");
1748 assert_eq!(cmd.args.len(), 1);
1749 match &cmd.args[0] {
1750 Arg::ShortFlag(name) => assert_eq!(name, "l"),
1751 _ => panic!("expected ShortFlag"),
1752 }
1753 }
1754 _ => panic!("expected Command"),
1755 }
1756 }
1757
1758 #[test]
1759 fn parse_long_flag() {
1760 let result = parse("git push --force");
1761 assert!(result.is_ok());
1762 let program = result.expect("ok");
1763 match &program.statements[0] {
1764 Stmt::Command(cmd) => {
1765 assert_eq!(cmd.name, "git");
1766 assert_eq!(cmd.args.len(), 2);
1767 match &cmd.args[0] {
1768 Arg::Positional(Expr::Literal(Value::String(s))) => assert_eq!(s, "push"),
1769 _ => panic!("expected Positional push"),
1770 }
1771 match &cmd.args[1] {
1772 Arg::LongFlag(name) => assert_eq!(name, "force"),
1773 _ => panic!("expected LongFlag"),
1774 }
1775 }
1776 _ => panic!("expected Command"),
1777 }
1778 }
1779
1780 #[test]
1781 fn parse_long_flag_with_value() {
1782 let result = parse(r#"git commit --message="hello""#);
1783 assert!(result.is_ok());
1784 let program = result.expect("ok");
1785 match &program.statements[0] {
1786 Stmt::Command(cmd) => {
1787 assert_eq!(cmd.name, "git");
1788 assert_eq!(cmd.args.len(), 2);
1789 match &cmd.args[1] {
1790 Arg::Named { key, value } => {
1791 assert_eq!(key, "message");
1792 match value {
1793 Expr::Literal(Value::String(s)) => assert_eq!(s, "hello"),
1794 _ => panic!("expected String value"),
1795 }
1796 }
1797 _ => panic!("expected Named from --flag=value"),
1798 }
1799 }
1800 _ => panic!("expected Command"),
1801 }
1802 }
1803
1804 #[test]
1805 fn parse_mixed_flags_and_args() {
1806 let result = parse(r#"git commit -m "message" --amend"#);
1807 assert!(result.is_ok());
1808 let program = result.expect("ok");
1809 match &program.statements[0] {
1810 Stmt::Command(cmd) => {
1811 assert_eq!(cmd.name, "git");
1812 assert_eq!(cmd.args.len(), 4);
1813 assert!(matches!(&cmd.args[0], Arg::Positional(_)));
1815 match &cmd.args[1] {
1817 Arg::ShortFlag(name) => assert_eq!(name, "m"),
1818 _ => panic!("expected ShortFlag -m"),
1819 }
1820 assert!(matches!(&cmd.args[2], Arg::Positional(_)));
1822 match &cmd.args[3] {
1824 Arg::LongFlag(name) => assert_eq!(name, "amend"),
1825 _ => panic!("expected LongFlag --amend"),
1826 }
1827 }
1828 _ => panic!("expected Command"),
1829 }
1830 }
1831
1832 #[test]
1833 fn parse_redirect_stdout() {
1834 let result = parse("cmd > file");
1835 assert!(result.is_ok());
1836 let program = result.expect("ok");
1837 match &program.statements[0] {
1839 Stmt::Pipeline(p) => {
1840 assert_eq!(p.commands.len(), 1);
1841 let cmd = &p.commands[0];
1842 assert_eq!(cmd.redirects.len(), 1);
1843 assert!(matches!(cmd.redirects[0].kind, RedirectKind::StdoutOverwrite));
1844 }
1845 _ => panic!("expected Pipeline"),
1846 }
1847 }
1848
1849 #[test]
1850 fn parse_var_ref() {
1851 let result = parse("echo ${VAR}");
1852 assert!(result.is_ok());
1853 let program = result.expect("ok");
1854 match &program.statements[0] {
1855 Stmt::Command(cmd) => {
1856 assert_eq!(cmd.args.len(), 1);
1857 assert!(matches!(&cmd.args[0], Arg::Positional(Expr::VarRef(_))));
1858 }
1859 _ => panic!("expected Command"),
1860 }
1861 }
1862
1863 #[test]
1864 fn parse_multiple_statements() {
1865 let result = parse("a\nb\nc");
1866 assert!(result.is_ok());
1867 let program = result.expect("ok");
1868 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
1869 assert_eq!(non_empty.len(), 3);
1870 }
1871
1872 #[test]
1873 fn parse_semicolon_separated() {
1874 let result = parse("a; b; c");
1875 assert!(result.is_ok());
1876 let program = result.expect("ok");
1877 let non_empty: Vec<_> = program.statements.iter().filter(|s| !matches!(s, Stmt::Empty)).collect();
1878 assert_eq!(non_empty.len(), 3);
1879 }
1880
1881 #[test]
1882 fn parse_complex_pipeline() {
1883 let result = parse(r#"cat file | grep pattern="foo" | head count=10"#);
1884 assert!(result.is_ok());
1885 let program = result.expect("ok");
1886 match &program.statements[0] {
1887 Stmt::Pipeline(p) => assert_eq!(p.commands.len(), 3),
1888 _ => panic!("expected Pipeline"),
1889 }
1890 }
1891
1892 #[test]
1893 fn parse_json_as_string_arg() {
1894 let result = parse(r#"cmd '[[1, 2], [3, 4]]'"#);
1896 assert!(result.is_ok());
1897 }
1898
1899 #[test]
1900 fn parse_mixed_args() {
1901 let result = parse(r#"cmd pos1 key="val" pos2 num=42"#);
1902 assert!(result.is_ok());
1903 let program = result.expect("ok");
1904 match &program.statements[0] {
1905 Stmt::Command(cmd) => assert_eq!(cmd.args.len(), 4),
1906 _ => panic!("expected Command"),
1907 }
1908 }
1909
1910 #[test]
1911 fn error_unterminated_string() {
1912 let result = parse(r#"echo "hello"#);
1913 assert!(result.is_err());
1914 }
1915
1916 #[test]
1917 fn error_unterminated_var_ref() {
1918 let result = parse("echo ${VAR");
1919 assert!(result.is_err());
1920 }
1921
1922 #[test]
1923 fn error_missing_fi() {
1924 let result = parse("if true; then echo");
1925 assert!(result.is_err());
1926 }
1927
1928 #[test]
1929 fn error_missing_done() {
1930 let result = parse("for X in items; do echo");
1931 assert!(result.is_err());
1932 }
1933
1934 #[test]
1935 fn parse_nested_cmd_subst() {
1936 let result = parse("X=$(echo $(date))").unwrap();
1938 match &result.statements[0] {
1939 Stmt::Assignment(a) => {
1940 assert_eq!(a.name, "X");
1941 match &a.value {
1942 Expr::CommandSubst(outer) => {
1943 assert_eq!(outer.commands[0].name, "echo");
1944 match &outer.commands[0].args[0] {
1946 Arg::Positional(Expr::CommandSubst(inner)) => {
1947 assert_eq!(inner.commands[0].name, "date");
1948 }
1949 other => panic!("expected nested cmd subst, got {:?}", other),
1950 }
1951 }
1952 other => panic!("expected cmd subst, got {:?}", other),
1953 }
1954 }
1955 other => panic!("expected assignment, got {:?}", other),
1956 }
1957 }
1958
1959 #[test]
1960 fn parse_deeply_nested_cmd_subst() {
1961 let result = parse("X=$(a $(b $(c)))").unwrap();
1963 match &result.statements[0] {
1964 Stmt::Assignment(a) => match &a.value {
1965 Expr::CommandSubst(level1) => {
1966 assert_eq!(level1.commands[0].name, "a");
1967 match &level1.commands[0].args[0] {
1968 Arg::Positional(Expr::CommandSubst(level2)) => {
1969 assert_eq!(level2.commands[0].name, "b");
1970 match &level2.commands[0].args[0] {
1971 Arg::Positional(Expr::CommandSubst(level3)) => {
1972 assert_eq!(level3.commands[0].name, "c");
1973 }
1974 other => panic!("expected level3 cmd subst, got {:?}", other),
1975 }
1976 }
1977 other => panic!("expected level2 cmd subst, got {:?}", other),
1978 }
1979 }
1980 other => panic!("expected cmd subst, got {:?}", other),
1981 },
1982 other => panic!("expected assignment, got {:?}", other),
1983 }
1984 }
1985
1986 #[test]
1991 fn value_int_preserved() {
1992 let result = parse("X=42").unwrap();
1993 match &result.statements[0] {
1994 Stmt::Assignment(a) => {
1995 assert_eq!(a.name, "X");
1996 match &a.value {
1997 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
1998 other => panic!("expected int literal, got {:?}", other),
1999 }
2000 }
2001 other => panic!("expected assignment, got {:?}", other),
2002 }
2003 }
2004
2005 #[test]
2006 fn value_negative_int_preserved() {
2007 let result = parse("X=-99").unwrap();
2008 match &result.statements[0] {
2009 Stmt::Assignment(a) => match &a.value {
2010 Expr::Literal(Value::Int(n)) => assert_eq!(*n, -99),
2011 other => panic!("expected int, got {:?}", other),
2012 },
2013 other => panic!("expected assignment, got {:?}", other),
2014 }
2015 }
2016
2017 #[test]
2018 fn value_float_preserved() {
2019 let result = parse("PI=3.14").unwrap();
2020 match &result.statements[0] {
2021 Stmt::Assignment(a) => match &a.value {
2022 Expr::Literal(Value::Float(f)) => assert!((*f - 3.14).abs() < 0.001),
2023 other => panic!("expected float, got {:?}", other),
2024 },
2025 other => panic!("expected assignment, got {:?}", other),
2026 }
2027 }
2028
2029 #[test]
2030 fn value_string_preserved() {
2031 let result = parse(r#"echo "hello world""#).unwrap();
2032 match &result.statements[0] {
2033 Stmt::Command(cmd) => {
2034 assert_eq!(cmd.name, "echo");
2035 match &cmd.args[0] {
2036 Arg::Positional(Expr::Literal(Value::String(s))) => {
2037 assert_eq!(s, "hello world");
2038 }
2039 other => panic!("expected string arg, got {:?}", other),
2040 }
2041 }
2042 other => panic!("expected command, got {:?}", other),
2043 }
2044 }
2045
2046 #[test]
2047 fn value_string_with_escapes_preserved() {
2048 let result = parse(r#"echo "line1\nline2""#).unwrap();
2049 match &result.statements[0] {
2050 Stmt::Command(cmd) => match &cmd.args[0] {
2051 Arg::Positional(Expr::Literal(Value::String(s))) => {
2052 assert_eq!(s, "line1\nline2");
2053 }
2054 other => panic!("expected string, got {:?}", other),
2055 },
2056 other => panic!("expected command, got {:?}", other),
2057 }
2058 }
2059
2060 #[test]
2061 fn value_command_name_preserved() {
2062 let result = parse("my-command").unwrap();
2063 match &result.statements[0] {
2064 Stmt::Command(cmd) => assert_eq!(cmd.name, "my-command"),
2065 other => panic!("expected command, got {:?}", other),
2066 }
2067 }
2068
2069 #[test]
2070 fn value_assignment_name_preserved() {
2071 let result = parse("MY_VAR=1").unwrap();
2072 match &result.statements[0] {
2073 Stmt::Assignment(a) => assert_eq!(a.name, "MY_VAR"),
2074 other => panic!("expected assignment, got {:?}", other),
2075 }
2076 }
2077
2078 #[test]
2079 fn value_for_variable_preserved() {
2080 let result = parse("for ITEM in items; do echo; done").unwrap();
2081 match &result.statements[0] {
2082 Stmt::For(f) => assert_eq!(f.variable, "ITEM"),
2083 other => panic!("expected for, got {:?}", other),
2084 }
2085 }
2086
2087 #[test]
2088 fn value_varref_name_preserved() {
2089 let result = parse("echo ${MESSAGE}").unwrap();
2090 match &result.statements[0] {
2091 Stmt::Command(cmd) => match &cmd.args[0] {
2092 Arg::Positional(Expr::VarRef(path)) => {
2093 assert_eq!(path.segments.len(), 1);
2094 let VarSegment::Field(name) = &path.segments[0];
2095 assert_eq!(name, "MESSAGE");
2096 }
2097 other => panic!("expected varref, got {:?}", other),
2098 },
2099 other => panic!("expected command, got {:?}", other),
2100 }
2101 }
2102
2103 #[test]
2104 fn value_varref_field_access_preserved() {
2105 let result = parse("echo ${RESULT.data}").unwrap();
2106 match &result.statements[0] {
2107 Stmt::Command(cmd) => match &cmd.args[0] {
2108 Arg::Positional(Expr::VarRef(path)) => {
2109 assert_eq!(path.segments.len(), 2);
2110 let VarSegment::Field(a) = &path.segments[0];
2111 let VarSegment::Field(b) = &path.segments[1];
2112 assert_eq!(a, "RESULT");
2113 assert_eq!(b, "data");
2114 }
2115 other => panic!("expected varref, got {:?}", other),
2116 },
2117 other => panic!("expected command, got {:?}", other),
2118 }
2119 }
2120
2121 #[test]
2122 fn value_varref_index_ignored() {
2123 let result = parse("echo ${ITEMS[0]}").unwrap();
2125 match &result.statements[0] {
2126 Stmt::Command(cmd) => match &cmd.args[0] {
2127 Arg::Positional(Expr::VarRef(path)) => {
2128 assert_eq!(path.segments.len(), 1);
2130 let VarSegment::Field(name) = &path.segments[0];
2131 assert_eq!(name, "ITEMS");
2132 }
2133 other => panic!("expected varref, got {:?}", other),
2134 },
2135 other => panic!("expected command, got {:?}", other),
2136 }
2137 }
2138
2139 #[test]
2140 fn value_last_result_ref_preserved() {
2141 let result = parse("echo ${?.ok}").unwrap();
2142 match &result.statements[0] {
2143 Stmt::Command(cmd) => match &cmd.args[0] {
2144 Arg::Positional(Expr::VarRef(path)) => {
2145 assert_eq!(path.segments.len(), 2);
2146 let VarSegment::Field(name) = &path.segments[0];
2147 assert_eq!(name, "?");
2148 }
2149 other => panic!("expected varref, got {:?}", other),
2150 },
2151 other => panic!("expected command, got {:?}", other),
2152 }
2153 }
2154
2155 #[test]
2156 fn value_named_arg_preserved() {
2157 let result = parse("cmd count=42").unwrap();
2158 match &result.statements[0] {
2159 Stmt::Command(cmd) => {
2160 assert_eq!(cmd.name, "cmd");
2161 match &cmd.args[0] {
2162 Arg::Named { key, value } => {
2163 assert_eq!(key, "count");
2164 match value {
2165 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 42),
2166 other => panic!("expected int, got {:?}", other),
2167 }
2168 }
2169 other => panic!("expected named arg, got {:?}", other),
2170 }
2171 }
2172 other => panic!("expected command, got {:?}", other),
2173 }
2174 }
2175
2176 #[test]
2177 fn value_function_def_name_preserved() {
2178 let result = parse("greet() { echo }").unwrap();
2179 match &result.statements[0] {
2180 Stmt::ToolDef(t) => {
2181 assert_eq!(t.name, "greet");
2182 assert!(t.params.is_empty());
2183 }
2184 other => panic!("expected function def, got {:?}", other),
2185 }
2186 }
2187
2188 #[test]
2193 fn parse_comparison_equals() {
2194 let result = parse("if [[ ${X} == 5 ]]; then echo; fi").unwrap();
2196 match &result.statements[0] {
2197 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2198 Expr::Test(test) => match test.as_ref() {
2199 TestExpr::Comparison { left, op, right } => {
2200 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2201 assert_eq!(*op, TestCmpOp::Eq);
2202 match right.as_ref() {
2203 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 5),
2204 other => panic!("expected int, got {:?}", other),
2205 }
2206 }
2207 other => panic!("expected comparison, got {:?}", other),
2208 },
2209 other => panic!("expected test expr, got {:?}", other),
2210 },
2211 other => panic!("expected if, got {:?}", other),
2212 }
2213 }
2214
2215 #[test]
2216 fn parse_comparison_not_equals() {
2217 let result = parse("if [[ ${X} != 0 ]]; then echo; fi").unwrap();
2218 match &result.statements[0] {
2219 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2220 Expr::Test(test) => match test.as_ref() {
2221 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotEq),
2222 other => panic!("expected comparison, got {:?}", other),
2223 },
2224 other => panic!("expected test expr, got {:?}", other),
2225 },
2226 other => panic!("expected if, got {:?}", other),
2227 }
2228 }
2229
2230 #[test]
2231 fn parse_comparison_less_than() {
2232 let result = parse("if [[ ${COUNT} -lt 10 ]]; then echo; fi").unwrap();
2233 match &result.statements[0] {
2234 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2235 Expr::Test(test) => match test.as_ref() {
2236 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Lt),
2237 other => panic!("expected comparison, got {:?}", other),
2238 },
2239 other => panic!("expected test expr, got {:?}", other),
2240 },
2241 other => panic!("expected if, got {:?}", other),
2242 }
2243 }
2244
2245 #[test]
2246 fn parse_comparison_greater_than() {
2247 let result = parse("if [[ ${COUNT} -gt 0 ]]; then echo; fi").unwrap();
2248 match &result.statements[0] {
2249 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2250 Expr::Test(test) => match test.as_ref() {
2251 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Gt),
2252 other => panic!("expected comparison, got {:?}", other),
2253 },
2254 other => panic!("expected test expr, got {:?}", other),
2255 },
2256 other => panic!("expected if, got {:?}", other),
2257 }
2258 }
2259
2260 #[test]
2261 fn parse_comparison_less_equal() {
2262 let result = parse("if [[ ${X} -le 100 ]]; then echo; fi").unwrap();
2263 match &result.statements[0] {
2264 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2265 Expr::Test(test) => match test.as_ref() {
2266 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::LtEq),
2267 other => panic!("expected comparison, got {:?}", other),
2268 },
2269 other => panic!("expected test expr, got {:?}", other),
2270 },
2271 other => panic!("expected if, got {:?}", other),
2272 }
2273 }
2274
2275 #[test]
2276 fn parse_comparison_greater_equal() {
2277 let result = parse("if [[ ${X} -ge 1 ]]; then echo; fi").unwrap();
2278 match &result.statements[0] {
2279 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2280 Expr::Test(test) => match test.as_ref() {
2281 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::GtEq),
2282 other => panic!("expected comparison, got {:?}", other),
2283 },
2284 other => panic!("expected test expr, got {:?}", other),
2285 },
2286 other => panic!("expected if, got {:?}", other),
2287 }
2288 }
2289
2290 #[test]
2291 fn parse_regex_match() {
2292 let result = parse(r#"if [[ ${NAME} =~ "^test" ]]; then echo; fi"#).unwrap();
2293 match &result.statements[0] {
2294 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2295 Expr::Test(test) => match test.as_ref() {
2296 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::Match),
2297 other => panic!("expected comparison, got {:?}", other),
2298 },
2299 other => panic!("expected test expr, got {:?}", other),
2300 },
2301 other => panic!("expected if, got {:?}", other),
2302 }
2303 }
2304
2305 #[test]
2306 fn parse_regex_not_match() {
2307 let result = parse(r#"if [[ ${NAME} !~ "^test" ]]; then echo; fi"#).unwrap();
2308 match &result.statements[0] {
2309 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2310 Expr::Test(test) => match test.as_ref() {
2311 TestExpr::Comparison { op, .. } => assert_eq!(*op, TestCmpOp::NotMatch),
2312 other => panic!("expected comparison, got {:?}", other),
2313 },
2314 other => panic!("expected test expr, got {:?}", other),
2315 },
2316 other => panic!("expected if, got {:?}", other),
2317 }
2318 }
2319
2320 #[test]
2321 fn parse_string_interpolation() {
2322 let result = parse(r#"echo "Hello ${NAME}!""#).unwrap();
2323 match &result.statements[0] {
2324 Stmt::Command(cmd) => match &cmd.args[0] {
2325 Arg::Positional(Expr::Interpolated(parts)) => {
2326 assert_eq!(parts.len(), 3);
2327 match &parts[0] {
2328 StringPart::Literal(s) => assert_eq!(s, "Hello "),
2329 other => panic!("expected literal, got {:?}", other),
2330 }
2331 match &parts[1] {
2332 StringPart::Var(path) => {
2333 assert_eq!(path.segments.len(), 1);
2334 let VarSegment::Field(name) = &path.segments[0];
2335 assert_eq!(name, "NAME");
2336 }
2337 other => panic!("expected var, got {:?}", other),
2338 }
2339 match &parts[2] {
2340 StringPart::Literal(s) => assert_eq!(s, "!"),
2341 other => panic!("expected literal, got {:?}", other),
2342 }
2343 }
2344 other => panic!("expected interpolated, got {:?}", other),
2345 },
2346 other => panic!("expected command, got {:?}", other),
2347 }
2348 }
2349
2350 #[test]
2351 fn parse_string_interpolation_multiple_vars() {
2352 let result = parse(r#"echo "${FIRST} and ${SECOND}""#).unwrap();
2353 match &result.statements[0] {
2354 Stmt::Command(cmd) => match &cmd.args[0] {
2355 Arg::Positional(Expr::Interpolated(parts)) => {
2356 assert_eq!(parts.len(), 3);
2358 assert!(matches!(&parts[0], StringPart::Var(_)));
2359 assert!(matches!(&parts[1], StringPart::Literal(_)));
2360 assert!(matches!(&parts[2], StringPart::Var(_)));
2361 }
2362 other => panic!("expected interpolated, got {:?}", other),
2363 },
2364 other => panic!("expected command, got {:?}", other),
2365 }
2366 }
2367
2368 #[test]
2369 fn parse_empty_function_body() {
2370 let result = parse("empty() { }").unwrap();
2371 match &result.statements[0] {
2372 Stmt::ToolDef(t) => {
2373 assert_eq!(t.name, "empty");
2374 assert!(t.params.is_empty());
2375 assert!(t.body.is_empty());
2376 }
2377 other => panic!("expected function def, got {:?}", other),
2378 }
2379 }
2380
2381 #[test]
2382 fn parse_bash_style_function() {
2383 let result = parse("function greet { echo hello }").unwrap();
2384 match &result.statements[0] {
2385 Stmt::ToolDef(t) => {
2386 assert_eq!(t.name, "greet");
2387 assert!(t.params.is_empty());
2388 assert_eq!(t.body.len(), 1);
2389 }
2390 other => panic!("expected function def, got {:?}", other),
2391 }
2392 }
2393
2394 #[test]
2395 fn parse_comparison_string_values() {
2396 let result = parse(r#"if [[ ${STATUS} == "ok" ]]; then echo; fi"#).unwrap();
2397 match &result.statements[0] {
2398 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2399 Expr::Test(test) => match test.as_ref() {
2400 TestExpr::Comparison { left, op, right } => {
2401 assert!(matches!(left.as_ref(), Expr::VarRef(_)));
2402 assert_eq!(*op, TestCmpOp::Eq);
2403 match right.as_ref() {
2404 Expr::Literal(Value::String(s)) => assert_eq!(s, "ok"),
2405 other => panic!("expected string, got {:?}", other),
2406 }
2407 }
2408 other => panic!("expected comparison, got {:?}", other),
2409 },
2410 other => panic!("expected test expr, got {:?}", other),
2411 },
2412 other => panic!("expected if, got {:?}", other),
2413 }
2414 }
2415
2416 #[test]
2421 fn parse_cmd_subst_simple() {
2422 let result = parse("X=$(echo)").unwrap();
2423 match &result.statements[0] {
2424 Stmt::Assignment(a) => {
2425 assert_eq!(a.name, "X");
2426 match &a.value {
2427 Expr::CommandSubst(pipeline) => {
2428 assert_eq!(pipeline.commands.len(), 1);
2429 assert_eq!(pipeline.commands[0].name, "echo");
2430 }
2431 other => panic!("expected command subst, got {:?}", other),
2432 }
2433 }
2434 other => panic!("expected assignment, got {:?}", other),
2435 }
2436 }
2437
2438 #[test]
2439 fn parse_cmd_subst_with_args() {
2440 let result = parse(r#"X=$(fetch url="http://example.com")"#).unwrap();
2441 match &result.statements[0] {
2442 Stmt::Assignment(a) => match &a.value {
2443 Expr::CommandSubst(pipeline) => {
2444 assert_eq!(pipeline.commands[0].name, "fetch");
2445 assert_eq!(pipeline.commands[0].args.len(), 1);
2446 match &pipeline.commands[0].args[0] {
2447 Arg::Named { key, .. } => assert_eq!(key, "url"),
2448 other => panic!("expected named arg, got {:?}", other),
2449 }
2450 }
2451 other => panic!("expected command subst, got {:?}", other),
2452 },
2453 other => panic!("expected assignment, got {:?}", other),
2454 }
2455 }
2456
2457 #[test]
2458 fn parse_cmd_subst_pipeline() {
2459 let result = parse("X=$(cat file | grep pattern)").unwrap();
2460 match &result.statements[0] {
2461 Stmt::Assignment(a) => match &a.value {
2462 Expr::CommandSubst(pipeline) => {
2463 assert_eq!(pipeline.commands.len(), 2);
2464 assert_eq!(pipeline.commands[0].name, "cat");
2465 assert_eq!(pipeline.commands[1].name, "grep");
2466 }
2467 other => panic!("expected command subst, got {:?}", other),
2468 },
2469 other => panic!("expected assignment, got {:?}", other),
2470 }
2471 }
2472
2473 #[test]
2474 fn parse_cmd_subst_in_condition() {
2475 let result = parse("if validate; then echo; fi").unwrap();
2477 match &result.statements[0] {
2478 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2479 Expr::Command(cmd) => {
2480 assert_eq!(cmd.name, "validate");
2481 }
2482 other => panic!("expected command, got {:?}", other),
2483 },
2484 other => panic!("expected if, got {:?}", other),
2485 }
2486 }
2487
2488 #[test]
2489 fn parse_cmd_subst_in_command_arg() {
2490 let result = parse("echo $(whoami)").unwrap();
2491 match &result.statements[0] {
2492 Stmt::Command(cmd) => {
2493 assert_eq!(cmd.name, "echo");
2494 match &cmd.args[0] {
2495 Arg::Positional(Expr::CommandSubst(pipeline)) => {
2496 assert_eq!(pipeline.commands[0].name, "whoami");
2497 }
2498 other => panic!("expected command subst, got {:?}", other),
2499 }
2500 }
2501 other => panic!("expected command, got {:?}", other),
2502 }
2503 }
2504
2505 #[test]
2510 fn parse_condition_and() {
2511 let result = parse("if check-a && check-b; then echo; fi").unwrap();
2513 match &result.statements[0] {
2514 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2515 Expr::BinaryOp { left, op, right } => {
2516 assert_eq!(*op, BinaryOp::And);
2517 assert!(matches!(left.as_ref(), Expr::Command(_)));
2518 assert!(matches!(right.as_ref(), Expr::Command(_)));
2519 }
2520 other => panic!("expected binary op, got {:?}", other),
2521 },
2522 other => panic!("expected if, got {:?}", other),
2523 }
2524 }
2525
2526 #[test]
2527 fn parse_condition_or() {
2528 let result = parse("if try-a || try-b; then echo; fi").unwrap();
2529 match &result.statements[0] {
2530 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2531 Expr::BinaryOp { left, op, right } => {
2532 assert_eq!(*op, BinaryOp::Or);
2533 assert!(matches!(left.as_ref(), Expr::Command(_)));
2534 assert!(matches!(right.as_ref(), Expr::Command(_)));
2535 }
2536 other => panic!("expected binary op, got {:?}", other),
2537 },
2538 other => panic!("expected if, got {:?}", other),
2539 }
2540 }
2541
2542 #[test]
2543 fn parse_condition_and_or_precedence() {
2544 let result = parse("if cmd-a && cmd-b || cmd-c; then echo; fi").unwrap();
2546 match &result.statements[0] {
2547 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2548 Expr::BinaryOp { left, op, right } => {
2549 assert_eq!(*op, BinaryOp::Or);
2551 match left.as_ref() {
2553 Expr::BinaryOp { op: inner_op, .. } => {
2554 assert_eq!(*inner_op, BinaryOp::And);
2555 }
2556 other => panic!("expected binary op (&&), got {:?}", other),
2557 }
2558 assert!(matches!(right.as_ref(), Expr::Command(_)));
2560 }
2561 other => panic!("expected binary op, got {:?}", other),
2562 },
2563 other => panic!("expected if, got {:?}", other),
2564 }
2565 }
2566
2567 #[test]
2568 fn parse_condition_multiple_and() {
2569 let result = parse("if cmd-a && cmd-b && cmd-c; then echo; fi").unwrap();
2570 match &result.statements[0] {
2571 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2572 Expr::BinaryOp { left, op, .. } => {
2573 assert_eq!(*op, BinaryOp::And);
2574 match left.as_ref() {
2576 Expr::BinaryOp { op: inner_op, .. } => {
2577 assert_eq!(*inner_op, BinaryOp::And);
2578 }
2579 other => panic!("expected binary op, got {:?}", other),
2580 }
2581 }
2582 other => panic!("expected binary op, got {:?}", other),
2583 },
2584 other => panic!("expected if, got {:?}", other),
2585 }
2586 }
2587
2588 #[test]
2589 fn parse_condition_mixed_comparison_and_logical() {
2590 let result = parse("if [[ ${X} == 5 ]] && [[ ${Y} -gt 0 ]]; then echo; fi").unwrap();
2592 match &result.statements[0] {
2593 Stmt::If(if_stmt) => match if_stmt.condition.as_ref() {
2594 Expr::BinaryOp { left, op, right } => {
2595 assert_eq!(*op, BinaryOp::And);
2596 match left.as_ref() {
2598 Expr::Test(test) => match test.as_ref() {
2599 TestExpr::Comparison { op: left_op, .. } => {
2600 assert_eq!(*left_op, TestCmpOp::Eq);
2601 }
2602 other => panic!("expected comparison, got {:?}", other),
2603 },
2604 other => panic!("expected test, got {:?}", other),
2605 }
2606 match right.as_ref() {
2608 Expr::Test(test) => match test.as_ref() {
2609 TestExpr::Comparison { op: right_op, .. } => {
2610 assert_eq!(*right_op, TestCmpOp::Gt);
2611 }
2612 other => panic!("expected comparison, got {:?}", other),
2613 },
2614 other => panic!("expected test, got {:?}", other),
2615 }
2616 }
2617 other => panic!("expected binary op, got {:?}", other),
2618 },
2619 other => panic!("expected if, got {:?}", other),
2620 }
2621 }
2622
2623 #[test]
2629 fn script_level1_linear() {
2630 let script = r#"
2631NAME="kaish"
2632VERSION=1
2633TIMEOUT=30
2634ITEMS="alpha beta gamma"
2635
2636echo "Starting ${NAME} v${VERSION}"
2637cat "README.md" | grep pattern="install" | head count=5
2638fetch url="https://api.example.com/status" timeout=${TIMEOUT} > "/tmp/status.json"
2639echo "Items: ${ITEMS}"
2640"#;
2641 let result = parse(script).unwrap();
2642 let stmts: Vec<_> = result.statements.iter()
2643 .filter(|s| !matches!(s, Stmt::Empty))
2644 .collect();
2645
2646 assert_eq!(stmts.len(), 8);
2647 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(_))); }
2656
2657 #[test]
2659 fn script_level2_branching() {
2660 let script = r#"
2661RESULT=$(validate "input.json")
2662
2663if [[ ${RESULT.ok} == true ]]; then
2664 echo "Validation passed"
2665 process "input.json" > "output.json"
2666else
2667 echo "Validation failed: ${RESULT.err}"
2668fi
2669
2670if [[ ${COUNT} -gt 0 ]] && [[ ${COUNT} -le 100 ]]; then
2671 echo "Count in valid range"
2672fi
2673
2674if check-network || check-cache; then
2675 fetch url=${URL}
2676fi
2677"#;
2678 let result = parse(script).unwrap();
2679 let stmts: Vec<_> = result.statements.iter()
2680 .filter(|s| !matches!(s, Stmt::Empty))
2681 .collect();
2682
2683 assert_eq!(stmts.len(), 4);
2684
2685 match stmts[0] {
2687 Stmt::Assignment(a) => {
2688 assert_eq!(a.name, "RESULT");
2689 assert!(matches!(&a.value, Expr::CommandSubst(_)));
2690 }
2691 other => panic!("expected assignment, got {:?}", other),
2692 }
2693
2694 match stmts[1] {
2696 Stmt::If(if_stmt) => {
2697 assert_eq!(if_stmt.then_branch.len(), 2);
2698 assert!(if_stmt.else_branch.is_some());
2699 assert_eq!(if_stmt.else_branch.as_ref().unwrap().len(), 1);
2700 }
2701 other => panic!("expected if, got {:?}", other),
2702 }
2703
2704 match stmts[2] {
2706 Stmt::If(if_stmt) => {
2707 match if_stmt.condition.as_ref() {
2708 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
2709 other => panic!("expected && condition, got {:?}", other),
2710 }
2711 }
2712 other => panic!("expected if, got {:?}", other),
2713 }
2714
2715 match stmts[3] {
2717 Stmt::If(if_stmt) => {
2718 match if_stmt.condition.as_ref() {
2719 Expr::BinaryOp { op, left, right } => {
2720 assert_eq!(*op, BinaryOp::Or);
2721 assert!(matches!(left.as_ref(), Expr::Command(_)));
2722 assert!(matches!(right.as_ref(), Expr::Command(_)));
2723 }
2724 other => panic!("expected || condition, got {:?}", other),
2725 }
2726 }
2727 other => panic!("expected if, got {:?}", other),
2728 }
2729 }
2730
2731 #[test]
2733 fn script_level3_loops_and_functions() {
2734 let script = r#"
2735greet() {
2736 echo "Hello, $1!"
2737}
2738
2739fetch_all() {
2740 for URL in $@; do
2741 fetch url=${URL}
2742 done
2743}
2744
2745USERS="alice bob charlie"
2746
2747for USER in ${USERS}; do
2748 greet ${USER}
2749 if [[ ${USER} == "bob" ]]; then
2750 echo "Found Bob!"
2751 fi
2752done
2753
2754long-running-task &
2755"#;
2756 let result = parse(script).unwrap();
2757 let stmts: Vec<_> = result.statements.iter()
2758 .filter(|s| !matches!(s, Stmt::Empty))
2759 .collect();
2760
2761 assert_eq!(stmts.len(), 5);
2762
2763 match stmts[0] {
2765 Stmt::ToolDef(t) => {
2766 assert_eq!(t.name, "greet");
2767 assert!(t.params.is_empty());
2768 }
2769 other => panic!("expected function def, got {:?}", other),
2770 }
2771
2772 match stmts[1] {
2774 Stmt::ToolDef(t) => {
2775 assert_eq!(t.name, "fetch_all");
2776 assert_eq!(t.body.len(), 1);
2777 assert!(matches!(&t.body[0], Stmt::For(_)));
2778 }
2779 other => panic!("expected function def, got {:?}", other),
2780 }
2781
2782 assert!(matches!(stmts[2], Stmt::Assignment(_)));
2784
2785 match stmts[3] {
2787 Stmt::For(f) => {
2788 assert_eq!(f.variable, "USER");
2789 assert_eq!(f.body.len(), 2);
2790 assert!(matches!(&f.body[0], Stmt::Command(_)));
2791 assert!(matches!(&f.body[1], Stmt::If(_)));
2792 }
2793 other => panic!("expected for loop, got {:?}", other),
2794 }
2795
2796 match stmts[4] {
2798 Stmt::Pipeline(p) => {
2799 assert!(p.background);
2800 assert_eq!(p.commands[0].name, "long-running-task");
2801 }
2802 other => panic!("expected pipeline (background), got {:?}", other),
2803 }
2804 }
2805
2806 #[test]
2808 fn script_level4_complex_nesting() {
2809 let script = r#"
2810RESULT=$(cat "config.json" | jq query=".servers" | validate schema="server-schema.json")
2811
2812if ping host=${HOST} && [[ ${RESULT} == true ]]; then
2813 for SERVER in "prod-1 prod-2"; do
2814 deploy target=${SERVER} port=8080
2815 if [[ ${?.code} != 0 ]]; then
2816 notify channel="ops" message="Deploy failed"
2817 fi
2818 done
2819fi
2820"#;
2821 let result = parse(script).unwrap();
2822 let stmts: Vec<_> = result.statements.iter()
2823 .filter(|s| !matches!(s, Stmt::Empty))
2824 .collect();
2825
2826 assert_eq!(stmts.len(), 2);
2827
2828 match stmts[0] {
2830 Stmt::Assignment(a) => {
2831 assert_eq!(a.name, "RESULT");
2832 match &a.value {
2833 Expr::CommandSubst(pipeline) => {
2834 assert_eq!(pipeline.commands.len(), 3);
2835 }
2836 other => panic!("expected command subst, got {:?}", other),
2837 }
2838 }
2839 other => panic!("expected assignment, got {:?}", other),
2840 }
2841
2842 match stmts[1] {
2844 Stmt::If(if_stmt) => {
2845 match if_stmt.condition.as_ref() {
2846 Expr::BinaryOp { op, .. } => assert_eq!(*op, BinaryOp::And),
2847 other => panic!("expected && condition, got {:?}", other),
2848 }
2849 assert_eq!(if_stmt.then_branch.len(), 1);
2850 match &if_stmt.then_branch[0] {
2851 Stmt::For(f) => {
2852 assert_eq!(f.body.len(), 2);
2853 assert!(matches!(&f.body[1], Stmt::If(_)));
2854 }
2855 other => panic!("expected for in if body, got {:?}", other),
2856 }
2857 }
2858 other => panic!("expected if, got {:?}", other),
2859 }
2860 }
2861
2862 #[test]
2864 fn script_level5_edge_cases() {
2865 let script = r#"
2866echo ""
2867echo "quotes: \"nested\" here"
2868echo "escapes: \n\t\r\\"
2869echo "unicode: \u2764"
2870
2871X=-99999
2872Y=3.14159265358979
2873Z=-0.001
2874
2875cmd a=1 b="two" c=true d=false e=null
2876
2877if true; then
2878 if false; then
2879 echo "inner"
2880 else
2881 echo "else"
2882 fi
2883fi
2884
2885for I in "a b c"; do
2886 echo ${I}
2887done
2888
2889no_params() {
2890 echo "no params"
2891}
2892
2893function all_args {
2894 echo "args: $@"
2895}
2896
2897a | b | c | d | e &
2898cmd 2> "errors.log"
2899cmd &> "all.log"
2900cmd >> "append.log"
2901cmd < "input.txt"
2902"#;
2903 let result = parse(script).unwrap();
2904 let stmts: Vec<_> = result.statements.iter()
2905 .filter(|s| !matches!(s, Stmt::Empty))
2906 .collect();
2907
2908 assert!(stmts.len() >= 10, "expected many statements, got {}", stmts.len());
2910
2911 let bg_stmt = stmts.iter().find(|s| matches!(s, Stmt::Pipeline(p) if p.background));
2913 assert!(bg_stmt.is_some(), "expected background pipeline");
2914
2915 match bg_stmt.unwrap() {
2916 Stmt::Pipeline(p) => {
2917 assert_eq!(p.commands.len(), 5);
2918 assert!(p.background);
2919 }
2920 _ => unreachable!(),
2921 }
2922 }
2923
2924 #[test]
2929 fn parse_keyword_as_variable_rejected() {
2930 let result = parse(r#"if="value""#);
2933 assert!(result.is_err(), "if= should fail - 'if' is a keyword");
2934
2935 let result = parse("while=true");
2936 assert!(result.is_err(), "while= should fail - 'while' is a keyword");
2937
2938 let result = parse(r#"then="next""#);
2939 assert!(result.is_err(), "then= should fail - 'then' is a keyword");
2940 }
2941
2942 #[test]
2943 fn parse_set_command_with_flag() {
2944 let result = parse("set -e");
2945 assert!(result.is_ok(), "failed to parse set -e: {:?}", result);
2946 let program = result.unwrap();
2947 match &program.statements[0] {
2948 Stmt::Command(cmd) => {
2949 assert_eq!(cmd.name, "set");
2950 assert_eq!(cmd.args.len(), 1);
2951 match &cmd.args[0] {
2952 Arg::ShortFlag(f) => assert_eq!(f, "e"),
2953 other => panic!("expected ShortFlag, got {:?}", other),
2954 }
2955 }
2956 other => panic!("expected Command, got {:?}", other),
2957 }
2958 }
2959
2960 #[test]
2961 fn parse_set_command_no_args() {
2962 let result = parse("set");
2963 assert!(result.is_ok(), "failed to parse set: {:?}", result);
2964 let program = result.unwrap();
2965 match &program.statements[0] {
2966 Stmt::Command(cmd) => {
2967 assert_eq!(cmd.name, "set");
2968 assert_eq!(cmd.args.len(), 0);
2969 }
2970 other => panic!("expected Command, got {:?}", other),
2971 }
2972 }
2973
2974 #[test]
2975 fn parse_set_assignment_vs_command() {
2976 let result = parse("X=5");
2978 assert!(result.is_ok());
2979 let program = result.unwrap();
2980 assert!(matches!(&program.statements[0], Stmt::Assignment(_)));
2981
2982 let result = parse("set -e");
2984 assert!(result.is_ok());
2985 let program = result.unwrap();
2986 assert!(matches!(&program.statements[0], Stmt::Command(_)));
2987 }
2988
2989 #[test]
2990 fn parse_true_as_command() {
2991 let result = parse("true");
2992 assert!(result.is_ok());
2993 let program = result.unwrap();
2994 match &program.statements[0] {
2995 Stmt::Command(cmd) => assert_eq!(cmd.name, "true"),
2996 other => panic!("expected Command(true), got {:?}", other),
2997 }
2998 }
2999
3000 #[test]
3001 fn parse_false_as_command() {
3002 let result = parse("false");
3003 assert!(result.is_ok());
3004 let program = result.unwrap();
3005 match &program.statements[0] {
3006 Stmt::Command(cmd) => assert_eq!(cmd.name, "false"),
3007 other => panic!("expected Command(false), got {:?}", other),
3008 }
3009 }
3010
3011 #[test]
3012 fn parse_dot_as_source_alias() {
3013 let result = parse(". script.kai");
3014 assert!(result.is_ok(), "failed to parse . script.kai: {:?}", result);
3015 let program = result.unwrap();
3016 match &program.statements[0] {
3017 Stmt::Command(cmd) => {
3018 assert_eq!(cmd.name, ".");
3019 assert_eq!(cmd.args.len(), 1);
3020 }
3021 other => panic!("expected Command(.), got {:?}", other),
3022 }
3023 }
3024
3025 #[test]
3026 fn parse_source_command() {
3027 let result = parse("source utils.kai");
3028 assert!(result.is_ok(), "failed to parse source: {:?}", result);
3029 let program = result.unwrap();
3030 match &program.statements[0] {
3031 Stmt::Command(cmd) => {
3032 assert_eq!(cmd.name, "source");
3033 assert_eq!(cmd.args.len(), 1);
3034 }
3035 other => panic!("expected Command(source), got {:?}", other),
3036 }
3037 }
3038
3039 #[test]
3040 fn parse_test_expr_file_test() {
3041 let result = parse(r#"[[ -f "/path/file" ]]"#);
3043 assert!(result.is_ok(), "failed to parse file test: {:?}", result);
3044 }
3045
3046 #[test]
3047 fn parse_test_expr_comparison() {
3048 let result = parse(r#"[[ $X == "value" ]]"#);
3049 assert!(result.is_ok(), "failed to parse comparison test: {:?}", result);
3050 }
3051
3052 #[test]
3053 fn parse_test_expr_single_eq() {
3054 let result = parse(r#"[[ $X = "value" ]]"#);
3056 assert!(result.is_ok(), "failed to parse single-= comparison: {:?}", result);
3057 let program = result.unwrap();
3058 match &program.statements[0] {
3059 Stmt::Test(TestExpr::Comparison { op, .. }) => {
3060 assert_eq!(op, &TestCmpOp::Eq);
3061 }
3062 other => panic!("expected Test(Comparison), got {:?}", other),
3063 }
3064 }
3065
3066 #[test]
3067 fn parse_while_loop() {
3068 let result = parse("while true; do echo; done");
3069 assert!(result.is_ok(), "failed to parse while loop: {:?}", result);
3070 let program = result.unwrap();
3071 assert!(matches!(&program.statements[0], Stmt::While(_)));
3072 }
3073
3074 #[test]
3075 fn parse_break_with_level() {
3076 let result = parse("break 2");
3077 assert!(result.is_ok());
3078 let program = result.unwrap();
3079 match &program.statements[0] {
3080 Stmt::Break(Some(n)) => assert_eq!(*n, 2),
3081 other => panic!("expected Break(2), got {:?}", other),
3082 }
3083 }
3084
3085 #[test]
3086 fn parse_continue_with_level() {
3087 let result = parse("continue 3");
3088 assert!(result.is_ok());
3089 let program = result.unwrap();
3090 match &program.statements[0] {
3091 Stmt::Continue(Some(n)) => assert_eq!(*n, 3),
3092 other => panic!("expected Continue(3), got {:?}", other),
3093 }
3094 }
3095
3096 #[test]
3097 fn parse_exit_with_code() {
3098 let result = parse("exit 1");
3099 assert!(result.is_ok());
3100 let program = result.unwrap();
3101 match &program.statements[0] {
3102 Stmt::Exit(Some(expr)) => {
3103 match expr.as_ref() {
3104 Expr::Literal(Value::Int(n)) => assert_eq!(*n, 1),
3105 other => panic!("expected Int(1), got {:?}", other),
3106 }
3107 }
3108 other => panic!("expected Exit(1), got {:?}", other),
3109 }
3110 }
3111}