Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected handling of combination of backslashes and spaces #14

Open
tkeith opened this issue Apr 11, 2024 · 6 comments
Open

Unexpected handling of combination of backslashes and spaces #14

tkeith opened this issue Apr 11, 2024 · 6 comments

Comments

@tkeith
Copy link

tkeith commented Apr 11, 2024

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
Copy link
Owner

ljharb commented Apr 11, 2024

try echo "foo \\ bar".

@tkeith
Copy link
Author

tkeith commented Apr 11, 2024

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
Copy link
Owner

ljharb commented Apr 11, 2024

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

@tkeith
Copy link
Author

tkeith commented Apr 11, 2024

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
Copy link
Owner

ljharb commented Apr 11, 2024

ah, sorry, early morning here.

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

@tkeith
Copy link
Author

tkeith commented Apr 11, 2024

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}`,
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants