Skip to content

Commit 257a25c

Browse files
committed
Fix quote - was severely broken
The quote function was severely broken: 1. operators were quoted; should have been output literally 2. backslashes were used as escapes inside single-quoted strings (they lose the escape meaning inside single quoted strings) 3. Ugliness - when no quoting was needed, it was applied anyway examples: - VAR=value needs no quoting. it was output as VAR\=value - main^{commit} needs no quoting. was output as main\^\{commit\} Fixes #1 and ljharb#11 Author-Rebase-Consent: https://No-rebase.github.io
1 parent ac7be63 commit 257a25c

File tree

2 files changed

+96
-22
lines changed

2 files changed

+96
-22
lines changed

index.js

+21-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
exports.quote = function (xs) {
22
return xs.map(function (s) {
33
if (s && typeof s === 'object') {
4-
return s.op.replace(/(.)/g, '\\$1');
4+
return s.op;
55
}
6-
else if (/["\s]/.test(s) && !/'/.test(s)) {
7-
return "'" + s.replace(/(['\\])/g, '\\$1') + "'";
8-
}
9-
else if (/["'\s]/.test(s)) {
10-
return '"' + s.replace(/(["\\$`(){}!#&*|])/g, '\\$1') + '"';
11-
}
12-
else {
13-
return s.replace(/([\\$`(){}!#&*|])/g, '\\$1');
6+
// Enclose strings with metacharacters in single quoted,
7+
// and escape any single quotes.
8+
// Match strictly to avoid escaping things that don't need to be.
9+
// bash: | & ; ( ) < > space tab
10+
// Also escapes bash curly brace ranges {a..b} {a..z..3} {1..20} {a,b} but not
11+
// {a...b} or {..a}
12+
if ((/(?:["\\$`!\s|&;\(\)<>]|{[\d]+\.{2}[\d]+(?:\.\.\d+)?}|{[a-zA-Z].{2}[a-zA-Z](?:\.\.\d+)?}|{[^{]*,[^}]*})/m).test(s)) {
13+
// If input contains outer single quote, escape each of them individually.
14+
// eg. 'a b c' -> \''a b c'\'
15+
var outer_quotes = s.match(/^('*)(.*?)('*)$/s);
16+
17+
// the starting outer quotes individually escaped
18+
return String(outer_quotes[1]).replace(/(.)/g, '\\$1') +
19+
// the text inside the outer single quotes is single quoted
20+
"'" + outer_quotes[2].replace(/'/g, '\'\\\'\'') + "'" +
21+
// the ending outer quotes individually escaped
22+
String(outer_quotes[3]).replace(/(.)/g, '\\$1');
1423
}
24+
// Only escape the single quotes in strings without metachars or
25+
// separators
26+
return String(s).replace(/(')/g, '\\$1');
1527
}).join(' ');
1628
};
1729

test/quote.js

+75-13
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,82 @@ var test = require('tape');
22
var quote = require('../').quote;
33

44
test('quote', function (t) {
5-
t.equal(quote([ 'a', 'b', 'c d' ]), 'a b \'c d\'');
6-
t.equal(
7-
quote([ 'a', 'b', "it's a \"neat thing\"" ]),
8-
'a b "it\'s a \\"neat thing\\""'
9-
);
10-
t.equal(
11-
quote([ '$', '`', '\'' ]),
12-
'\\$ \\` "\'"'
13-
);
14-
t.equal(quote([]), '');
15-
t.end();
5+
t.equal(quote(['a', 'b', 'c d']), "a b 'c d'");
6+
var quoted = quote(['a', 'b', "it's a \"neat thing\""]);
7+
t.equal(
8+
quoted,
9+
'a b \'it\'\\\'\'s a "neat thing"\''
10+
);
11+
t.isEqual(quoted.length, 28);
12+
t.equal(
13+
quote(['$', '`', '\'']),
14+
'\'$\' \'`\' \\\''
15+
);
16+
t.equal(quote([]), '');
17+
t.equal(quote(["'"]), "\\'");
18+
t.equal(quote(["''"]), "\\'\\'");
19+
t.equal(quote(['a\nb']), "'a\nb'");
20+
t.equal(quote([' #(){}*|][!']), "' #(){}*|][!'");
21+
t.equal(quote(["'#(){}*|][!"]), "\\''#(){}*|][!'");
22+
t.equal(quote(["'#(){}*|][!"]), '\\\'\'#(){}*|][!\'');
23+
t.equal(quote(['X#(){}*|][!']), "'X#(){}*|][!'");
24+
t.equal(quote(['a\n#\nb']), "'a\n#\nb'");
25+
t.equal(quote(['><;{}']), "'><;{}'");
26+
t.equal(quote(['a', 1, true, false]), 'a 1 true false');
27+
t.equal(quote(['a', 1, null, undefined]), 'a 1 null undefined');
28+
t.equal(quote(['a\\x']), "'a\\x'");
29+
30+
// Bash brace expansions {a,b} or {a..b} must be quoted
31+
t.equal(quote(['a{1,2}']), "'a{1,2}'");
32+
t.equal(quote(['a{,2}']), "'a{,2}'");
33+
t.equal(quote(['a{,,2}']), "'a{,,2}'");
34+
t.equal(quote(['\'a{,,2}\'']), "\\''a{,,2}'\\'");
35+
t.equal(quote(['a{1..2}']), "'a{1..2}'");
36+
t.equal(quote(['a{X..Z}']), "'a{X..Z}'");
37+
t.equal(quote(['a{{1..2}}']), "'a{{1..2}}'");
38+
39+
// ... but non brace expansions should not be
40+
t.equal(quote(['a{1...2}']), "a{1...2}");
41+
t.equal(quote(['a{1...2}']), "a{1...2}");
42+
t.equal(quote(['a{1..Z}']), "a{1..Z}");
43+
t.equal(quote(['a{a1..b1}']), "a{a1..b1}");
44+
t.equal(quote(['a{1a..4}']), "a{1a..4}");
45+
t.equal(quote(['a{..6}']), "a{..6}");
46+
t.equal(quote(['a{{1...2}}']), "a{{1...2}}");
47+
t.equal(quote(['a{1.2}']), "a{1.2}");
48+
t.equal(quote(['a{{1.2}}']), "a{{1.2}}");
49+
t.equal(quote(['a{12}']), "a{12}");
50+
quoted = quote(['\\ \\']);
51+
t.equal(quoted, "'\\ \\'");
52+
t.isEqual(quoted.length, 5); // 3-char string + 2 quotes
53+
// TODO: Ugly expansion of single quote at beginning or end of strings.
54+
// Should return \'
55+
t.equal(quote(["'$'"]), "\\''$'\\'");
56+
t.equal(quote(["'"]), "\\'");
57+
t.equal(quote(['gcc', '-DVAR=value']), 'gcc -DVAR=value');
58+
t.equal(quote(['gcc', '-DVAR=value with space']), "gcc '-DVAR=value with space'");
59+
t.end();
1660
});
1761

1862
test('quote ops', function (t) {
19-
t.equal(quote([ 'a', { op: '|' }, 'b' ]), 'a \\| b');
20-
t.end();
63+
t.equal(quote(['a', { op: '|' }, 'b']), 'a | b');
64+
t.equal(
65+
quote(['a', { op: '&&' }, 'b', { op: ';' }, 'c']),
66+
'a && b ; c'
67+
);
68+
t.end();
69+
});
70+
71+
test('quote windows paths', { skip: 'breaking change, disabled until 2.x' }, function (t) {
72+
var path = 'C:\\projects\\node-shell-quote\\index.js';
73+
74+
t.equal(quote([path, 'b', 'c d']), 'C:\\projects\\node-shell-quote\\index.js b \'c d\'');
75+
76+
t.end();
77+
});
78+
79+
test("chars for windows paths don't break out", function (t) {
80+
var x = '`:\\a\\b';
81+
t.equal(quote([x]), "'`:\\a\\b'");
82+
t.end();
2183
});

0 commit comments

Comments
 (0)