Repository: denoland/deno
File: ext/node/ops/shell.rs (Rust)

Code before:
}

/// Parses a shell command string into its arguments and shell operator suffix.
#[op2]
#[serde]
pub fn op_node_parse_shell_args(#[string] input: String) -> ParsedShellArgs {
  scan_and_split(&input)
}

/// Scan for the first unquoted shell operator (`<`, `>`, `|`, `&`, `;`)
/// and split the input into args and suffix. On non-Windows, respects
/// backslash escapes outside single quotes.
fn scan_and_split(input: &str) -> ParsedShellArgs {
  let bytes = input.as_bytes();
  let mut in_double = false;
  let mut in_single = false;
  let mut i = 0;

  while i < bytes.len() {
    let ch = bytes[i];

    // Backslash escapes next char (POSIX only, not on Windows)
    if !cfg!(windows) && ch == b'\\' && !in_single {
      i += 2;
      continue;
    }

    if ch == b'"' && !in_single {
      in_double = !in_double;
    } else if ch == b'\'' && !in_double {
      in_single = !in_single;
    } else if !in_double
      && !in_single
      && (ch == b'<' || ch == b'>' || ch == b'|' || ch == b';' || ch == b'&')
    {
      // Walk back for fd prefix on redirects (e.g. 2>, &>)
      let mut split_idx = i;
      if ch == b'<' || ch == b'>' {
        let mut j = i;
        while j > 0 && bytes[j - 1].is_ascii_digit() {
          j -= 1;
        }
        let mut fd_start = j;
        if j > 0 && bytes[j - 1] == b'&' {
          fd_start = j - 1;
        }
        if fd_start < i
          && (fd_start == 0
            || bytes[fd_start - 1] == b' '
            || bytes[fd_start - 1] == b'\t')
        {

Review comment (suggestion):
Some users on Windows might have the shell configured to be something else. For example, the "shell" argument can be a path to the specific shell to use. I wonder if we need to handle that.

Code after:
}

/// Parses a shell command string into its arguments and shell operator suffix.
/// When `is_cmd_exe` is true, backslashes are not treated as escape characters
/// (cmd.exe semantics). When false, POSIX backslash escaping is used.
#[op2]
#[serde]
pub fn op_node_parse_shell_args(
  #[string] input: &str,
  is_cmd_exe: bool,
) -> ParsedShellArgs {
  scan_and_split(input, is_cmd_exe)
}

/// Scan for the first unquoted shell operator (`<`, `>`, `|`, `&`, `;`)
/// and split the input into args and suffix. When `is_cmd_exe` is false,
/// backslash escapes the next character outside single quotes (POSIX).
fn scan_and_split(input: &str, is_cmd_exe: bool) -> ParsedShellArgs {
  let bytes = input.as_bytes();
  let mut in_double = false;
  let mut in_single = false;
  let mut i = 0;

  while i < bytes.len() {
    let ch = bytes[i];

    // POSIX: backslash escapes next char outside single quotes
    if !is_cmd_exe && ch == b'\\' && !in_single {
      i += 2;
      continue;
    }

    if ch == b'"' && !in_single {
      in_double = !in_double;
    } else if ch == b'\'' && !in_double {
      in_single = !in_single;
    } else if !in_double
      && !in_single
      && (ch == b'<' || ch == b'>' || ch == b'|' || ch == b';' || ch == b'&')
    {
      // Walk back for fd prefix on redirects (e.g. 2>, &>)
      let mut split_idx = i;
      if ch == b'<' || ch == b'>' {
        let mut j = i;
        while j > 0 && bytes[j - 1].is_ascii_digit() {
          j -= 1;
        }
        let mut fd_start = j;
        if j > 0 && bytes[j - 1] == b'&' {
          fd_start = j - 1;
        }