deno_task_shell/
parser.rs

1// Copyright 2018-2025 the Deno authors. MIT license.
2
3use anyhow::Result;
4use anyhow::bail;
5use monch::*;
6
7// Shell grammar rules this is loosely based on:
8// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_10_02
9
10#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
11#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SequentialList {
14  pub items: Vec<SequentialListItem>,
15}
16
17#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
18#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct SequentialListItem {
21  pub is_async: bool,
22  pub sequence: Sequence,
23}
24
25#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
26#[cfg_attr(
27  feature = "serialization",
28  serde(rename_all = "camelCase", tag = "kind")
29)]
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum Sequence {
32  /// `MY_VAR=5`
33  ShellVar(EnvVar),
34  /// `cmd_name <args...>`, `cmd1 | cmd2`
35  Pipeline(Pipeline),
36  /// `cmd1 && cmd2 || cmd3`
37  BooleanList(Box<BooleanList>),
38}
39
40#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
41#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Pipeline {
44  /// `! pipeline`
45  pub negated: bool,
46  pub inner: PipelineInner,
47}
48
49impl From<Pipeline> for Sequence {
50  fn from(p: Pipeline) -> Self {
51    Sequence::Pipeline(p)
52  }
53}
54
55#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
56#[cfg_attr(
57  feature = "serialization",
58  serde(rename_all = "camelCase", tag = "kind")
59)]
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum PipelineInner {
62  /// Ex. `cmd_name <args...>`
63  Command(Command),
64  /// `cmd1 | cmd2`
65  PipeSequence(Box<PipeSequence>),
66}
67
68impl From<PipeSequence> for PipelineInner {
69  fn from(p: PipeSequence) -> Self {
70    PipelineInner::PipeSequence(Box::new(p))
71  }
72}
73
74#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
75#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
76#[derive(Copy, Clone, Debug, PartialEq, Eq)]
77pub enum BooleanListOperator {
78  // &&
79  And,
80  // ||
81  Or,
82}
83
84impl BooleanListOperator {
85  pub fn as_str(&self) -> &'static str {
86    match self {
87      BooleanListOperator::And => "&&",
88      BooleanListOperator::Or => "||",
89    }
90  }
91
92  pub fn moves_next_for_exit_code(&self, exit_code: i32) -> bool {
93    *self == BooleanListOperator::Or && exit_code != 0
94      || *self == BooleanListOperator::And && exit_code == 0
95  }
96}
97
98#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
99#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct BooleanList {
102  pub current: Sequence,
103  pub op: BooleanListOperator,
104  pub next: Sequence,
105}
106
107#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
108#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
109#[derive(Copy, Clone, Debug, PartialEq, Eq)]
110pub enum PipeSequenceOperator {
111  // |
112  Stdout,
113  // |&
114  StdoutStderr,
115}
116
117#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
118#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct PipeSequence {
121  pub current: Command,
122  pub op: PipeSequenceOperator,
123  pub next: PipelineInner,
124}
125
126impl From<PipeSequence> for Sequence {
127  fn from(p: PipeSequence) -> Self {
128    Sequence::Pipeline(Pipeline {
129      negated: false,
130      inner: p.into(),
131    })
132  }
133}
134
135#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
136#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct Command {
139  pub inner: CommandInner,
140  pub redirect: Option<Redirect>,
141}
142
143#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
144#[cfg_attr(
145  feature = "serialization",
146  serde(rename_all = "camelCase", tag = "kind")
147)]
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum CommandInner {
150  /// `cmd_name <args...>`
151  Simple(SimpleCommand),
152  /// `(list)`
153  Subshell(Box<SequentialList>),
154}
155
156impl From<Command> for Sequence {
157  fn from(c: Command) -> Self {
158    Pipeline {
159      negated: false,
160      inner: c.into(),
161    }
162    .into()
163  }
164}
165
166#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
167#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct SimpleCommand {
170  pub env_vars: Vec<EnvVar>,
171  pub args: Vec<Word>,
172}
173
174impl From<SimpleCommand> for Command {
175  fn from(c: SimpleCommand) -> Self {
176    Command {
177      redirect: None,
178      inner: CommandInner::Simple(c),
179    }
180  }
181}
182
183impl From<SimpleCommand> for PipelineInner {
184  fn from(c: SimpleCommand) -> Self {
185    PipelineInner::Command(c.into())
186  }
187}
188
189impl From<Command> for PipelineInner {
190  fn from(c: Command) -> Self {
191    PipelineInner::Command(c)
192  }
193}
194
195impl From<SimpleCommand> for Sequence {
196  fn from(c: SimpleCommand) -> Self {
197    let command: Command = c.into();
198    command.into()
199  }
200}
201
202#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
203#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
204#[derive(Debug, PartialEq, Eq, Clone)]
205pub struct EnvVar {
206  pub name: String,
207  pub value: Word,
208}
209
210impl EnvVar {
211  pub fn new(name: String, value: Word) -> Self {
212    EnvVar { name, value }
213  }
214}
215
216#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
217#[derive(Debug, PartialEq, Eq, Clone)]
218pub struct Word(Vec<WordPart>);
219
220impl Word {
221  pub fn new_string(text: &str) -> Self {
222    Word(vec![WordPart::Quoted(vec![WordPart::Text(
223      text.to_string(),
224    )])])
225  }
226
227  pub fn new_word(text: &str) -> Self {
228    Word(vec![WordPart::Text(text.to_string())])
229  }
230
231  pub fn parts(&self) -> &Vec<WordPart> {
232    &self.0
233  }
234
235  pub fn into_parts(self) -> Vec<WordPart> {
236    self.0
237  }
238}
239
240#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
241#[cfg_attr(
242  feature = "serialization",
243  serde(rename_all = "camelCase", tag = "kind", content = "value")
244)]
245#[derive(Debug, PartialEq, Eq, Clone)]
246pub enum WordPart {
247  /// Text in the string (ex. `some text`)
248  Text(String),
249  /// Variable substitution (ex. `$MY_VAR`)
250  Variable(String),
251  /// Tilde expansion (ex. `~`)
252  Tilde,
253  /// Command substitution (ex. `$(command)`)
254  Command(SequentialList),
255  /// Quoted string (ex. `"hello"` or `'test'`)
256  Quoted(Vec<WordPart>),
257}
258
259#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
260#[cfg_attr(
261  feature = "serialization",
262  serde(rename_all = "camelCase", tag = "kind", content = "fd")
263)]
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub enum RedirectFd {
266  Fd(u32),
267  StdoutStderr,
268}
269
270#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
271#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct Redirect {
274  pub maybe_fd: Option<RedirectFd>,
275  pub op: RedirectOp,
276  pub io_file: IoFile,
277}
278
279#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
280#[cfg_attr(
281  feature = "serialization",
282  serde(rename_all = "camelCase", tag = "kind", content = "value")
283)]
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub enum IoFile {
286  /// Filename to redirect to/from (ex. `file.txt`` in `cmd < file.txt`)
287  Word(Word),
288  /// File descriptor to redirect to/from (ex. `2` in `cmd >&2`)
289  Fd(u32),
290}
291
292#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
293#[cfg_attr(
294  feature = "serialization",
295  serde(rename_all = "camelCase", tag = "kind", content = "value")
296)]
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub enum RedirectOp {
299  Input(RedirectOpInput),
300  Output(RedirectOpOutput),
301}
302
303#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
304#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub enum RedirectOpInput {
307  /// <
308  Redirect,
309}
310
311#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
312#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub enum RedirectOpOutput {
315  /// >
316  Overwrite,
317  /// >>
318  Append,
319}
320
321pub fn parse(input: &str) -> Result<SequentialList> {
322  match parse_sequential_list(input) {
323    Ok((input, expr)) => {
324      if input.trim().is_empty() {
325        if expr.items.is_empty() {
326          bail!("Empty command.")
327        } else {
328          Ok(expr)
329        }
330      } else {
331        fail_for_trailing_input(input)
332          .into_result()
333          .map_err(|err| err.into())
334      }
335    }
336    Err(ParseError::Backtrace) => fail_for_trailing_input(input)
337      .into_result()
338      .map_err(|err| err.into()),
339    Err(ParseError::Failure(e)) => e.into_result().map_err(|err| err.into()),
340  }
341}
342
343fn parse_sequential_list(input: &str) -> ParseResult<SequentialList> {
344  let (input, items) = separated_list(
345    terminated(parse_sequential_list_item, skip_whitespace),
346    terminated(
347      skip_whitespace,
348      or(
349        map(parse_sequential_list_op, |_| ()),
350        map(parse_async_list_op, |_| ()),
351      ),
352    ),
353  )(input)?;
354  Ok((input, SequentialList { items }))
355}
356
357fn parse_sequential_list_item(input: &str) -> ParseResult<SequentialListItem> {
358  let (input, sequence) = parse_sequence(input)?;
359  Ok((
360    input,
361    SequentialListItem {
362      is_async: maybe(parse_async_list_op)(input)?.1.is_some(),
363      sequence,
364    },
365  ))
366}
367
368fn parse_sequence(input: &str) -> ParseResult<Sequence> {
369  let (input, current) = terminated(
370    or(
371      parse_shell_var_command,
372      map(parse_pipeline, Sequence::Pipeline),
373    ),
374    skip_whitespace,
375  )(input)?;
376
377  Ok(match parse_boolean_list_op(input) {
378    Ok((input, op)) => {
379      let (input, next_sequence) = assert_exists(
380        &parse_sequence,
381        "Expected command following boolean operator.",
382      )(input)?;
383      (
384        input,
385        Sequence::BooleanList(Box::new(BooleanList {
386          current,
387          op,
388          next: next_sequence,
389        })),
390      )
391    }
392    Err(ParseError::Backtrace) => (input, current),
393    Err(err) => return Err(err),
394  })
395}
396
397fn parse_shell_var_command(input: &str) -> ParseResult<Sequence> {
398  let env_vars_input = input;
399  let (input, mut env_vars) = if_not_empty(parse_env_vars)(input)?;
400  let (input, args) = parse_command_args(input)?;
401  if !args.is_empty() {
402    return ParseError::backtrace();
403  }
404  if env_vars.len() > 1 {
405    ParseError::fail(
406      env_vars_input,
407      "Cannot set multiple environment variables when there is no following command.",
408    )
409  } else {
410    ParseResult::Ok((input, Sequence::ShellVar(env_vars.remove(0))))
411  }
412}
413
414/// Parses a pipeline, which is a sequence of one or more commands.
415/// https://www.gnu.org/software/bash/manual/html_node/Pipelines.html
416fn parse_pipeline(input: &str) -> ParseResult<Pipeline> {
417  let (input, maybe_negated) = maybe(parse_negated_op)(input)?;
418  let (input, inner) = parse_pipeline_inner(input)?;
419
420  let pipeline = Pipeline {
421    negated: maybe_negated.is_some(),
422    inner,
423  };
424
425  Ok((input, pipeline))
426}
427
428fn parse_pipeline_inner(input: &str) -> ParseResult<PipelineInner> {
429  let original_input = input;
430  let (input, command) = parse_command(input)?;
431
432  let (input, inner) = match parse_pipe_sequence_op(input) {
433    Ok((input, op)) => {
434      let (input, next_inner) = assert_exists(
435        &parse_pipeline_inner,
436        "Expected command following pipeline operator.",
437      )(input)?;
438
439      if command.redirect.is_some() {
440        return ParseError::fail(
441          original_input,
442          "Redirects in pipe sequence commands are currently not supported.",
443        );
444      }
445
446      (
447        input,
448        PipelineInner::PipeSequence(Box::new(PipeSequence {
449          current: command,
450          op,
451          next: next_inner,
452        })),
453      )
454    }
455    Err(ParseError::Backtrace) => (input, PipelineInner::Command(command)),
456    Err(err) => return Err(err),
457  };
458
459  Ok((input, inner))
460}
461
462fn parse_command(input: &str) -> ParseResult<Command> {
463  let (input, inner) = terminated(
464    or(
465      map(parse_subshell, |l| CommandInner::Subshell(Box::new(l))),
466      map(parse_simple_command, CommandInner::Simple),
467    ),
468    skip_whitespace,
469  )(input)?;
470
471  let before_redirects_input = input;
472  let (input, mut redirects) =
473    many0(terminated(parse_redirect, skip_whitespace))(input)?;
474
475  if redirects.len() > 1 {
476    return ParseError::fail(
477      before_redirects_input,
478      "Multiple redirects are currently not supported.",
479    );
480  }
481
482  let command = Command {
483    redirect: redirects.pop(),
484    inner,
485  };
486
487  Ok((input, command))
488}
489
490fn parse_simple_command(input: &str) -> ParseResult<SimpleCommand> {
491  let (input, env_vars) = parse_env_vars(input)?;
492  let (input, args) = if_not_empty(parse_command_args)(input)?;
493  ParseResult::Ok((input, SimpleCommand { env_vars, args }))
494}
495
496fn parse_command_args(input: &str) -> ParseResult<Vec<Word>> {
497  many_till(
498    terminated(parse_shell_arg, assert_whitespace_or_end_and_skip),
499    or4(
500      parse_list_op,
501      map(parse_redirect, |_| ()),
502      map(parse_pipe_sequence_op, |_| ()),
503      map(ch(')'), |_| ()),
504    ),
505  )(input)
506}
507
508fn parse_shell_arg(input: &str) -> ParseResult<Word> {
509  let (input, value) = parse_word(input)?;
510  if value.parts().is_empty() {
511    ParseError::backtrace()
512  } else {
513    Ok((input, value))
514  }
515}
516
517fn parse_list_op(input: &str) -> ParseResult<()> {
518  or(
519    map(parse_boolean_list_op, |_| ()),
520    map(or(parse_sequential_list_op, parse_async_list_op), |_| ()),
521  )(input)
522}
523
524fn parse_boolean_list_op(input: &str) -> ParseResult<BooleanListOperator> {
525  or(
526    map(parse_op_str(BooleanListOperator::And.as_str()), |_| {
527      BooleanListOperator::And
528    }),
529    map(parse_op_str(BooleanListOperator::Or.as_str()), |_| {
530      BooleanListOperator::Or
531    }),
532  )(input)
533}
534
535fn parse_sequential_list_op(input: &str) -> ParseResult<&str> {
536  terminated(tag(";"), skip_whitespace)(input)
537}
538
539fn parse_async_list_op(input: &str) -> ParseResult<&str> {
540  parse_op_str("&")(input)
541}
542
543fn parse_negated_op(input: &str) -> ParseResult<&str> {
544  terminated(
545    tag("!"),
546    // must have whitespace following
547    whitespace,
548  )(input)
549}
550
551fn parse_op_str<'a>(
552  operator: &str,
553) -> impl Fn(&'a str) -> ParseResult<'a, &'a str> {
554  debug_assert!(operator == "&&" || operator == "||" || operator == "&");
555  let operator = operator.to_string();
556  terminated(
557    tag(operator),
558    terminated(check_not(one_of("|&")), skip_whitespace),
559  )
560}
561
562fn parse_pipe_sequence_op(input: &str) -> ParseResult<PipeSequenceOperator> {
563  terminated(
564    or(
565      map(tag("|&"), |_| PipeSequenceOperator::StdoutStderr),
566      map(ch('|'), |_| PipeSequenceOperator::Stdout),
567    ),
568    terminated(check_not(one_of("|&")), skip_whitespace),
569  )(input)
570}
571
572fn parse_redirect(input: &str) -> ParseResult<Redirect> {
573  // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_07
574  let (input, maybe_fd) = maybe(parse_u32)(input)?;
575  let (input, maybe_ampersand) = if maybe_fd.is_none() {
576    maybe(ch('&'))(input)?
577  } else {
578    (input, None)
579  };
580  let (input, op) = or3(
581    map(tag(">>"), |_| RedirectOp::Output(RedirectOpOutput::Append)),
582    map(or(tag(">"), tag(">|")), |_| {
583      RedirectOp::Output(RedirectOpOutput::Overwrite)
584    }),
585    map(ch('<'), |_| RedirectOp::Input(RedirectOpInput::Redirect)),
586  )(input)?;
587  let (input, io_file) = or(
588    map(preceded(ch('&'), parse_u32), IoFile::Fd),
589    map(preceded(skip_whitespace, parse_word), IoFile::Word),
590  )(input)?;
591
592  let maybe_fd = if let Some(fd) = maybe_fd {
593    Some(RedirectFd::Fd(fd))
594  } else if maybe_ampersand.is_some() {
595    Some(RedirectFd::StdoutStderr)
596  } else {
597    None
598  };
599
600  Ok((
601    input,
602    Redirect {
603      maybe_fd,
604      op,
605      io_file,
606    },
607  ))
608}
609
610fn parse_env_vars(input: &str) -> ParseResult<Vec<EnvVar>> {
611  many0(terminated(parse_env_var, skip_whitespace))(input)
612}
613
614fn parse_env_var(input: &str) -> ParseResult<EnvVar> {
615  let (input, name) = parse_env_var_name(input)?;
616  let (input, _) = ch('=')(input)?;
617  let (input, value) = with_error_context(
618    terminated(parse_env_var_value, assert_whitespace_or_end),
619    "Invalid environment variable value.",
620  )(input)?;
621  Ok((input, EnvVar::new(name.to_string(), value)))
622}
623
624fn parse_env_var_name(input: &str) -> ParseResult<&str> {
625  if_not_empty(take_while(is_valid_env_var_char))(input)
626}
627
628fn parse_env_var_value(input: &str) -> ParseResult<Word> {
629  parse_word(input)
630}
631
632fn parse_word(input: &str) -> ParseResult<Word> {
633  let parse_quoted_or_unquoted = or(
634    map(parse_quoted_string, |parts| vec![WordPart::Quoted(parts)]),
635    parse_unquoted_word,
636  );
637  let (input, mut parts) = parse_quoted_or_unquoted(input)?;
638  if parts.is_empty() {
639    Ok((input, Word(parts)))
640  } else {
641    let (input, result) = many0(if_not_empty(parse_quoted_or_unquoted))(input)?;
642    parts.extend(result.into_iter().flatten());
643    Ok((input, Word(parts)))
644  }
645}
646
647fn parse_unquoted_word(input: &str) -> ParseResult<Vec<WordPart>> {
648  assert(
649    parse_word_parts(ParseWordPartsMode::Unquoted),
650    |result| {
651      result
652        .ok()
653        .map(|(_, parts)| {
654          if parts.len() == 1 {
655            if let WordPart::Text(text) = &parts[0] {
656              return !is_reserved_word(text);
657            }
658          }
659          true
660        })
661        .unwrap_or(true)
662    },
663    "Unsupported reserved word.",
664  )(input)
665}
666
667fn parse_quoted_string(input: &str) -> ParseResult<Vec<WordPart>> {
668  // Strings may be up beside each other, and if they are they
669  // should be categorized as the same argument.
670  map(
671    many1(or(
672      map(parse_single_quoted_string, |text| {
673        vec![WordPart::Text(text.to_string())]
674      }),
675      parse_double_quoted_string,
676    )),
677    |vecs| vecs.into_iter().flatten().collect(),
678  )(input)
679}
680
681fn parse_single_quoted_string(input: &str) -> ParseResult<&str> {
682  // single quoted strings cannot contain a single quote
683  // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_02
684  delimited(
685    ch('\''),
686    take_while(|c| c != '\''),
687    with_failure_input(
688      input,
689      assert_exists(ch('\''), "Expected closing single quote."),
690    ),
691  )(input)
692}
693
694fn parse_double_quoted_string(input: &str) -> ParseResult<Vec<WordPart>> {
695  fn parse_words_within(input: &str) -> ParseResult<Vec<WordPart>> {
696    match parse_word_parts(ParseWordPartsMode::DoubleQuotes)(input) {
697      Ok((result_input, parts)) => {
698        if !result_input.is_empty() {
699          return ParseError::fail(
700            input,
701            format!(
702              "Failed parsing within double quotes. Unexpected character: {}",
703              result_input
704            ),
705          );
706        }
707        Ok((result_input, parts))
708      }
709      Err(err) => ParseError::fail(
710        input,
711        format!(
712          "Failed parsing within double quotes. {}",
713          match &err {
714            ParseError::Backtrace => "Could not determine expression.",
715            ParseError::Failure(parse_error_failure) =>
716              parse_error_failure.message.as_str(),
717          }
718        ),
719      ),
720    }
721  }
722
723  // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
724  let start_input = input;
725  let (mut input, _) = ch('"')(input)?;
726  let mut was_escape = false;
727  let mut pending_parts = Vec::new();
728  let mut iter = input.char_indices().peekable();
729  while let Some((index, c)) = iter.next() {
730    match c {
731      c if c == '$'
732        && !was_escape
733        && iter.peek().map(|(_, c)| *c) == Some('(') =>
734      {
735        let previous_input = &input[..index];
736        pending_parts.extend(parse_words_within(previous_input)?.1);
737        let next_input = &input[index..];
738        let (next_input, sequence) = with_error_context(
739          parse_command_substitution,
740          "Failed parsing command substitution in double quoted string.",
741        )(next_input)?;
742        pending_parts.push(WordPart::Command(sequence));
743        iter = next_input.char_indices().peekable();
744        input = next_input;
745      }
746      c if c == '`' && !was_escape => {
747        let previous_input = &input[..index];
748        pending_parts.extend(parse_words_within(previous_input)?.1);
749        let next_input = &input[index..];
750        let (next_input, sequence) = with_error_context(
751          parse_backticks_command_substitution,
752          "Failed parsing backticks in double quoted string.",
753        )(next_input)?;
754        pending_parts.push(WordPart::Command(sequence));
755        iter = next_input.char_indices().peekable();
756        input = next_input;
757      }
758      c if c == '"' && !was_escape => {
759        let inner_input = &input[..index];
760        let (_, parts) = parse_words_within(inner_input)?;
761        return Ok((
762          &input[index + 1..],
763          if pending_parts.is_empty() {
764            parts
765          } else {
766            pending_parts.extend(parts);
767            pending_parts
768          },
769        ));
770      }
771      '\\' => {
772        was_escape = true;
773      }
774      _ => {
775        was_escape = false;
776      }
777    }
778  }
779
780  ParseError::fail(start_input, "Expected closing double quote.")
781}
782
783#[derive(Clone, Copy, PartialEq, Eq)]
784enum ParseWordPartsMode {
785  DoubleQuotes,
786  Unquoted,
787}
788
789fn parse_word_parts(
790  mode: ParseWordPartsMode,
791) -> impl Fn(&str) -> ParseResult<Vec<WordPart>> {
792  fn parse_escaped_dollar_sign(input: &str) -> ParseResult<char> {
793    or(
794      parse_escaped_char('$'),
795      terminated(
796        ch('$'),
797        check_not(or(map(parse_env_var_name, |_| ()), map(ch('('), |_| ()))),
798      ),
799    )(input)
800  }
801
802  fn parse_special_shell_var(input: &str) -> ParseResult<char> {
803    // for now, these hard error
804    preceded(ch('$'), |input| {
805      if let Some(char) = input.chars().next() {
806        // $$ - process id
807        // $# - number of arguments in $*
808        // $* - list of arguments passed to the current process
809        if "$#*".contains(char) {
810          return ParseError::fail(
811            input,
812            format!("${char} is currently not supported."),
813          );
814        }
815      }
816      ParseError::backtrace()
817    })(input)
818  }
819
820  fn parse_escaped_char<'a>(
821    c: char,
822  ) -> impl Fn(&'a str) -> ParseResult<'a, char> {
823    preceded(ch('\\'), ch(c))
824  }
825
826  fn first_escaped_char<'a>(
827    mode: ParseWordPartsMode,
828  ) -> impl Fn(&'a str) -> ParseResult<'a, char> {
829    or7(
830      parse_special_shell_var,
831      parse_escaped_dollar_sign,
832      parse_escaped_char('~'),
833      parse_escaped_char('`'),
834      parse_escaped_char('"'),
835      parse_escaped_char('('),
836      or(
837        parse_escaped_char(')'),
838        if_true(parse_escaped_char('\''), move |_| {
839          mode == ParseWordPartsMode::DoubleQuotes
840        }),
841      ),
842    )
843  }
844
845  fn append_char(result: &mut Vec<WordPart>, c: char) {
846    if let Some(WordPart::Text(text)) = result.last_mut() {
847      text.push(c);
848    } else {
849      result.push(WordPart::Text(c.to_string()));
850    }
851  }
852
853  move |input| {
854    enum PendingPart<'a> {
855      Char(char),
856      Variable(&'a str),
857      Tilde,
858      Command(SequentialList),
859      Parts(Vec<WordPart>),
860    }
861
862    let original_input = input;
863    let (input, parts) = many0(or7(
864      or3(
865        map(tag("$?"), |_| PendingPart::Variable("?")),
866        map(first_escaped_char(mode), PendingPart::Char),
867        map(parse_command_substitution, PendingPart::Command),
868      ),
869      map(parse_backticks_command_substitution, PendingPart::Command),
870      map(ch('~'), |_| PendingPart::Tilde),
871      map(preceded(ch('$'), parse_env_var_name), PendingPart::Variable),
872      // words can have escaped spaces
873      map(
874        if_true(preceded(ch('\\'), ch(' ')), |_| {
875          mode == ParseWordPartsMode::Unquoted
876        }),
877        PendingPart::Char,
878      ),
879      map(
880        if_true(next_char, |&c| match mode {
881          ParseWordPartsMode::DoubleQuotes => c != '"',
882          ParseWordPartsMode::Unquoted => {
883            !c.is_whitespace() && !"~(){}<>|&;\"'".contains(c)
884          }
885        }),
886        PendingPart::Char,
887      ),
888      |input| match mode {
889        ParseWordPartsMode::DoubleQuotes => ParseError::backtrace(),
890        ParseWordPartsMode::Unquoted => {
891          let (input, parts) =
892            map(parse_quoted_string, |parts| vec![WordPart::Quoted(parts)])(
893              input,
894            )?;
895          Ok((input, PendingPart::Parts(parts)))
896        }
897      },
898    ))(input)?;
899
900    let mut result = Vec::new();
901    let mut parts = parts.into_iter().enumerate().peekable();
902    while let Some((i, part)) = parts.next() {
903      match part {
904        PendingPart::Char(c) => {
905          append_char(&mut result, c);
906        }
907        PendingPart::Tilde => {
908          if i == 0 {
909            if matches!(parts.peek(), None | Some((_, PendingPart::Char('/'))))
910            {
911              result.push(WordPart::Tilde);
912            } else {
913              return ParseError::fail(
914                original_input,
915                "Unsupported tilde expansion.",
916              );
917            }
918          } else {
919            append_char(&mut result, '~');
920          }
921        }
922        PendingPart::Command(s) => result.push(WordPart::Command(s)),
923        PendingPart::Variable(v) => {
924          result.push(WordPart::Variable(v.to_string()));
925        }
926        PendingPart::Parts(parts) => result.extend(parts),
927      }
928    }
929
930    Ok((input, result))
931  }
932}
933
934fn parse_command_substitution(input: &str) -> ParseResult<SequentialList> {
935  delimited(
936    tag("$("),
937    parse_sequential_list,
938    with_failure_input(
939      input,
940      assert_exists(
941        ch(')'),
942        "Expected closing parenthesis for command substitution.",
943      ),
944    ),
945  )(input)
946}
947
948fn parse_backticks_command_substitution(
949  input: &str,
950) -> ParseResult<SequentialList> {
951  let start_input = input;
952  let (input, _) = ch('`')(input)?;
953  let mut was_escape = false;
954  for (index, c) in input.char_indices() {
955    match c {
956      c if c == '`' && !was_escape => {
957        let inner_input = &input[..index];
958        let inner_input = inner_input.replace("\\`", "`");
959        let parts = match parse_sequential_list(&inner_input) {
960          Ok((result_input, parts)) => {
961            if !result_input.is_empty() {
962              return ParseError::fail(
963                input,
964                format!(
965                  "Failed parsing within backticks. Unexpected character: {}",
966                  result_input
967                ),
968              );
969            }
970            parts
971          }
972          Err(err) => {
973            return ParseError::fail(
974              input,
975              format!(
976                "Failed parsing within {}. {}",
977                if c == '`' {
978                  "backticks"
979                } else {
980                  "double quotes"
981                },
982                match &err {
983                  ParseError::Backtrace => "Could not determine expression.",
984                  ParseError::Failure(parse_error_failure) =>
985                    parse_error_failure.message.as_str(),
986                }
987              ),
988            );
989          }
990        };
991        return Ok((&input[index + 1..], parts));
992      }
993      '\\' => {
994        was_escape = true;
995      }
996      _ => {
997        was_escape = false;
998      }
999    }
1000  }
1001
1002  ParseError::fail(start_input, "Expected closing backtick.")
1003}
1004
1005fn parse_subshell(input: &str) -> ParseResult<SequentialList> {
1006  delimited(
1007    terminated(ch('('), skip_whitespace),
1008    parse_sequential_list,
1009    with_failure_input(
1010      input,
1011      assert_exists(ch(')'), "Expected closing parenthesis on subshell."),
1012    ),
1013  )(input)
1014}
1015
1016fn parse_u32(input: &str) -> ParseResult<u32> {
1017  let mut value: u32 = 0;
1018  let mut byte_index = 0;
1019  for c in input.chars() {
1020    if c.is_ascii_digit() {
1021      let shifted_val = match value.checked_mul(10) {
1022        Some(val) => val,
1023        None => return ParseError::backtrace(),
1024      };
1025      value = match shifted_val.checked_add(c.to_digit(10).unwrap()) {
1026        Some(val) => val,
1027        None => return ParseError::backtrace(),
1028      };
1029    } else if byte_index == 0 {
1030      return ParseError::backtrace();
1031    } else {
1032      break;
1033    }
1034    byte_index += c.len_utf8();
1035  }
1036  Ok((&input[byte_index..], value))
1037}
1038
1039fn assert_whitespace_or_end_and_skip(input: &str) -> ParseResult<()> {
1040  terminated(assert_whitespace_or_end, skip_whitespace)(input)
1041}
1042
1043fn assert_whitespace_or_end(input: &str) -> ParseResult<()> {
1044  if let Some(next_char) = input.chars().next() {
1045    if !next_char.is_whitespace()
1046      && !matches!(next_char, ';' | '&' | '|' | '(' | ')')
1047    {
1048      return Err(ParseError::Failure(fail_for_trailing_input(input)));
1049    }
1050  }
1051  Ok((input, ()))
1052}
1053
1054fn is_valid_env_var_char(c: char) -> bool {
1055  // [a-zA-Z0-9_]+
1056  c.is_ascii_alphanumeric() || c == '_'
1057}
1058
1059fn is_reserved_word(text: &str) -> bool {
1060  matches!(
1061    text,
1062    "if"
1063      | "then"
1064      | "else"
1065      | "elif"
1066      | "fi"
1067      | "do"
1068      | "done"
1069      | "case"
1070      | "esac"
1071      | "while"
1072      | "until"
1073      | "for"
1074      | "in"
1075  )
1076}
1077
1078fn fail_for_trailing_input(input: &str) -> ParseErrorFailure {
1079  ParseErrorFailure::new(input, "Unexpected character.")
1080}
1081
1082#[cfg(test)]
1083mod test {
1084  use super::*;
1085  use pretty_assertions::assert_eq;
1086
1087  #[test]
1088  fn test_main() {
1089    assert_eq!(parse("").err().unwrap().to_string(), "Empty command.");
1090    assert_eq!(
1091      parse("&& testing").err().unwrap().to_string(),
1092      concat!("Unexpected character.\n", "  && testing\n", "  ~",),
1093    );
1094    assert_eq!(
1095      parse("test { test").err().unwrap().to_string(),
1096      concat!("Unexpected character.\n", "  { test\n", "  ~",),
1097    );
1098    assert!(parse("cp test/* other").is_ok());
1099    assert!(parse("cp test/? other").is_ok());
1100    assert_eq!(
1101      parse("(test").err().unwrap().to_string(),
1102      concat!(
1103        "Expected closing parenthesis on subshell.\n",
1104        "  (test\n",
1105        "  ~"
1106      ),
1107    );
1108    assert_eq!(
1109      parse("cmd \"test").err().unwrap().to_string(),
1110      concat!("Expected closing double quote.\n", "  \"test\n", "  ~"),
1111    );
1112    assert_eq!(
1113      parse("cmd 'test").err().unwrap().to_string(),
1114      concat!("Expected closing single quote.\n", "  'test\n", "  ~"),
1115    );
1116    assert_eq!(
1117      parse("cmd \"test$(echo testing\"")
1118        .err()
1119        .unwrap()
1120        .to_string(),
1121      concat!(
1122        "Failed parsing command substitution in double quoted string.\n",
1123        "\n",
1124        "Expected closing double quote.\n",
1125        "  \"\n",
1126        "  ~"
1127      ),
1128    );
1129
1130    assert!(parse("( test ||other&&test;test);(t&est );").is_ok());
1131    assert!(parse("command --arg='value'").is_ok());
1132    assert!(parse("command --arg=\"value\"").is_ok());
1133    assert!(
1134      parse("deno run --allow-read=. --allow-write=./testing main.ts").is_ok(),
1135    );
1136  }
1137
1138  #[test]
1139  fn test_sequential_list() {
1140    run_test(
1141      parse_sequential_list,
1142      concat!(
1143        "Name=Value OtherVar=Other command arg1 || command2 arg12 arg13 ; ",
1144        "command3 && command4 & command5 ; export ENV6=5 ; ",
1145        "ENV7=other && command8 || command9 ; ",
1146        "cmd10 && (cmd11 || cmd12)"
1147      ),
1148      Ok(SequentialList {
1149        items: vec![
1150          SequentialListItem {
1151            is_async: false,
1152            sequence: Sequence::BooleanList(Box::new(BooleanList {
1153              current: SimpleCommand {
1154                env_vars: vec![
1155                  EnvVar::new("Name".to_string(), Word::new_word("Value")),
1156                  EnvVar::new("OtherVar".to_string(), Word::new_word("Other")),
1157                ],
1158                args: vec![Word::new_word("command"), Word::new_word("arg1")],
1159              }
1160              .into(),
1161              op: BooleanListOperator::Or,
1162              next: SimpleCommand {
1163                env_vars: vec![],
1164                args: vec![
1165                  Word::new_word("command2"),
1166                  Word::new_word("arg12"),
1167                  Word::new_word("arg13"),
1168                ],
1169              }
1170              .into(),
1171            })),
1172          },
1173          SequentialListItem {
1174            is_async: true,
1175            sequence: Sequence::BooleanList(Box::new(BooleanList {
1176              current: SimpleCommand {
1177                env_vars: vec![],
1178                args: vec![Word::new_word("command3")],
1179              }
1180              .into(),
1181              op: BooleanListOperator::And,
1182              next: SimpleCommand {
1183                env_vars: vec![],
1184                args: vec![Word::new_word("command4")],
1185              }
1186              .into(),
1187            })),
1188          },
1189          SequentialListItem {
1190            is_async: false,
1191            sequence: SimpleCommand {
1192              env_vars: vec![],
1193              args: vec![Word::new_word("command5")],
1194            }
1195            .into(),
1196          },
1197          SequentialListItem {
1198            is_async: false,
1199            sequence: SimpleCommand {
1200              env_vars: vec![],
1201              args: vec![Word::new_word("export"), Word::new_word("ENV6=5")],
1202            }
1203            .into(),
1204          },
1205          SequentialListItem {
1206            is_async: false,
1207            sequence: Sequence::BooleanList(Box::new(BooleanList {
1208              current: Sequence::ShellVar(EnvVar::new(
1209                "ENV7".to_string(),
1210                Word::new_word("other"),
1211              )),
1212              op: BooleanListOperator::And,
1213              next: Sequence::BooleanList(Box::new(BooleanList {
1214                current: SimpleCommand {
1215                  env_vars: vec![],
1216                  args: vec![Word::new_word("command8")],
1217                }
1218                .into(),
1219                op: BooleanListOperator::Or,
1220                next: SimpleCommand {
1221                  env_vars: vec![],
1222                  args: vec![Word::new_word("command9")],
1223                }
1224                .into(),
1225              })),
1226            })),
1227          },
1228          SequentialListItem {
1229            is_async: false,
1230            sequence: Sequence::BooleanList(Box::new(BooleanList {
1231              current: SimpleCommand {
1232                env_vars: vec![],
1233                args: vec![Word::new_word("cmd10")],
1234              }
1235              .into(),
1236              op: BooleanListOperator::And,
1237              next: Command {
1238                inner: CommandInner::Subshell(Box::new(SequentialList {
1239                  items: vec![SequentialListItem {
1240                    is_async: false,
1241                    sequence: Sequence::BooleanList(Box::new(BooleanList {
1242                      current: SimpleCommand {
1243                        env_vars: vec![],
1244                        args: vec![Word::new_word("cmd11")],
1245                      }
1246                      .into(),
1247                      op: BooleanListOperator::Or,
1248                      next: SimpleCommand {
1249                        env_vars: vec![],
1250                        args: vec![Word::new_word("cmd12")],
1251                      }
1252                      .into(),
1253                    })),
1254                  }],
1255                })),
1256                redirect: None,
1257              }
1258              .into(),
1259            })),
1260          },
1261        ],
1262      }),
1263    );
1264
1265    run_test(
1266      parse_sequential_list,
1267      "command1 ; command2 ; A='b' command3",
1268      Ok(SequentialList {
1269        items: vec![
1270          SequentialListItem {
1271            is_async: false,
1272            sequence: SimpleCommand {
1273              env_vars: vec![],
1274              args: vec![Word::new_word("command1")],
1275            }
1276            .into(),
1277          },
1278          SequentialListItem {
1279            is_async: false,
1280            sequence: SimpleCommand {
1281              env_vars: vec![],
1282              args: vec![Word::new_word("command2")],
1283            }
1284            .into(),
1285          },
1286          SequentialListItem {
1287            is_async: false,
1288            sequence: SimpleCommand {
1289              env_vars: vec![EnvVar::new(
1290                "A".to_string(),
1291                Word::new_string("b"),
1292              )],
1293              args: vec![Word::new_word("command3")],
1294            }
1295            .into(),
1296          },
1297        ],
1298      }),
1299    );
1300
1301    run_test(
1302      parse_sequential_list,
1303      "test &&",
1304      Err("Expected command following boolean operator."),
1305    );
1306
1307    run_test(
1308      parse_sequential_list,
1309      "command &",
1310      Ok(SequentialList {
1311        items: vec![SequentialListItem {
1312          is_async: true,
1313          sequence: SimpleCommand {
1314            env_vars: vec![],
1315            args: vec![Word::new_word("command")],
1316          }
1317          .into(),
1318        }],
1319      }),
1320    );
1321
1322    run_test(
1323      parse_sequential_list,
1324      "test | other",
1325      Ok(SequentialList {
1326        items: vec![SequentialListItem {
1327          is_async: false,
1328          sequence: PipeSequence {
1329            current: SimpleCommand {
1330              env_vars: vec![],
1331              args: vec![Word::new_word("test")],
1332            }
1333            .into(),
1334            op: PipeSequenceOperator::Stdout,
1335            next: SimpleCommand {
1336              env_vars: vec![],
1337              args: vec![Word::new_word("other")],
1338            }
1339            .into(),
1340          }
1341          .into(),
1342        }],
1343      }),
1344    );
1345
1346    run_test(
1347      parse_sequential_list,
1348      "test |& other",
1349      Ok(SequentialList {
1350        items: vec![SequentialListItem {
1351          is_async: false,
1352          sequence: PipeSequence {
1353            current: SimpleCommand {
1354              env_vars: vec![],
1355              args: vec![Word::new_word("test")],
1356            }
1357            .into(),
1358            op: PipeSequenceOperator::StdoutStderr,
1359            next: SimpleCommand {
1360              env_vars: vec![],
1361              args: vec![Word::new_word("other")],
1362            }
1363            .into(),
1364          }
1365          .into(),
1366        }],
1367      }),
1368    );
1369
1370    run_test(
1371      parse_sequential_list,
1372      "ENV=1 ENV2=3 && test",
1373      Err(
1374        "Cannot set multiple environment variables when there is no following command.",
1375      ),
1376    );
1377
1378    run_test(
1379      parse_sequential_list,
1380      "echo $MY_ENV;",
1381      Ok(SequentialList {
1382        items: vec![SequentialListItem {
1383          is_async: false,
1384          sequence: SimpleCommand {
1385            env_vars: vec![],
1386            args: vec![
1387              Word::new_word("echo"),
1388              Word(vec![WordPart::Variable("MY_ENV".to_string())]),
1389            ],
1390          }
1391          .into(),
1392        }],
1393      }),
1394    );
1395
1396    run_test(
1397      parse_sequential_list,
1398      "! cmd1 | cmd2 && cmd3",
1399      Ok(SequentialList {
1400        items: vec![SequentialListItem {
1401          is_async: false,
1402          sequence: Sequence::BooleanList(Box::new(BooleanList {
1403            current: Pipeline {
1404              negated: true,
1405              inner: PipeSequence {
1406                current: SimpleCommand {
1407                  args: vec![Word::new_word("cmd1")],
1408                  env_vars: vec![],
1409                }
1410                .into(),
1411                op: PipeSequenceOperator::Stdout,
1412                next: SimpleCommand {
1413                  args: vec![Word::new_word("cmd2")],
1414                  env_vars: vec![],
1415                }
1416                .into(),
1417              }
1418              .into(),
1419            }
1420            .into(),
1421            op: BooleanListOperator::And,
1422            next: SimpleCommand {
1423              args: vec![Word::new_word("cmd3")],
1424              env_vars: vec![],
1425            }
1426            .into(),
1427          })),
1428        }],
1429      }),
1430    );
1431  }
1432
1433  #[test]
1434  fn test_env_var() {
1435    run_test(
1436      parse_env_var,
1437      "Name=Value",
1438      Ok(EnvVar {
1439        name: "Name".to_string(),
1440        value: Word::new_word("Value"),
1441      }),
1442    );
1443    run_test(
1444      parse_env_var,
1445      "Name='quoted value'",
1446      Ok(EnvVar {
1447        name: "Name".to_string(),
1448        value: Word::new_string("quoted value"),
1449      }),
1450    );
1451    run_test(
1452      parse_env_var,
1453      "Name=\"double quoted value\"",
1454      Ok(EnvVar {
1455        name: "Name".to_string(),
1456        value: Word::new_string("double quoted value"),
1457      }),
1458    );
1459    run_test_with_end(
1460      parse_env_var,
1461      "Name= command_name",
1462      Ok(EnvVar {
1463        name: "Name".to_string(),
1464        value: Word(vec![]),
1465      }),
1466      " command_name",
1467    );
1468
1469    run_test(
1470      parse_env_var,
1471      "Name=$(test)",
1472      Ok(EnvVar {
1473        name: "Name".to_string(),
1474        value: Word(vec![WordPart::Command(SequentialList {
1475          items: vec![SequentialListItem {
1476            is_async: false,
1477            sequence: SimpleCommand {
1478              env_vars: vec![],
1479              args: vec![Word::new_word("test")],
1480            }
1481            .into(),
1482          }],
1483        })]),
1484      }),
1485    );
1486
1487    run_test(
1488      parse_env_var,
1489      "Name=$(OTHER=5)",
1490      Ok(EnvVar {
1491        name: "Name".to_string(),
1492        value: Word(vec![WordPart::Command(SequentialList {
1493          items: vec![SequentialListItem {
1494            is_async: false,
1495            sequence: Sequence::ShellVar(EnvVar {
1496              name: "OTHER".to_string(),
1497              value: Word::new_word("5"),
1498            }),
1499          }],
1500        })]),
1501      }),
1502    );
1503  }
1504
1505  #[test]
1506  fn test_single_quotes() {
1507    run_test(
1508      parse_quoted_string,
1509      "'test'",
1510      Ok(vec![WordPart::Text("test".to_string())]),
1511    );
1512    run_test(
1513      parse_quoted_string,
1514      r"'te\\'",
1515      Ok(vec![WordPart::Text(r"te\\".to_string())]),
1516    );
1517    run_test_with_end(
1518      parse_quoted_string,
1519      r"'te\'st'",
1520      Ok(vec![WordPart::Text(r"te\".to_string())]),
1521      "st'",
1522    );
1523    run_test(
1524      parse_quoted_string,
1525      "'  '",
1526      Ok(vec![WordPart::Text("  ".to_string())]),
1527    );
1528    run_test(
1529      parse_quoted_string,
1530      "'  ",
1531      Err("Expected closing single quote."),
1532    );
1533  }
1534
1535  #[test]
1536  fn test_single_quotes_mid_word() {
1537    run_test(
1538      parse_word,
1539      "--inspect='[::0]:3366'",
1540      Ok(Word(vec![
1541        WordPart::Text("--inspect=".to_string()),
1542        WordPart::Quoted(vec![WordPart::Text("[::0]:3366".to_string())]),
1543      ])),
1544    );
1545  }
1546
1547  #[test]
1548  fn test_double_quotes() {
1549    run_test(
1550      parse_quoted_string,
1551      r#""  ""#,
1552      Ok(vec![WordPart::Text("  ".to_string())]),
1553    );
1554    run_test(
1555      parse_quoted_string,
1556      r#""test""#,
1557      Ok(vec![WordPart::Text("test".to_string())]),
1558    );
1559    run_test(
1560      parse_quoted_string,
1561      r#""te\"\$\`st""#,
1562      Ok(vec![WordPart::Text(r#"te"$`st"#.to_string())]),
1563    );
1564    run_test(
1565      parse_quoted_string,
1566      r#""  "#,
1567      Err("Expected closing double quote."),
1568    );
1569    run_test(
1570      parse_quoted_string,
1571      r#""$Test""#,
1572      Ok(vec![WordPart::Variable("Test".to_string())]),
1573    );
1574    run_test(
1575      parse_quoted_string,
1576      r#""$Test,$Other_Test""#,
1577      Ok(vec![
1578        WordPart::Variable("Test".to_string()),
1579        WordPart::Text(",".to_string()),
1580        WordPart::Variable("Other_Test".to_string()),
1581      ]),
1582    );
1583    run_test(
1584      parse_quoted_string,
1585      r#""asdf`""#,
1586      Err(
1587        "Failed parsing backticks in double quoted string.\n\nExpected closing backtick.",
1588      ),
1589    );
1590
1591    run_test_with_end(
1592      parse_quoted_string,
1593      r#""test" asdf"#,
1594      Ok(vec![WordPart::Text("test".to_string())]),
1595      " asdf",
1596    );
1597
1598    run_test(
1599      parse_quoted_string,
1600      r#""test $(deno eval 'console.info("test")') test `backticks "test"` test""#,
1601      Ok(vec![
1602        WordPart::Text("test ".to_string()),
1603        WordPart::Command(SequentialList {
1604          items: Vec::from([SequentialListItem {
1605            is_async: false,
1606            sequence: Sequence::Pipeline(Pipeline {
1607              negated: false,
1608              inner: PipelineInner::Command(Command {
1609                redirect: None,
1610                inner: CommandInner::Simple(SimpleCommand {
1611                  env_vars: vec![],
1612                  args: Vec::from([
1613                    Word::new_word("deno"),
1614                    Word::new_word("eval"),
1615                    Word::new_string("console.info(\"test\")"),
1616                  ]),
1617                }),
1618              }),
1619            }),
1620          }]),
1621        }),
1622        WordPart::Text(" test ".to_string()),
1623        WordPart::Command(SequentialList {
1624          items: Vec::from([SequentialListItem {
1625            is_async: false,
1626            sequence: Sequence::Pipeline(Pipeline {
1627              negated: false,
1628              inner: PipelineInner::Command(Command {
1629                redirect: None,
1630                inner: CommandInner::Simple(SimpleCommand {
1631                  env_vars: vec![],
1632                  args: Vec::from([
1633                    Word::new_word("backticks"),
1634                    Word::new_string("test"),
1635                  ]),
1636                }),
1637              }),
1638            }),
1639          }]),
1640        }),
1641        WordPart::Text(" test".to_string()),
1642      ]),
1643    );
1644  }
1645
1646  #[test]
1647  fn tilde_expansion() {
1648    run_test(
1649      parse_word_parts(ParseWordPartsMode::Unquoted),
1650      r#"~test"#,
1651      Err("Unsupported tilde expansion."),
1652    );
1653    run_test(
1654      parse_word_parts(ParseWordPartsMode::Unquoted),
1655      r#"~+/test"#,
1656      Err("Unsupported tilde expansion."),
1657    );
1658    run_test(
1659      parse_word_parts(ParseWordPartsMode::Unquoted),
1660      r#"~/test"#,
1661      Ok(vec![WordPart::Tilde, WordPart::Text("/test".to_string())]),
1662    );
1663  }
1664
1665  #[test]
1666  fn test_parse_word() {
1667    run_test(parse_unquoted_word, "if", Err("Unsupported reserved word."));
1668    run_test(
1669      parse_unquoted_word,
1670      "$",
1671      Ok(vec![WordPart::Text("$".to_string())]),
1672    );
1673    // unsupported shell variables
1674    run_test(
1675      parse_unquoted_word,
1676      "$$",
1677      Err("$$ is currently not supported."),
1678    );
1679    run_test(
1680      parse_unquoted_word,
1681      "$#",
1682      Err("$# is currently not supported."),
1683    );
1684    run_test(
1685      parse_unquoted_word,
1686      "$*",
1687      Err("$* is currently not supported."),
1688    );
1689    run_test(
1690      parse_unquoted_word,
1691      "test\\ test",
1692      Ok(vec![WordPart::Text("test test".to_string())]),
1693    );
1694  }
1695
1696  #[test]
1697  fn test_parse_u32() {
1698    run_test(parse_u32, "999", Ok(999));
1699    run_test(parse_u32, "11", Ok(11));
1700    run_test(parse_u32, "0", Ok(0));
1701    run_test_with_end(parse_u32, "1>", Ok(1), ">");
1702    run_test(parse_u32, "-1", Err("backtrace"));
1703    run_test(parse_u32, "a", Err("backtrace"));
1704    run_test(
1705      parse_u32,
1706      "16116951273372934291112534924737",
1707      Err("backtrace"),
1708    );
1709    run_test(parse_u32, "4294967295", Ok(4294967295));
1710    run_test(parse_u32, "4294967296", Err("backtrace"));
1711  }
1712
1713  #[track_caller]
1714  fn run_test<'a, T: PartialEq + std::fmt::Debug>(
1715    combinator: impl Fn(&'a str) -> ParseResult<'a, T>,
1716    input: &'a str,
1717    expected: Result<T, &str>,
1718  ) {
1719    run_test_with_end(combinator, input, expected, "");
1720  }
1721
1722  #[track_caller]
1723  fn run_test_with_end<'a, T: PartialEq + std::fmt::Debug>(
1724    combinator: impl Fn(&'a str) -> ParseResult<'a, T>,
1725    input: &'a str,
1726    expected: Result<T, &str>,
1727    expected_end: &str,
1728  ) {
1729    match combinator(input) {
1730      Ok((input, value)) => {
1731        assert_eq!(value, expected.unwrap());
1732        assert_eq!(input, expected_end);
1733      }
1734      Err(ParseError::Backtrace) => {
1735        assert_eq!("backtrace", expected.err().unwrap());
1736      }
1737      Err(ParseError::Failure(err)) => {
1738        assert_eq!(
1739          err.message,
1740          match expected.err() {
1741            Some(err) => err,
1742            None =>
1743              panic!("Got error: {:#}", err.into_result::<T>().err().unwrap()),
1744          }
1745        );
1746      }
1747    }
1748  }
1749
1750  #[test]
1751  fn test_redirects() {
1752    let expected = Ok(Command {
1753      inner: CommandInner::Simple(SimpleCommand {
1754        env_vars: vec![],
1755        args: vec![Word::new_word("echo"), Word::new_word("1")],
1756      }),
1757      redirect: Some(Redirect {
1758        maybe_fd: None,
1759        op: RedirectOp::Output(RedirectOpOutput::Overwrite),
1760        io_file: IoFile::Word(Word(vec![WordPart::Text(
1761          "test.txt".to_string(),
1762        )])),
1763      }),
1764    });
1765
1766    run_test(parse_command, "echo 1 > test.txt", expected.clone());
1767    run_test(parse_command, "echo 1 >test.txt", expected.clone());
1768
1769    // append
1770    run_test(
1771      parse_command,
1772      r#"command >> "test.txt""#,
1773      Ok(Command {
1774        inner: CommandInner::Simple(SimpleCommand {
1775          env_vars: vec![],
1776          args: vec![Word::new_word("command")],
1777        }),
1778        redirect: Some(Redirect {
1779          maybe_fd: None,
1780          op: RedirectOp::Output(RedirectOpOutput::Append),
1781          io_file: IoFile::Word(Word(vec![WordPart::Quoted(vec![
1782            WordPart::Text("test.txt".to_string()),
1783          ])])),
1784        }),
1785      }),
1786    );
1787
1788    // fd
1789    run_test(
1790      parse_command,
1791      r#"command 2> test.txt"#,
1792      Ok(Command {
1793        inner: CommandInner::Simple(SimpleCommand {
1794          env_vars: vec![],
1795          args: vec![Word::new_word("command")],
1796        }),
1797        redirect: Some(Redirect {
1798          maybe_fd: Some(RedirectFd::Fd(2)),
1799          op: RedirectOp::Output(RedirectOpOutput::Overwrite),
1800          io_file: IoFile::Word(Word(vec![WordPart::Text(
1801            "test.txt".to_string(),
1802          )])),
1803        }),
1804      }),
1805    );
1806
1807    // both
1808    run_test(
1809      parse_command,
1810      r#"command &> test.txt"#,
1811      Ok(Command {
1812        inner: CommandInner::Simple(SimpleCommand {
1813          env_vars: vec![],
1814          args: vec![Word::new_word("command")],
1815        }),
1816        redirect: Some(Redirect {
1817          maybe_fd: Some(RedirectFd::StdoutStderr),
1818          op: RedirectOp::Output(RedirectOpOutput::Overwrite),
1819          io_file: IoFile::Word(Word(vec![WordPart::Text(
1820            "test.txt".to_string(),
1821          )])),
1822        }),
1823      }),
1824    );
1825
1826    // output redirect to fd
1827    run_test(
1828      parse_command,
1829      r#"command 2>&1"#,
1830      Ok(Command {
1831        inner: CommandInner::Simple(SimpleCommand {
1832          env_vars: vec![],
1833          args: vec![Word::new_word("command")],
1834        }),
1835        redirect: Some(Redirect {
1836          maybe_fd: Some(RedirectFd::Fd(2)),
1837          op: RedirectOp::Output(RedirectOpOutput::Overwrite),
1838          io_file: IoFile::Fd(1),
1839        }),
1840      }),
1841    );
1842
1843    // input redirect to fd
1844    run_test(
1845      parse_command,
1846      r#"command <&0"#,
1847      Ok(Command {
1848        inner: CommandInner::Simple(SimpleCommand {
1849          env_vars: vec![],
1850          args: vec![Word::new_word("command")],
1851        }),
1852        redirect: Some(Redirect {
1853          maybe_fd: None,
1854          op: RedirectOp::Input(RedirectOpInput::Redirect),
1855          io_file: IoFile::Fd(0),
1856        }),
1857      }),
1858    );
1859
1860    run_test_with_end(
1861      parse_command,
1862      "echo 1 1> stdout.txt 2> stderr.txt",
1863      Err("Multiple redirects are currently not supported."),
1864      "1> stdout.txt 2> stderr.txt",
1865    );
1866
1867    // redirect in pipeline sequence command should error
1868    run_test_with_end(
1869      parse_sequence,
1870      "echo 1 1> stdout.txt | cat",
1871      Err("Redirects in pipe sequence commands are currently not supported."),
1872      "echo 1 1> stdout.txt | cat",
1873    );
1874  }
1875
1876  #[cfg(feature = "serialization")]
1877  #[test]
1878  fn serializes_command_to_json() {
1879    assert_json_equals(
1880      serialize_to_json("./example > output.txt"),
1881      serde_json::json!({
1882        "items": [{
1883          "isAsync": false,
1884          "sequence": {
1885            "inner": {
1886              "inner": {
1887                "args": [[{
1888                  "kind": "text",
1889                  "value": "./example"
1890                }]],
1891                "envVars": [],
1892                "kind": "simple"
1893              },
1894              "kind": "command",
1895              "redirect": {
1896                "ioFile": {
1897                  "kind": "word",
1898                  "value": [{
1899                    "kind": "text",
1900                    "value": "output.txt"
1901                  }],
1902                },
1903                "maybeFd": null,
1904                "op": {
1905                  "kind": "output",
1906                  "value": "overwrite",
1907                }
1908              }
1909            },
1910            "kind": "pipeline",
1911            "negated": false
1912          }
1913        }]
1914      }),
1915    );
1916    assert_json_equals(
1917      serialize_to_json("./example 2> output.txt"),
1918      serde_json::json!({
1919        "items": [{
1920          "isAsync": false,
1921          "sequence": {
1922            "inner": {
1923              "inner": {
1924                "args": [[{
1925                  "kind": "text",
1926                  "value": "./example"
1927                }]],
1928                "envVars": [],
1929                "kind": "simple"
1930              },
1931              "kind": "command",
1932              "redirect": {
1933                "ioFile": {
1934                  "kind": "word",
1935                  "value": [{
1936                    "kind": "text",
1937                    "value": "output.txt"
1938                  }],
1939                },
1940                "maybeFd": {
1941                  "kind": "fd",
1942                  "fd": 2,
1943                },
1944                "op": {
1945                  "kind": "output",
1946                  "value": "overwrite",
1947                }
1948              }
1949            },
1950            "kind": "pipeline",
1951            "negated": false
1952          }
1953        }]
1954      }),
1955    );
1956    assert_json_equals(
1957      serialize_to_json("./example &> output.txt"),
1958      serde_json::json!({
1959        "items": [{
1960          "isAsync": false,
1961          "sequence": {
1962            "inner": {
1963              "inner": {
1964                "args": [[{
1965                  "kind": "text",
1966                  "value": "./example"
1967                }]],
1968                "envVars": [],
1969                "kind": "simple"
1970              },
1971              "kind": "command",
1972              "redirect": {
1973                "ioFile": {
1974                  "kind": "word",
1975                  "value": [{
1976                    "kind": "text",
1977                    "value": "output.txt"
1978                  }],
1979                },
1980                "maybeFd": {
1981                  "kind": "stdoutStderr"
1982                },
1983                "op": {
1984                  "kind": "output",
1985                  "value": "overwrite",
1986                }
1987              }
1988            },
1989            "kind": "pipeline",
1990            "negated": false
1991          }
1992        }]
1993      }),
1994    );
1995    assert_json_equals(
1996      serialize_to_json("./example < output.txt"),
1997      serde_json::json!({
1998        "items": [{
1999          "isAsync": false,
2000          "sequence": {
2001            "inner": {
2002              "inner": {
2003                "args": [[{
2004                  "kind": "text",
2005                  "value": "./example"
2006                }]],
2007                "envVars": [],
2008                "kind": "simple"
2009              },
2010              "kind": "command",
2011              "redirect": {
2012                "ioFile": {
2013                  "kind": "word",
2014                  "value": [{
2015                    "kind": "text",
2016                    "value": "output.txt"
2017                  }],
2018                },
2019                "maybeFd": null,
2020                "op": {
2021                  "kind": "input",
2022                  "value": "redirect",
2023                }
2024              }
2025            },
2026            "kind": "pipeline",
2027            "negated": false
2028          }
2029        }]
2030      }),
2031    );
2032
2033    assert_json_equals(
2034      serialize_to_json("./example <&0"),
2035      serde_json::json!({
2036        "items": [{
2037          "isAsync": false,
2038          "sequence": {
2039            "inner": {
2040              "inner": {
2041                "args": [[{
2042                  "kind": "text",
2043                  "value": "./example"
2044                }]],
2045                "envVars": [],
2046                "kind": "simple"
2047              },
2048              "kind": "command",
2049              "redirect": {
2050                "ioFile": {
2051                  "kind": "fd",
2052                  "value": 0,
2053                },
2054                "maybeFd": null,
2055                "op": {
2056                  "kind": "input",
2057                  "value": "redirect",
2058                }
2059              }
2060            },
2061            "kind": "pipeline",
2062            "negated": false
2063          }
2064        }]
2065      }),
2066    );
2067  }
2068
2069  #[cfg(feature = "serialization")]
2070  #[track_caller]
2071  fn assert_json_equals(
2072    actual: serde_json::Value,
2073    expected: serde_json::Value,
2074  ) {
2075    if actual != expected {
2076      let actual = serde_json::to_string_pretty(&actual).unwrap();
2077      let expected = serde_json::to_string_pretty(&expected).unwrap();
2078      assert_eq!(actual, expected);
2079    }
2080  }
2081
2082  #[cfg(feature = "serialization")]
2083  fn serialize_to_json(text: &str) -> serde_json::Value {
2084    let command = parse(text).unwrap();
2085    serde_json::to_value(command).unwrap()
2086  }
2087}