ljharb / shell-quote

MIT License
27 stars 10 forks source link

Unexpected handling of combination of backslashes and spaces #14

Open tkeith opened 6 months ago

tkeith commented 6 months ago

I might be misunderstanding the intended usage of shell-quote, but it seems like strings containing both backslashes and spaces are not handled properly:

Let's consider the string foo \ bar. quote(["foo \\ bar"]) returns 'foo \\ bar', and in bash, echo 'foo \\ bar' prints foo \\ bar.

This failure case does not occur with the same string without spaces (foo\bar): quote(["foo\\bar"]) returns foo\\bar, and in bash, echo foo\\bar prints foo\bar.

Here's a quick typescript file to reproduce with a few test cases:

import { quote } from "shell-quote";
import { spawn } from "child_process";

function spawnCommandWithArgs(
  command: string,
  args: string[] = [],
): Promise<string> {
  return new Promise((resolve, reject) => {
    const childProcess = spawn(command, args, {
      stdio: "pipe", // Pipe stdout and stderr to parent
    });

    let stdout = "";
    let stderr = "";

    // Collect data from stdout
    childProcess.stdout.on("data", (data: Buffer) => {
      stdout += data.toString();
    });

    // Collect data from stderr
    childProcess.stderr.on("data", (data: Buffer) => {
      stderr += data.toString();
    });

    // Handle command completion
    childProcess.on("close", (code) => {
      if (code === 0) {
        resolve(stdout);
      } else {
        reject(new Error(stderr || `Command failed with exit code ${code}`));
      }
    });

    // Handle errors starting the process
    childProcess.on("error", (err) => {
      reject(err);
    });
  });
}

async function main() {
  const testStrings = [
    "foo \\ bar",
    "foo\\bar",
    "foo \\\\ bar",
    "foo\\\\bar",
    "foo\nbar",
    "foo\\\nbar",
  ];

  console.log();

  for (const testString of testStrings) {
    const quotedWithShellQuote = quote([testString]);
    const echoWithShellQuote = await spawnCommandWithArgs("bash", [
      "-c",
      `echo -n ${quotedWithShellQuote}`,
    ]);

    if (echoWithShellQuote === testString) {
      console.log("PASSED:");
    } else {
      console.log("FAILED:");
    }

    console.log("*** begin test string");
    console.log(testString);
    console.log("*** end test string");

    if (echoWithShellQuote !== testString) {
      console.log("*** begin quoted with shell-quote");
      console.log(quotedWithShellQuote);
      console.log("*** end quoted with shell-quote");
      console.log("*** begin echoed output");
      console.log(echoWithShellQuote);
      console.log("*** end echoed output");
    }

    console.log();
  }
}

void main()
  .then(() => {
    process.exit(0);
  })
  .catch((e) => {
    console.error(e);
    process.exit(1);
  });

The output of running that file is:

FAILED:
*** begin test string
foo \ bar
*** end test string
*** begin quoted with shell-quote
'foo \\ bar'
*** end quoted with shell-quote
*** begin echoed output
foo \\ bar
*** end echoed output

PASSED:
*** begin test string
foo\bar
*** end test string

FAILED:
*** begin test string
foo \\ bar
*** end test string
*** begin quoted with shell-quote
'foo \\\\ bar'
*** end quoted with shell-quote
*** begin echoed output
foo \\\\ bar
*** end echoed output

PASSED:
*** begin test string
foo\\bar
*** end test string

PASSED:
*** begin test string
foo
bar
*** end test string

FAILED:
*** begin test string
foo\
bar
*** end test string
*** begin quoted with shell-quote
'foo\\
bar'
*** end quoted with shell-quote
*** begin echoed output
foo\\
bar
*** end echoed output
ljharb commented 6 months ago

try echo "foo \\ bar".

tkeith commented 6 months ago

try echo "foo \\ bar".

Yes, of course this correctly prints foo \ bar, but how would I programmatically derive "foo \\ bar" from shell-quote's result of 'foo \\ bar'?

ljharb commented 6 months ago

This package returns a string; the bounding quotes aren't part of it, so you just always include them.

tkeith commented 6 months ago

Quoting the string foo \ bar via quote(["foo \\ bar"]) returns the literal string 'foo \\ bar', which already includes bounding single quotes. So if this is intended to be used within double quotes, the echo statement would be echo "'foo \\ bar'", which still prints an incorrect result, 'foo \ bar'.

ljharb commented 6 months ago

ah, sorry, early morning here.

i'll take another look at this issue when i'm more awake.

tkeith commented 6 months ago

Thank you -- still quite possible I'm misunderstanding how to use it correctly.

My comparison points are Python's shlex.quote and Linux's printf "%q", which both seem to be quoting/escaping the input strings "correctly" by my definition (returning strings that when passed to echo in bash, print the originally passed-in string).

This has led me to the following temporary solution for JavaScript on Unix-based systems (I'm aware that this is hugely non-performant given the use of spawn):

import { spawnSync } from "child_process";

export default function quote(rawInput: string): string {
  const result = spawnSync("printf", ["%q", rawInput], { encoding: "utf8" });

  if (result.status === 0) {
    return result.stdout;
  } else {
    throw new Error(
      `printf failed with code ${result.status} and error: ${result.stderr}`,
    );
  }
}