1use thiserror::Error;
10
11use crate::{
12 ast::{
13 AndOr, AndOrList, ArithBinaryOp, ArithExpr, ArithUnaryOp, Assignment, CaseClause,
14 CasePattern, CaseStatement, ForLoop, FunctionDefinition, IfStatement, Parameter, Pipeline,
15 Program, Redirect, SimpleCommand, Statement, WhileLoop, Word, WordPart,
16 },
17 lexer::{LexError, RawParameter, RawPart, RawWord, Token, TokenKind, tokenize},
18};
19
20const MAX_NESTING_DEPTH: u32 = 64;
30
31const MAX_ARITHMETIC_TOKENS: usize = 4_096;
37
38pub(crate) const REJECTED_COMMANDS: &[(&str, &str)] = &[
43 (
44 "eval",
45 "`eval` is excluded: running text the script assembled at runtime is self-modifying code and defeats the point of parsing the script up front",
46 ),
47 (
48 "exec",
49 "`exec` is excluded: this shell never replaces a process image and has no processes to replace",
50 ),
51 (
52 "source",
53 "`source` is excluded: there is no filesystem to read scripts from",
54 ),
55 (
56 ".",
57 "`.` (source) is excluded: there is no filesystem to read scripts from",
58 ),
59 (
60 "trap",
61 "`trap` is excluded: this shell has no signals or job control",
62 ),
63 (
64 "wait",
65 "`wait` is excluded: this shell has no job control, so nothing can be waiting",
66 ),
67 ("jobs", "`jobs` is excluded: this shell has no job control"),
68 ("fg", "`fg` is excluded: this shell has no job control"),
69 ("bg", "`bg` is excluded: this shell has no job control"),
70 (
71 "kill",
72 "`kill` is excluded: this shell has no processes or signals",
73 ),
74 (
75 "declare",
76 "`declare` is excluded: arrays and maps are real JSON values here, so `declare -A` has nothing to declare",
77 ),
78 (
79 "export",
80 "`export` is excluded: there is no process environment to export into",
81 ),
82 (
83 "set",
84 "`set` is excluded: this shell has no shell options, so `set -e`/`set -u`/`set -o pipefail` would change nothing while looking like they had; check each command's status with `$?`, `&&`, `||`, or `exit`",
85 ),
86 (
87 "[[",
88 "`[[ ... ]]` is not part of this shell; use `[ ... ]` or `test`, which support -z, -n, =, !=, <, >, and -eq/-ne/-lt/-le/-gt/-ge",
89 ),
90];
91
92const RESERVED_WORDS: &[&str] = &[
93 "if", "then", "elif", "else", "fi", "for", "in", "do", "done", "while", "case", "esac",
94 "until", "select", "function",
95];
96
97#[derive(Clone, Debug, Eq, Error, PartialEq)]
99pub enum ParseError {
100 #[error(transparent)]
102 Lex(#[from] LexError),
103 #[error("line {line}: {message}")]
105 Syntax {
106 line: usize,
108 message: String,
110 },
111}
112
113impl ParseError {
114 fn syntax(line: usize, message: impl Into<String>) -> Self {
115 Self::Syntax {
116 line,
117 message: message.into(),
118 }
119 }
120}
121
122pub fn parse(source: &str) -> Result<Program, ParseError> {
124 parse_nested(source, 0)
125}
126
127fn parse_nested(source: &str, depth: u32) -> Result<Program, ParseError> {
129 let tokens = tokenize(source)?;
130 let mut parser = Parser::new(tokens, depth);
131 let program = parser.parse_program(&[])?;
132 if let Some(token) = parser.peek() {
133 let line = token.line;
134 let kind = token.kind.clone();
135 return Err(ParseError::syntax(line, format!("unexpected {kind}")));
136 }
137 Ok(program)
138}
139
140fn too_deep(line: usize, construct: &str) -> ParseError {
142 ParseError::syntax(
143 line,
144 format!(
145 "{construct} nested more than {MAX_NESTING_DEPTH} levels deep; this shell parses on a fixed stack and refuses rather than risking it"
146 ),
147 )
148}
149
150struct Parser {
151 tokens: Vec<Token>,
152 position: usize,
153 depth: u32,
155}
156
157impl Parser {
158 fn new(tokens: Vec<Token>, depth: u32) -> Self {
159 Self {
160 tokens,
161 position: 0,
162 depth,
163 }
164 }
165
166 fn enter(&mut self, construct: &str) -> Result<(), ParseError> {
168 if self.depth >= MAX_NESTING_DEPTH {
169 return Err(too_deep(self.line(), construct));
170 }
171 self.depth += 1;
172 Ok(())
173 }
174
175 fn leave(&mut self) {
176 self.depth = self.depth.saturating_sub(1);
177 }
178
179 fn peek(&self) -> Option<&Token> {
180 self.tokens.get(self.position)
181 }
182
183 fn peek_kind(&self) -> Option<&TokenKind> {
184 self.peek().map(|token| &token.kind)
185 }
186
187 fn line(&self) -> usize {
188 self.peek().map_or_else(
189 || self.tokens.last().map_or(1, |token| token.line),
190 |token| token.line,
191 )
192 }
193
194 fn skip_newlines(&mut self) {
195 while matches!(self.peek_kind(), Some(TokenKind::Newline)) {
196 self.position += 1;
197 }
198 }
199
200 fn skip_separators(&mut self) {
201 while matches!(
202 self.peek_kind(),
203 Some(TokenKind::Newline | TokenKind::Semicolon)
204 ) {
205 self.position += 1;
206 }
207 }
208
209 fn peek_reserved(&self) -> Option<&str> {
210 match self.peek_kind()? {
211 TokenKind::Word(word) => {
212 let literal = word.as_literal()?;
213 RESERVED_WORDS
214 .iter()
215 .find(|reserved| **reserved == literal)
216 .copied()
217 }
218 _ => None,
219 }
220 }
221
222 fn eat_reserved(&mut self, expected: &str) -> bool {
223 if self.peek_reserved() == Some(expected) {
224 self.position += 1;
225 return true;
226 }
227 false
228 }
229
230 fn expect_reserved(&mut self, expected: &str, context: &str) -> Result<(), ParseError> {
231 if self.eat_reserved(expected) {
232 return Ok(());
233 }
234 let line = self.line();
235 Err(ParseError::syntax(
236 line,
237 format!("expected `{expected}` in {context}"),
238 ))
239 }
240
241 fn parse_program(&mut self, terminators: &[&str]) -> Result<Program, ParseError> {
242 self.enter("a command block")?;
243 let program = self.parse_program_body(terminators);
244 self.leave();
245 program
246 }
247
248 fn parse_program_body(&mut self, terminators: &[&str]) -> Result<Program, ParseError> {
249 let mut statements = Vec::new();
250 loop {
251 self.skip_separators();
252 match self.peek_kind() {
253 None | Some(TokenKind::RightBrace | TokenKind::DoubleSemicolon) => break,
256 Some(_) => {}
257 }
258 if self
259 .peek_reserved()
260 .is_some_and(|reserved| terminators.contains(&reserved))
261 {
262 break;
263 }
264 statements.push(self.parse_statement()?);
265 match self.peek_kind() {
266 None
267 | Some(
268 TokenKind::Newline
269 | TokenKind::Semicolon
270 | TokenKind::RightBrace
271 | TokenKind::DoubleSemicolon,
272 ) => {}
273 Some(TokenKind::Word(_)) if self.peek_reserved().is_some() => {}
274 Some(other) => {
275 let line = self.line();
276 let other = other.clone();
277 return Err(ParseError::syntax(
278 line,
279 format!("unexpected {other} after a command"),
280 ));
281 }
282 }
283 }
284 Ok(Program { statements })
285 }
286
287 fn parse_statement(&mut self) -> Result<Statement, ParseError> {
288 match self.peek_reserved() {
289 Some("if") => return self.parse_if().map(Statement::If),
290 Some("for") => return self.parse_for().map(Statement::For),
291 Some("while") => return self.parse_while(false).map(Statement::While),
292 Some("until") => return self.parse_while(true).map(Statement::While),
293 Some("case") => return self.parse_case().map(Statement::Case),
294 Some("esac") => {
295 let line = self.line();
296 return Err(ParseError::syntax(line, "`esac` without a matching `case`"));
297 }
298 Some("select") => {
299 let line = self.line();
300 return Err(ParseError::syntax(
301 line,
302 "`select` is not part of this shell: there is no interactive terminal to prompt",
303 ));
304 }
305 Some("function") => {
306 let line = self.line();
307 return Err(ParseError::syntax(
308 line,
309 "the `function` keyword is not part of this shell; define functions as `name() { ... }`",
310 ));
311 }
312 Some(other) => {
313 let line = self.line();
314 return Err(ParseError::syntax(line, format!("unexpected `{other}`")));
315 }
316 None => {}
317 }
318
319 if let Some(definition) = self.try_parse_function()? {
320 return Ok(Statement::Function(definition));
321 }
322
323 self.parse_and_or_list().map(Statement::List)
324 }
325
326 fn try_parse_function(&mut self) -> Result<Option<FunctionDefinition>, ParseError> {
327 let Some(TokenKind::Word(word)) = self.peek_kind() else {
328 return Ok(None);
329 };
330 let Some(name) = word.as_literal().map(str::to_owned) else {
331 return Ok(None);
332 };
333 if !matches!(
334 self.tokens.get(self.position + 1).map(|token| &token.kind),
335 Some(TokenKind::LeftParen)
336 ) || !matches!(
337 self.tokens.get(self.position + 2).map(|token| &token.kind),
338 Some(TokenKind::RightParen)
339 ) {
340 return Ok(None);
341 }
342
343 let line = self.line();
344 if !is_valid_name(&name) {
345 return Err(ParseError::syntax(
346 line,
347 format!("{name:?} is not a valid function name"),
348 ));
349 }
350 if let Some((_, reason)) = REJECTED_COMMANDS
351 .iter()
352 .find(|(rejected, _)| *rejected == name)
353 {
354 return Err(ParseError::syntax(
355 line,
356 format!("cannot define a function named {name:?}: {reason}"),
357 ));
358 }
359 if RESERVED_WORDS.contains(&name.as_str()) {
360 return Err(ParseError::syntax(
361 line,
362 format!("cannot define a function named {name:?}: it is a reserved word"),
363 ));
364 }
365
366 self.position += 3;
367 self.skip_newlines();
368 if !matches!(self.peek_kind(), Some(TokenKind::LeftBrace)) {
369 let line = self.line();
370 return Err(ParseError::syntax(
371 line,
372 format!("expected `{{` to open the body of function {name:?}"),
373 ));
374 }
375 self.position += 1;
376 let body = self.parse_program(&[])?;
377 if !matches!(self.peek_kind(), Some(TokenKind::RightBrace)) {
378 let line = self.line();
379 return Err(ParseError::syntax(
380 line,
381 format!("expected `}}` to close the body of function {name:?}"),
382 ));
383 }
384 self.position += 1;
385 Ok(Some(FunctionDefinition { name, body }))
386 }
387
388 fn parse_if(&mut self) -> Result<IfStatement, ParseError> {
389 self.expect_reserved("if", "an `if` statement")?;
390 let mut branches = Vec::new();
391 let condition = self.parse_and_or_list()?;
392 self.skip_separators();
393 self.expect_reserved("then", "an `if` statement")?;
394 let body = self.parse_program(&["elif", "else", "fi"])?;
395 branches.push((condition, body));
396
397 let mut otherwise = None;
398 loop {
399 match self.peek_reserved() {
400 Some("elif") => {
401 self.position += 1;
402 let condition = self.parse_and_or_list()?;
403 self.skip_separators();
404 self.expect_reserved("then", "an `elif` branch")?;
405 let body = self.parse_program(&["elif", "else", "fi"])?;
406 branches.push((condition, body));
407 }
408 Some("else") => {
409 self.position += 1;
410 otherwise = Some(self.parse_program(&["fi"])?);
411 break;
412 }
413 _ => break,
414 }
415 }
416 self.expect_reserved("fi", "an `if` statement")?;
417 Ok(IfStatement {
418 branches,
419 otherwise,
420 })
421 }
422
423 fn parse_for(&mut self) -> Result<ForLoop, ParseError> {
424 self.expect_reserved("for", "a `for` loop")?;
425 let line = self.line();
426 if matches!(self.peek_kind(), Some(TokenKind::LeftParen)) {
428 return Err(ParseError::syntax(
429 line,
430 "C-style `for (( ... ))` loops are not supported; use `for x in ...` over a list, or a `while` loop with `i=$(( i + 1 ))`",
431 ));
432 }
433 let Some(TokenKind::Word(word)) = self.peek_kind() else {
434 return Err(ParseError::syntax(line, "expected a `for` loop variable"));
435 };
436 let Some(variable) = word.as_literal().map(str::to_owned) else {
437 return Err(ParseError::syntax(
438 line,
439 "a `for` loop variable must be a plain name",
440 ));
441 };
442 if !is_valid_name(&variable) {
443 return Err(ParseError::syntax(
444 line,
445 format!("{variable:?} is not a valid `for` loop variable name"),
446 ));
447 }
448 self.position += 1;
449 self.expect_reserved("in", "a `for` loop")?;
450
451 let mut words = Vec::new();
452 while let Some(TokenKind::Word(raw)) = self.peek_kind() {
453 if self.peek_reserved() == Some("do") {
454 break;
455 }
456 let raw = raw.clone();
457 self.position += 1;
458 let depth = self.depth;
459 words.push(convert_word(&raw, line, depth)?);
460 }
461
462 self.skip_separators();
463 self.expect_reserved("do", "a `for` loop")?;
464 let body = self.parse_program(&["done"])?;
465 self.expect_reserved("done", "a `for` loop")?;
466 Ok(ForLoop {
467 variable,
468 words,
469 body,
470 })
471 }
472
473 fn parse_case(&mut self) -> Result<CaseStatement, ParseError> {
475 self.expect_reserved("case", "a `case` statement")?;
476 let line = self.line();
477 let Some(TokenKind::Word(raw)) = self.peek_kind() else {
478 return Err(ParseError::syntax(
479 line,
480 "expected a word to match after `case`",
481 ));
482 };
483 let raw = raw.clone();
484 let depth = self.depth;
485 self.position += 1;
486 let subject = convert_word(&raw, line, depth)?;
487 self.skip_newlines();
488 self.expect_reserved("in", "a `case` statement")?;
489
490 let mut clauses = Vec::new();
491 loop {
492 self.skip_separators();
493 if self.peek_reserved() == Some("esac") {
494 break;
495 }
496 if self.peek().is_none() {
497 let line = self.line();
498 return Err(ParseError::syntax(
499 line,
500 "expected `esac` in a `case` statement",
501 ));
502 }
503 clauses.push(self.parse_case_clause()?);
504 }
505 self.expect_reserved("esac", "a `case` statement")?;
506 Ok(CaseStatement { subject, clauses })
507 }
508
509 fn parse_case_clause(&mut self) -> Result<CaseClause, ParseError> {
511 if matches!(self.peek_kind(), Some(TokenKind::LeftParen)) {
514 self.position += 1;
515 }
516
517 let mut patterns = vec![self.parse_case_pattern()?];
518 while matches!(self.peek_kind(), Some(TokenKind::Pipe)) {
519 self.position += 1;
520 self.skip_newlines();
521 patterns.push(self.parse_case_pattern()?);
522 }
523 if !matches!(self.peek_kind(), Some(TokenKind::RightParen)) {
524 let line = self.line();
525 return Err(ParseError::syntax(
526 line,
527 "expected `)` to close a `case` pattern list",
528 ));
529 }
530 self.position += 1;
531
532 let body = self.parse_program(&["esac"])?;
533 if matches!(self.peek_kind(), Some(TokenKind::DoubleSemicolon)) {
534 self.position += 1;
535 } else if self.peek_reserved() != Some("esac") {
536 let line = self.line();
537 return Err(ParseError::syntax(
538 line,
539 "expected `;;` to end a `case` clause",
540 ));
541 }
542 Ok(CaseClause { patterns, body })
543 }
544
545 fn parse_case_pattern(&mut self) -> Result<CasePattern, ParseError> {
547 let line = self.line();
548 let Some(TokenKind::Word(raw)) = self.peek_kind() else {
549 let found = self
550 .peek_kind()
551 .map_or_else(|| "end of script".to_owned(), TokenKind::to_string);
552 return Err(ParseError::syntax(
553 line,
554 format!("expected a `case` pattern, found {found}"),
555 ));
556 };
557 let raw = raw.clone();
558 let depth = self.depth;
559 self.position += 1;
560
561 if raw.as_literal() == Some("*") {
564 return Ok(CasePattern::Any);
565 }
566
567 let word = convert_word(&raw, line, depth)?;
568 if word_is_constant(&raw) {
569 if let Some((character, meaning)) = literal_pattern_metacharacter(&raw) {
570 return Err(ParseError::syntax(
571 line,
572 unsupported_case_pattern(character, meaning),
573 ));
574 }
575 return Ok(CasePattern::Literal(word));
576 }
577 Ok(CasePattern::Expanded(word))
578 }
579
580 fn parse_while(&mut self, until: bool) -> Result<WhileLoop, ParseError> {
581 let keyword = if until { "until" } else { "while" };
582 let context = format!("an `{keyword}` loop");
583 self.expect_reserved(keyword, &context)?;
584 let condition = self.parse_and_or_list()?;
585 self.skip_separators();
586 self.expect_reserved("do", &context)?;
587 let body = self.parse_program(&["done"])?;
588 self.expect_reserved("done", &context)?;
589 Ok(WhileLoop {
590 condition,
591 body,
592 until,
593 })
594 }
595
596 fn parse_and_or_list(&mut self) -> Result<AndOrList, ParseError> {
597 let first = self.parse_pipeline()?;
598 let mut rest = Vec::new();
599 loop {
600 let operator = match self.peek_kind() {
601 Some(TokenKind::AndAnd) => AndOr::And,
602 Some(TokenKind::OrOr) => AndOr::Or,
603 _ => break,
604 };
605 self.position += 1;
606 self.skip_newlines();
607 rest.push((operator, self.parse_pipeline()?));
608 }
609 Ok(AndOrList { first, rest })
610 }
611
612 fn parse_pipeline(&mut self) -> Result<Pipeline, ParseError> {
613 let negated = self.eat_pipeline_negation();
617 let mut commands = vec![self.parse_simple_command()?];
618 while matches!(self.peek_kind(), Some(TokenKind::Pipe)) {
619 self.position += 1;
620 self.skip_newlines();
621 commands.push(self.parse_simple_command()?);
622 }
623 Ok(Pipeline { commands, negated })
624 }
625
626 fn eat_pipeline_negation(&mut self) -> bool {
627 let Some(TokenKind::Word(word)) = self.peek_kind() else {
628 return false;
629 };
630 if word.as_literal() != Some("!") {
631 return false;
632 }
633 self.position += 1;
634 true
635 }
636
637 fn parse_simple_command(&mut self) -> Result<SimpleCommand, ParseError> {
638 let mut assignments = Vec::new();
639 let mut words = Vec::new();
640 let mut redirect: Option<Redirect> = None;
641 let mut here_doc: Option<Word> = None;
642 let mut after_empty_assignment = false;
645
646 loop {
647 match self.peek_kind() {
648 Some(TokenKind::Word(raw)) => {
649 if words.is_empty() && self.peek_reserved().is_some() {
650 break;
651 }
652 let raw = raw.clone();
653 let line = self.line();
654 let depth = self.depth;
655 self.position += 1;
656 let assignment = if words.is_empty() {
657 split_assignment(&raw)
658 } else {
659 None
660 };
661 if let Some((name, value)) = assignment {
662 after_empty_assignment = value.parts.is_empty();
663 assignments.push(Assignment {
664 name,
665 value: convert_word(&value, line, depth)?,
666 });
667 continue;
668 }
669 after_empty_assignment = false;
670 words.push(convert_word(&raw, line, depth)?);
671 }
672 Some(TokenKind::Great | TokenKind::GreatGreat) => {
673 let append = matches!(self.peek_kind(), Some(TokenKind::GreatGreat));
674 let line = self.line();
675 let depth = self.depth;
676 self.position += 1;
677 let Some(TokenKind::Word(raw)) = self.peek_kind() else {
678 return Err(ParseError::syntax(
679 line,
680 "expected an in-memory buffer name after a redirection operator",
681 ));
682 };
683 let raw = raw.clone();
684 self.position += 1;
685 if redirect.is_some() {
686 return Err(ParseError::syntax(
687 line,
688 "a command accepts at most one buffer redirection",
689 ));
690 }
691 after_empty_assignment = false;
692 redirect = Some(Redirect {
693 append,
694 target: convert_word(&raw, line, depth)?,
695 });
696 }
697 Some(TokenKind::Ampersand) => {
700 let line = self.line();
701 return Err(ParseError::syntax(
702 line,
703 "backgrounding with `&` is not supported: this shell has no job control, so `&` can only mean something it cannot do",
704 ));
705 }
706 Some(TokenKind::LeftParen) => {
710 let line = self.line();
711 if after_empty_assignment {
712 return Err(ParseError::syntax(
713 line,
714 "bash array literals `name=(a b c)` are not supported: arrays here are real JSON, so write `name='[\"a\",\"b\",\"c\"]'` or `name=$(... | jq ...)` and index it with `${name[0]}`",
715 ));
716 }
717 if matches!(
718 self.tokens.get(self.position + 1).map(|token| &token.kind),
719 Some(TokenKind::LeftParen)
720 ) {
721 return Err(ParseError::syntax(
722 line,
723 "the arithmetic command `(( ... ))` is not supported; use the arithmetic expansion `x=$(( ... ))`, or `[ ... ]` to test a value",
724 ));
725 }
726 return Err(ParseError::syntax(
727 line,
728 "subshells `( ... )` are not supported: this shell forks no processes; use a function instead",
729 ));
730 }
731 Some(TokenKind::RightParen) => {
732 let line = self.line();
733 return Err(ParseError::syntax(line, "unexpected `)`"));
734 }
735 Some(TokenKind::LeftBrace) => {
737 let line = self.line();
738 return Err(ParseError::syntax(
739 line,
740 "brace command groups `{ ...; }` are not supported; only function bodies use braces",
741 ));
742 }
743 Some(TokenKind::HereDoc(raw)) => {
745 let raw = raw.clone();
746 let line = self.line();
747 let depth = self.depth;
748 self.position += 1;
749 if here_doc.is_some() {
750 return Err(ParseError::syntax(
751 line,
752 "a command accepts at most one here-document",
753 ));
754 }
755 after_empty_assignment = false;
756 here_doc = Some(convert_word(&raw, line, depth)?);
757 }
758 Some(TokenKind::LessParen) => {
759 let line = self.line();
760 return Err(ParseError::syntax(
761 line,
762 "process substitution `<( ... )` is not supported: this shell forks no processes and has no file descriptors",
763 ));
764 }
765 Some(TokenKind::Less) => {
766 let line = self.line();
767 return Err(ParseError::syntax(
768 line,
769 "input redirection `<` is not supported: there are no files; pipe a value or `cat` a named buffer instead",
770 ));
771 }
772 _ => break,
773 }
774 }
775
776 if words.is_empty() && assignments.is_empty() {
777 let line = self.line();
778 let found = self
779 .peek_kind()
780 .map_or_else(|| "end of script".to_owned(), TokenKind::to_string);
781 return Err(ParseError::syntax(
782 line,
783 format!("expected a command, found {found}"),
784 ));
785 }
786
787 Ok(SimpleCommand {
788 assignments,
789 words,
790 redirect,
791 here_doc,
792 })
793 }
794}
795
796const CASE_METACHARACTERS: &[(char, &str)] = &[
804 ('*', "any run of characters"),
805 ('?', "any single character"),
806 ('[', "a character class"),
807];
808
809pub(crate) fn pattern_metacharacter(text: &str) -> Option<(char, &'static str)> {
811 text.chars().find_map(|character| {
812 CASE_METACHARACTERS
813 .iter()
814 .find(|(candidate, _)| *candidate == character)
815 .map(|(candidate, meaning)| (*candidate, *meaning))
816 })
817}
818
819pub(crate) fn unsupported_case_pattern(character: char, meaning: &str) -> String {
824 format!(
825 "a `case` pattern here is literal text, so `{character}` — which would match {meaning} in bash — is not supported; spell the value out, add another `PATTERN|PATTERN` alternative, quote it as `'{character}'` to match the character itself, or use `*)` for the default branch"
826 )
827}
828
829pub(crate) fn expanded_case_pattern(character: char, meaning: &str) -> String {
831 format!(
832 "this `case` pattern expanded to text containing `{character}`, which would match {meaning} in bash; patterns here are literal text, and quoting cannot exempt an expanded one because its quotes are already gone — build the pattern without `{character}`, or branch with `if` and `jq` instead"
833 )
834}
835
836fn word_is_constant(word: &RawWord) -> bool {
838 fn parts_are_constant(parts: &[RawPart]) -> bool {
839 parts.iter().all(|part| match part {
840 RawPart::Literal(_) | RawPart::SingleQuoted(_) => true,
841 RawPart::DoubleQuoted(inner) => parts_are_constant(inner),
842 RawPart::Parameter(_) | RawPart::CommandSubstitution(_) | RawPart::Arithmetic(_) => {
843 false
844 }
845 })
846 }
847 parts_are_constant(&word.parts)
848}
849
850fn literal_pattern_metacharacter(word: &RawWord) -> Option<(char, &'static str)> {
855 word.parts.iter().find_map(|part| match part {
856 RawPart::Literal(text) => pattern_metacharacter(text),
857 _ => None,
858 })
859}
860
861fn is_valid_name(name: &str) -> bool {
862 let mut characters = name.chars();
863 let Some(first) = characters.next() else {
864 return false;
865 };
866 if !(first.is_ascii_alphabetic() || first == '_') {
867 return false;
868 }
869 characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
870}
871
872fn split_assignment(word: &RawWord) -> Option<(String, RawWord)> {
874 let RawPart::Literal(first) = word.parts.first()? else {
875 return None;
876 };
877 let equals = first.find('=')?;
878 let name = &first[..equals];
879 if !is_valid_name(name) {
880 return None;
881 }
882 let remainder = &first[equals + 1..];
883 let mut parts = Vec::new();
884 if !remainder.is_empty() {
885 parts.push(RawPart::Literal(remainder.to_owned()));
886 }
887 parts.extend(word.parts.iter().skip(1).cloned());
888 Some((name.to_owned(), RawWord { parts }))
889}
890
891fn convert_word(raw: &RawWord, line: usize, depth: u32) -> Result<Word, ParseError> {
892 Ok(Word {
893 parts: convert_parts(&raw.parts, line, depth)?,
894 })
895}
896
897fn convert_parts(raw: &[RawPart], line: usize, depth: u32) -> Result<Vec<WordPart>, ParseError> {
898 raw.iter()
899 .map(|part| convert_part(part, line, depth))
900 .collect::<Result<Vec<_>, _>>()
901}
902
903fn convert_part(raw: &RawPart, line: usize, depth: u32) -> Result<WordPart, ParseError> {
904 Ok(match raw {
905 RawPart::Literal(text) => WordPart::Literal(text.clone()),
906 RawPart::SingleQuoted(text) => WordPart::SingleQuoted(text.clone()),
907 RawPart::DoubleQuoted(parts) => WordPart::DoubleQuoted(convert_parts(parts, line, depth)?),
908 RawPart::Parameter(parameter) => {
909 WordPart::Parameter(convert_parameter(parameter, line, depth)?)
910 }
911 RawPart::CommandSubstitution(body) => {
914 if depth >= MAX_NESTING_DEPTH {
915 return Err(too_deep(line, "command substitution `$( ... )`"));
916 }
917 WordPart::CommandSubstitution(parse_nested(body, depth + 1)?)
918 }
919 RawPart::Arithmetic(body) => WordPart::Arithmetic(parse_arithmetic(body, line, depth)?),
920 })
921}
922
923fn convert_parameter(raw: &RawParameter, line: usize, depth: u32) -> Result<Parameter, ParseError> {
924 Ok(match raw {
925 RawParameter::Named { name, indices } => Parameter::Named {
926 name: name.clone(),
927 indices: indices
928 .iter()
929 .map(|index| convert_word(index, line, depth))
930 .collect::<Result<Vec<_>, _>>()?,
931 },
932 RawParameter::Positional(position) => Parameter::Positional(*position),
933 RawParameter::AllPositional => Parameter::AllPositional,
934 RawParameter::AllPositionalJoined => Parameter::AllPositionalJoined,
935 RawParameter::PositionalCount => Parameter::PositionalCount,
936 RawParameter::LastStatus => Parameter::LastStatus,
937 })
938}
939
940#[derive(Clone, Debug, PartialEq)]
945enum ArithToken {
946 Integer(i64),
947 Float(f64),
948 Name(String),
949 Symbol(&'static str),
950}
951
952enum ArithSymbol {
958 Kept,
959 Rejected(&'static str),
960}
961
962const ARITH_SYMBOLS: &[(&str, ArithSymbol)] = &[
964 (
965 "**",
966 ArithSymbol::Rejected("`**` is not supported; multiply repeatedly, or use `jq pow`"),
967 ),
968 (
969 "++",
970 ArithSymbol::Rejected("`++` is not supported; write `i=$(( i + 1 ))`"),
971 ),
972 (
973 "--",
974 ArithSymbol::Rejected("`--` is not supported; write `i=$(( i - 1 ))`"),
975 ),
976 ("+=", ArithSymbol::Rejected(COMPOUND_ASSIGNMENT)),
977 ("-=", ArithSymbol::Rejected(COMPOUND_ASSIGNMENT)),
978 ("*=", ArithSymbol::Rejected(COMPOUND_ASSIGNMENT)),
979 ("/=", ArithSymbol::Rejected(COMPOUND_ASSIGNMENT)),
980 ("%=", ArithSymbol::Rejected(COMPOUND_ASSIGNMENT)),
981 ("<<", ArithSymbol::Rejected(BITWISE)),
982 (">>", ArithSymbol::Rejected(BITWISE)),
983 ("&&", ArithSymbol::Kept),
984 ("||", ArithSymbol::Kept),
985 ("<=", ArithSymbol::Kept),
986 (">=", ArithSymbol::Kept),
987 ("==", ArithSymbol::Kept),
988 ("!=", ArithSymbol::Kept),
989 ("+", ArithSymbol::Kept),
990 ("-", ArithSymbol::Kept),
991 ("*", ArithSymbol::Kept),
992 ("/", ArithSymbol::Kept),
993 ("%", ArithSymbol::Kept),
994 ("(", ArithSymbol::Kept),
995 (")", ArithSymbol::Kept),
996 ("<", ArithSymbol::Kept),
997 (">", ArithSymbol::Kept),
998 ("!", ArithSymbol::Kept),
999 (
1000 "=",
1001 ArithSymbol::Rejected(
1002 "assignment inside `$(( ... ))` is not supported; assign the expansion instead, as `name=$(( ... ))`",
1003 ),
1004 ),
1005 ("?", ArithSymbol::Rejected(TERNARY)),
1006 (":", ArithSymbol::Rejected(TERNARY)),
1007 ("&", ArithSymbol::Rejected(BITWISE)),
1008 ("|", ArithSymbol::Rejected(BITWISE)),
1009 ("^", ArithSymbol::Rejected(BITWISE)),
1010 ("~", ArithSymbol::Rejected(BITWISE)),
1011 (
1012 ",",
1013 ArithSymbol::Rejected("the comma operator is not supported; write one expansion per value"),
1014 ),
1015];
1016
1017const COMPOUND_ASSIGNMENT: &str =
1018 "compound assignment is not supported inside `$(( ... ))`; write `name=$(( name + 1 ))`";
1019const BITWISE: &str = "bitwise operators are not supported; this arithmetic is numeric only, so use `jq` for bit manipulation";
1020const TERNARY: &str = "the ternary `? :` is not supported; use `if`/`else`";
1021
1022fn tokenize_arithmetic(source: &str, line: usize) -> Result<Vec<ArithToken>, ParseError> {
1023 let mut tokens = Vec::new();
1024 let bytes = source.as_bytes();
1025 let mut index = 0;
1026 while index < bytes.len() {
1027 if tokens.len() >= MAX_ARITHMETIC_TOKENS {
1028 return Err(ParseError::syntax(
1029 line,
1030 format!(
1031 "an arithmetic expansion may hold at most {MAX_ARITHMETIC_TOKENS} tokens; split the calculation across assignments"
1032 ),
1033 ));
1034 }
1035 let character = source[index..].chars().next().unwrap_or('\0');
1038 if character.is_ascii_whitespace() {
1039 index += 1;
1040 continue;
1041 }
1042 if character.is_ascii_digit() {
1043 let start = index;
1044 while index < bytes.len() && (bytes[index] as char).is_ascii_digit() {
1045 index += 1;
1046 }
1047 if index < bytes.len() && bytes[index] == b'.' {
1048 index += 1;
1049 while index < bytes.len() && (bytes[index] as char).is_ascii_digit() {
1050 index += 1;
1051 }
1052 let literal = &source[start..index];
1053 let value = literal.parse::<f64>().map_err(|_| {
1054 ParseError::syntax(line, format!("invalid arithmetic literal {literal:?}"))
1055 })?;
1056 tokens.push(ArithToken::Float(value));
1057 } else {
1058 let literal = &source[start..index];
1059 let value = literal.parse::<i64>().map_err(|_| {
1060 ParseError::syntax(
1061 line,
1062 format!("arithmetic literal {literal:?} is out of range"),
1063 )
1064 })?;
1065 tokens.push(ArithToken::Integer(value));
1066 }
1067 continue;
1068 }
1069 if character.is_ascii_alphabetic() || character == '_' || character == '$' {
1070 if character == '$' {
1071 index += 1;
1072 }
1073 let start = index;
1074 while index < bytes.len()
1075 && ((bytes[index] as char).is_ascii_alphanumeric() || bytes[index] == b'_')
1076 {
1077 index += 1;
1078 }
1079 if start == index {
1080 return Err(ParseError::syntax(
1081 line,
1082 "expected a variable name after `$` in an arithmetic expansion",
1083 ));
1084 }
1085 tokens.push(ArithToken::Name(source[start..index].to_owned()));
1086 continue;
1087 }
1088 let (matched, symbol) = ARITH_SYMBOLS
1089 .iter()
1090 .find(|(symbol, _)| source[index..].starts_with(*symbol))
1091 .ok_or_else(|| {
1092 ParseError::syntax(
1093 line,
1094 format!("unsupported character {character:?} in an arithmetic expansion"),
1095 )
1096 })?;
1097 match symbol {
1098 ArithSymbol::Kept => {}
1099 ArithSymbol::Rejected(reason) => return Err(ParseError::syntax(line, *reason)),
1100 }
1101 index += matched.len();
1102 tokens.push(ArithToken::Symbol(matched));
1103 }
1104 Ok(tokens)
1105}
1106
1107struct ArithParser {
1108 tokens: Vec<ArithToken>,
1109 position: usize,
1110 line: usize,
1111 depth: u32,
1113}
1114
1115fn parse_arithmetic(source: &str, line: usize, depth: u32) -> Result<ArithExpr, ParseError> {
1116 let tokens = tokenize_arithmetic(source, line)?;
1117 if tokens.is_empty() {
1118 return Err(ParseError::syntax(line, "empty arithmetic expansion"));
1119 }
1120 let mut parser = ArithParser {
1121 tokens,
1122 position: 0,
1123 line,
1124 depth,
1125 };
1126 let expression = parser.parse_or()?;
1127 if parser.position != parser.tokens.len() {
1128 return Err(ParseError::syntax(
1129 line,
1130 "trailing tokens in an arithmetic expansion",
1131 ));
1132 }
1133 Ok(expression)
1134}
1135
1136impl ArithParser {
1137 fn peek_symbol(&self) -> Option<&'static str> {
1138 match self.tokens.get(self.position) {
1139 Some(ArithToken::Symbol(symbol)) => Some(symbol),
1140 _ => None,
1141 }
1142 }
1143
1144 fn eat_symbol(&mut self, symbol: &str) -> bool {
1145 if self.peek_symbol() == Some(symbol) {
1146 self.position += 1;
1147 return true;
1148 }
1149 false
1150 }
1151
1152 fn parse_binary_level(
1153 &mut self,
1154 operators: &[(&str, ArithBinaryOp)],
1155 next: fn(&mut Self) -> Result<ArithExpr, ParseError>,
1156 ) -> Result<ArithExpr, ParseError> {
1157 let mut left = next(self)?;
1158 while let Some(symbol) = self.peek_symbol() {
1159 let Some((_, operator)) = operators.iter().find(|(text, _)| *text == symbol) else {
1160 break;
1161 };
1162 let operator = *operator;
1163 self.position += 1;
1164 let right = next(self)?;
1165 left = ArithExpr::Binary(operator, Box::new(left), Box::new(right));
1166 }
1167 Ok(left)
1168 }
1169
1170 fn parse_or(&mut self) -> Result<ArithExpr, ParseError> {
1171 self.parse_binary_level(&[("||", ArithBinaryOp::Or)], Self::parse_and)
1172 }
1173
1174 fn parse_and(&mut self) -> Result<ArithExpr, ParseError> {
1175 self.parse_binary_level(&[("&&", ArithBinaryOp::And)], Self::parse_equality)
1176 }
1177
1178 fn parse_equality(&mut self) -> Result<ArithExpr, ParseError> {
1179 self.parse_binary_level(
1180 &[
1181 ("==", ArithBinaryOp::Equal),
1182 ("!=", ArithBinaryOp::NotEqual),
1183 ],
1184 Self::parse_relational,
1185 )
1186 }
1187
1188 fn parse_relational(&mut self) -> Result<ArithExpr, ParseError> {
1189 self.parse_binary_level(
1190 &[
1191 ("<=", ArithBinaryOp::LessOrEqual),
1192 (">=", ArithBinaryOp::GreaterOrEqual),
1193 ("<", ArithBinaryOp::Less),
1194 (">", ArithBinaryOp::Greater),
1195 ],
1196 Self::parse_additive,
1197 )
1198 }
1199
1200 fn parse_additive(&mut self) -> Result<ArithExpr, ParseError> {
1201 self.parse_binary_level(
1202 &[("+", ArithBinaryOp::Add), ("-", ArithBinaryOp::Subtract)],
1203 Self::parse_multiplicative,
1204 )
1205 }
1206
1207 fn parse_multiplicative(&mut self) -> Result<ArithExpr, ParseError> {
1208 self.parse_binary_level(
1209 &[
1210 ("*", ArithBinaryOp::Multiply),
1211 ("/", ArithBinaryOp::Divide),
1212 ("%", ArithBinaryOp::Remainder),
1213 ],
1214 Self::parse_unary,
1215 )
1216 }
1217
1218 fn parse_unary(&mut self) -> Result<ArithExpr, ParseError> {
1219 if self.eat_symbol("-") {
1220 return Ok(ArithExpr::Unary(
1221 ArithUnaryOp::Negate,
1222 Box::new(self.parse_unary()?),
1223 ));
1224 }
1225 if self.eat_symbol("+") {
1226 return self.parse_unary();
1227 }
1228 if self.eat_symbol("!") {
1229 return Ok(ArithExpr::Unary(
1230 ArithUnaryOp::Not,
1231 Box::new(self.parse_unary()?),
1232 ));
1233 }
1234 self.parse_primary()
1235 }
1236
1237 fn parse_primary(&mut self) -> Result<ArithExpr, ParseError> {
1238 let line = self.line;
1239 match self.tokens.get(self.position).cloned() {
1240 Some(ArithToken::Integer(value)) => {
1241 self.position += 1;
1242 Ok(ArithExpr::Integer(value))
1243 }
1244 Some(ArithToken::Float(value)) => {
1245 self.position += 1;
1246 Ok(ArithExpr::Float(value))
1247 }
1248 Some(ArithToken::Name(name)) => {
1249 self.position += 1;
1250 Ok(ArithExpr::Variable(name))
1251 }
1252 Some(ArithToken::Symbol("(")) => {
1255 if self.depth >= MAX_NESTING_DEPTH {
1256 return Err(too_deep(line, "an arithmetic expansion"));
1257 }
1258 self.position += 1;
1259 self.depth += 1;
1260 let inner = self.parse_or();
1261 self.depth -= 1;
1262 let inner = inner?;
1263 if !self.eat_symbol(")") {
1264 return Err(ParseError::syntax(
1265 line,
1266 "unbalanced parentheses in an arithmetic expansion",
1267 ));
1268 }
1269 Ok(inner)
1270 }
1271 Some(ArithToken::Symbol(symbol)) => Err(ParseError::syntax(
1272 line,
1273 format!("unexpected `{symbol}` in an arithmetic expansion"),
1274 )),
1275 None => Err(ParseError::syntax(
1276 line,
1277 "arithmetic expansion ended unexpectedly",
1278 )),
1279 }
1280 }
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285 use crate::ast::{ArithBinaryOp, ArithExpr, CasePattern, Statement, WordPart};
1286
1287 use super::{ParseError, parse};
1288
1289 fn syntax_error(source: &str) -> String {
1290 match parse(source).expect_err("must be rejected") {
1291 ParseError::Syntax { message, .. } => message,
1292 ParseError::Lex(error) => error.message,
1293 }
1294 }
1295
1296 #[test]
1297 fn parses_assignments_pipelines_and_lists() {
1298 let program = parse("x=1\necho $x | grep 1 && echo ok || echo no").expect("valid script");
1299 assert_eq!(program.statements.len(), 2);
1300 let Statement::List(list) = &program.statements[1] else {
1301 panic!("expected a list");
1302 };
1303 assert_eq!(list.first.commands.len(), 2);
1304 assert_eq!(list.rest.len(), 2);
1305 }
1306
1307 #[test]
1308 fn parses_control_flow_and_functions() {
1309 let program = parse(
1310 "greet() { echo \"hi $1\"; }\nfor name in a b; do greet $name; done\nwhile false; do break; done\nif true; then echo y; elif false; then echo m; else echo n; fi",
1311 )
1312 .expect("valid script");
1313 assert_eq!(program.statements.len(), 4);
1314 assert!(matches!(program.statements[0], Statement::Function(_)));
1315 assert!(matches!(program.statements[1], Statement::For(_)));
1316 assert!(matches!(program.statements[2], Statement::While(_)));
1317 assert!(matches!(program.statements[3], Statement::If(_)));
1318 }
1319
1320 #[test]
1321 fn parses_buffer_redirections() {
1322 let program = parse("echo hi > buf\necho there >> buf").expect("valid script");
1323 let Statement::List(list) = &program.statements[0] else {
1324 panic!("expected a list");
1325 };
1326 let redirect = list.first.commands[0]
1327 .redirect
1328 .as_ref()
1329 .expect("a redirect");
1330 assert!(!redirect.append);
1331 let Statement::List(list) = &program.statements[1] else {
1332 panic!("expected a list");
1333 };
1334 assert!(
1335 list.first.commands[0]
1336 .redirect
1337 .as_ref()
1338 .expect("a redirect")
1339 .append
1340 );
1341 }
1342
1343 #[test]
1344 fn parses_arithmetic_with_precedence() {
1345 let program = parse("echo $(( 1 + 2 * 3 ))").expect("valid script");
1346 let Statement::List(list) = &program.statements[0] else {
1347 panic!("expected a list");
1348 };
1349 let WordPart::Arithmetic(expression) = &list.first.commands[0].words[1].parts[0] else {
1350 panic!("expected arithmetic");
1351 };
1352 let ArithExpr::Binary(ArithBinaryOp::Add, left, right) = expression else {
1353 panic!("expected addition at the root, found {expression:?}");
1354 };
1355 assert_eq!(**left, ArithExpr::Integer(1));
1356 assert!(matches!(
1357 **right,
1358 ArithExpr::Binary(ArithBinaryOp::Multiply, _, _)
1359 ));
1360 }
1361
1362 #[test]
1363 fn backgrounding_is_a_hard_parse_error() {
1364 let message = syntax_error("sleep 1 &");
1365 assert!(message.contains("backgrounding"), "{message}");
1366 assert!(message.contains("job control"), "{message}");
1367 }
1368
1369 #[test]
1370 fn dropped_grammar_is_rejected_by_name() {
1371 assert!(syntax_error("(echo hi)").contains("subshells"));
1372 assert!(syntax_error("{ echo hi; }").contains("brace command groups"));
1373 assert!(syntax_error("cat <<<\"$x\"").contains("here-string"));
1374 assert!(syntax_error("diff <(a) b").contains("process substitution"));
1375 assert!(syntax_error("cat < file").contains("input redirection"));
1376 assert!(syntax_error("select x in a; do echo $x; done").contains("select"));
1377 assert!(syntax_error("function f { echo hi; }").contains("`function` keyword"));
1378 assert!(syntax_error("esac").contains("without a matching `case`"));
1379 }
1380
1381 #[test]
1382 fn parses_case_statements_with_alternatives_and_a_default() {
1383 let program =
1384 parse("case $x in\n a|b) echo ab ;;\n ready) echo go ;;\n *) echo other ;;\nesac")
1385 .expect("valid script");
1386 let Statement::Case(statement) = &program.statements[0] else {
1387 panic!(
1388 "expected a case statement, found {:?}",
1389 program.statements[0]
1390 );
1391 };
1392 assert_eq!(statement.clauses.len(), 3);
1393 assert_eq!(statement.clauses[0].patterns.len(), 2);
1394 assert!(matches!(statement.clauses[2].patterns[0], CasePattern::Any));
1395 }
1396
1397 #[test]
1398 fn a_final_case_clause_may_omit_its_terminator() {
1399 let program = parse("case $x in a) echo a ;; *) echo b\nesac").expect("valid script");
1400 let Statement::Case(statement) = &program.statements[0] else {
1401 panic!("expected a case statement");
1402 };
1403 assert_eq!(statement.clauses.len(), 2);
1404 }
1405
1406 #[test]
1407 fn case_patterns_that_would_glob_are_rejected_by_name() {
1408 for (source, expected) in [
1411 ("case $f in *.json) echo j ;; esac", "any run of characters"),
1412 ("case $f in a?c) echo q ;; esac", "any single character"),
1413 ("case $f in [ab]) echo c ;; esac", "a character class"),
1414 ] {
1415 let message = syntax_error(source);
1416 assert!(message.contains(expected), "{source}: {message}");
1417 assert!(message.contains("literal text"), "{source}: {message}");
1418 }
1419
1420 assert!(parse("case $f in '*') echo star ;; esac").is_ok());
1422
1423 let program = parse("case $f in \\*) echo star ;; esac").expect("valid script");
1427 let Statement::Case(statement) = &program.statements[0] else {
1428 panic!("expected a case statement");
1429 };
1430 assert!(matches!(
1431 statement.clauses[0].patterns[0],
1432 CasePattern::Literal(_)
1433 ));
1434 assert!(parse("case $f in a\\*b) echo star ;; esac").is_ok());
1435 }
1436
1437 #[test]
1438 fn malformed_case_statements_are_reported() {
1439 assert!(syntax_error("case $x in a) echo a ;;").contains("expected `esac`"));
1440 assert!(syntax_error("case $x in a echo a ;; esac").contains("expected `)`"));
1441 assert!(syntax_error("case in a) echo a ;; esac").contains("expected `in`"));
1442 }
1443
1444 #[test]
1445 fn a_here_document_becomes_the_command_input() {
1446 let program = parse("jq . <<EOF\n{\"a\": 1}\nEOF\n").expect("valid script");
1447 let Statement::List(list) = &program.statements[0] else {
1448 panic!("expected a list");
1449 };
1450 let command = &list.first.commands[0];
1451 assert_eq!(command.words.len(), 2);
1452 let body = command.here_doc.as_ref().expect("a here-document");
1453 assert_eq!(body.as_literal(), Some("{\"a\": 1}"));
1454 }
1455
1456 #[test]
1457 fn a_command_accepts_at_most_one_here_document() {
1458 assert!(syntax_error("cat <<A <<B\na\nA\nb\nB\n").contains("at most one here-document"));
1459 }
1460
1461 #[test]
1462 fn parses_until_loops_and_nested_control_flow() {
1463 let program = parse(
1464 "outer() {\n for a in 1 2; do\n until false; do\n if true; then break; fi\n done\n done\n}\nouter",
1465 )
1466 .expect("nested control flow composes");
1467 assert_eq!(program.statements.len(), 2);
1468 let Statement::Function(definition) = &program.statements[0] else {
1469 panic!("expected a function definition");
1470 };
1471 let Statement::For(loop_statement) = &definition.body.statements[0] else {
1472 panic!("expected a for loop");
1473 };
1474 let Statement::While(inner) = &loop_statement.body.statements[0] else {
1475 panic!("expected an until loop");
1476 };
1477 assert!(inner.until);
1478 assert!(matches!(inner.body.statements[0], Statement::If(_)));
1479 }
1480
1481 #[test]
1482 fn functions_cannot_shadow_rejected_commands() {
1483 let message = syntax_error("eval() { echo hi; }");
1484 assert!(
1485 message.contains("cannot define a function named"),
1486 "{message}"
1487 );
1488 }
1489
1490 #[test]
1491 fn globbing_characters_parse_as_literal_words() {
1492 let program = parse("echo *").expect("an unquoted `*` is an ordinary character");
1493 let Statement::List(list) = &program.statements[0] else {
1494 panic!("expected a list");
1495 };
1496 assert_eq!(
1497 list.first.commands[0].words[1].parts,
1498 vec![WordPart::Literal("*".to_owned())]
1499 );
1500 }
1501
1502 #[test]
1503 fn command_substitution_is_parsed_recursively() {
1504 let program = parse("x=$(echo hi)").expect("valid script");
1505 let Statement::List(list) = &program.statements[0] else {
1506 panic!("expected a list");
1507 };
1508 let assignment = &list.first.commands[0].assignments[0];
1509 assert_eq!(assignment.name, "x");
1510 assert!(assignment.value.is_bare_command_substitution());
1511 }
1512
1513 #[test]
1514 fn unterminated_blocks_are_reported() {
1515 assert!(syntax_error("if true; then echo hi").contains("expected `fi`"));
1516 assert!(syntax_error("for x in a; do echo $x").contains("expected `done`"));
1517 assert!(syntax_error("f() { echo hi").contains("expected `}`"));
1518 }
1519}