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

Code before:
// Copyright 2018-2026 the Deno authors. MIT license.

//! Shell argument splitter for child_process command transformation.
//!
//! Splits a shell command string into arguments and a "shell suffix"
//! (everything after the first unquoted shell operator: redirects,
//! pipes, boolean operators, etc.). The suffix is preserved verbatim
//! so it can be passed through to the shell unchanged.

use deno_core::op2;
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct ParsedShellArgs {
  args: Vec<String>,
  shell_suffix: String,
}

/// 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

Review comment (suggestion):
```suggestion
pub fn op_node_parse_shell_args(#[string] input: &str) -> ParsedShellArgs {
  scan_and_split(input)
```

Code after:
// Copyright 2018-2026 the Deno authors. MIT license.

//! Shell argument splitter for child_process command transformation.
//!
//! Splits a shell command string into arguments and a "shell suffix"
//! (everything after the first unquoted shell operator: redirects,
//! pipes, boolean operators, etc.). The suffix is preserved verbatim
//! so it can be passed through to the shell unchanged.

use deno_core::op2;
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct ParsedShellArgs {
  args: Vec<String>,
  shell_suffix: String,
}

/// 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;
    }