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