ljharb / ljharb/shell-quote

`parse()` does not support bash ANSI-C quoting (`$'...'`)

Open
#32 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
62
Forks
23
PR merge metrics
No merged PRs in 30d

Description

## Summary

`shell-quote` (reproduced on **v1.10.0**) does not implement bash's [ANSI-C quoting](https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html) — the `$'...'` form. This is not an exotic construct: it is exactly what Chrome/Edge DevTools emit from *Copy as cURL (bash)* (e.g. `--data-raw $'...'`), so any consumer that uses `shell-quote` to parse real-world copied shell commands silently gets the wrong arguments.

## Current behaviour

The scanner treats the leading `$` as the start of a parameter expansion (empty name → emits a literal `$`), and then treats the rest of the token as a plain single-quoted string. Consequences:

1. the leading `$` of the `$'` prefix is kept in the output (it should be dropped);
2. ANSI-C escape sequences are **not** decoded (`\n`, `\t`, `\x41`, `\'`, …);
3. `\'` does not escape the quote — it terminates the string and the remainder is mis-parsed.

## Reproduction

```js
const { parse } = require('shell-quote'); // v1.10.0

const cases = [
'$' + "'" + '{"a":"${field}"}' + "'",
'$' + "'" + 'line1\\nline2' + "'",
'$' + "'" + 'a\\\'b' + "'",
'$' + "'" + 'tab\\there' + "'",
'$' + "'" + '\\x41' + "'",
'$' + "'" + 'cost is $5' + "'"
];

cases.forEach((c) => console.log(JSON.stringify(c), '->', JSON.stringify(parse(c)[0])));
```

| input | bash output (`printf '%s' `) | `shell-quote@1.10.0` |
| --- | --- | --- |
| `$'{"a":"${field}"}'` | `{"a":"${field}"}` | `${"a":"${field}"}` ❌ leading `$` kept |
| `$'line1\nline2'` | `line1` ⏎ `line2` | `$line1\nline2` ❌ leading `$` + escape not decoded |
| `$'a\'b'` | `a'b` | `$a\b` ❌ (quote terminated by `\'`, `\` kept) |
| `$'tab\there'` | `tab`⇥`here` | `$tab\there` ❌ |
| `$'\x41'` | `A` | `$\x41` ❌ |
| `$'cost is $5'` | `cost is $5` | `$cost is $5` ❌ leading `$` kept |

## Expected behaviour

Per the bash manual, inside `$'...'`:

- the documented escape sequences are decoded (`\n`, `\t`, `\'`, `\"`, `\\`, `\nnn`, `\xHH`, `\uHHHH`, `\UHHHHHHHH`, `\cx`, `\a`, `\b`, `\e`, `\E`, `\f`, `\r`, `\v`, `\?`, …);
- **no** parameter expansion, command substitution or brace expansion happens — `${field}` and `$5` stay literal;
- the `$'` prefix and the surrounding quotes are removed, and the token is not word-split.

## Real-world impact / related

- postmanlabs/curl-to-postman#104 — Postman's cURL importer corrupted literal `${...}` in Chrome-copied cURL commands. It uses a **vendored copy** of `shell-quote` (`assets/shell-quote.js`), patched to decode escapes; because that patch kept the double-quote branch's variable resolution, `${field}` was rendered as `$field`. Fix: postmanlabs/curl-to-postman#105.
- If `parse()` supported `$'...'` natively, downstream consumers (Postman included) would not need to vendor + patch this file.
- Nice-to-have on the other side: `quote()` could emit `$'...'` for strings containing a single quote (instead of `'\''`), which is what bash/DevTools do and what round-trips through the above fix.

Thanks for maintaining this library!

Contributor guide

Open the contributing guide

Research direction

Start with the parse() entry point and reproduce the listed ANSI-C quoting cases. Compare the results with the documented Bash behavior, including escape decoding and literal parameter text; done means the prefix and surrounding quotes are removed, escapes decode correctly, and the token remains unsplit.

Written by the indexing model from the issue text.

Assessment

Tech stack
bash, javascript
Domain
cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.