-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
361 lines (341 loc) · 11.5 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
/**
* Shell type.
* @typedef {'bash'|'powershell'|'cmd'} ShellType
*/
/**
* Shell options.
*
* The `program` refers to the script or executable
* being run.
*
* ### Expansion
*
* When `expansion` is `true`, uses double
* quote to allow expansion of special characters.
* Be careful to escape all special characters which
* you do not want to expand.
*
* When `expansion` is `false`, uses single quotes
* to preserve literal meaning. If an invalid literal
* character is present, double quotes are used with
* special characters escaped.
*
* @typedef {{
* shell: ShellType,
* program: string,
* expansion?: boolean,
* }} IShellOptions
*/
/** @type ShellType[] */
const kSupportedShells = ["bash", "powershell", "cmd"];
const kInvalidBashSingleQuoteStrings = ["'"];
/**
* Default options;
* @type IShellOptions
**/
var _defaults = {
shell: "bash",
};
/**
* Return the current default shell options.
* @returns {IShellOptions}
*/
function getDefaults() {
return _defaults;
}
/**
* Sets the default shell options.
* @param {IShellOptions} options
*/
function setDefaults(options) {
options = options || {};
if (!options.shell || typeof options.shell !== "string") {
throw new Error("Invalid shell");
}
options.shell = options.shell.toLowerCase();
if (kSupportedShells.indexOf(options.shell) < 0) {
throw new Error("Invalid shell");
}
_defaults = { ...options };
if (options.posix) {
_defaults.posix = true;
}
}
/**
* Encodes a CLI command specified as arguments.
* The result is ready to use as a CLI command for the
* specified shell.
*
* Specifiying an array instead of a string combines the
* contents of the array into a single string argument.
* This is usefull to form a single string argument or
* to pass nested arguments. Cross-shell encoding is supported.
*
* The following examples assume that bash is the default shell:
* 1. `shellEncode('echo', ['Hello', 'World!'])` gives:
* - `'echo "Hello World!"'`
*
* Add an option object as the last argument or item of array
* to set shell options (see {@link IShellOptions}).
* Note that different options can be nested.
*
* For example:
* 1. `shellEncode('ps', ['Write-Output', ['Hello', 'World!'], { shell: 'powershell' }], { shell: 'cmd' })` gives:
* - `'ps "Write-Output ""Hello World!"""'`
*
* @param {string|string[]|IShellOptions} cmds
* @return {string} Encoded CLI command
*/
function shellEncode(...cmds) {
return _encode(cmds, {}, true);
}
function _encode(cmds, outerOptions, skipOneLevel) {
var maybeOptions = cmds[cmds.length - 1];
var inlineOptions = null;
if (
!(
typeof maybeOptions === "string" ||
(typeof maybeOptions === "object" && maybeOptions instanceof Array)
)
) {
inlineOptions = cmds.pop();
}
var options = {
..._defaults,
...outerOptions,
};
if (skipOneLevel) {
options = {
...options,
...inlineOptions,
};
}
inlineOptions = inlineOptions || {};
var newCmds = cmds;
var enclose = false;
var program = options.program || '';
if (newCmds && newCmds instanceof Array) {
var innerShell = options.shell;
if (inlineOptions && inlineOptions.shell) {
innerShell = inlineOptions.shell;
}
if (innerShell === options.shell) {
inlineOptions = {
...options,
...inlineOptions,
shell: innerShell,
};
if (program) {
inlineOptions.program = program;
}
}
newCmds = newCmds
.map((cmd, i) => {
if (typeof cmd === "object" && cmd instanceof Array) {
cmd = _encode(cmd, inlineOptions);
}
if (i === 0 && !program) {
program = cmd;
inlineOptions.program = program;
}
return cmd;
});
if (options.shell === 'cmd') {
// echo in CMD needs special treatment
if (newCmds.length === 2 && !newCmds[1]) {
// Special case: echo empty line.
// Reference: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/echo
newCmds = 'echo.';
} else {
let joinedCmds = newCmds.slice(0, 2).join(' ');
let cmd = '';
for (let i = 2; i < newCmds.length; i++) {
cmd = newCmds[i];
if (newCmds[i - 2] !== 'echo') {
joinedCmds += ' ';
} // Else: avoid trailing space in output
joinedCmds += cmd;
}
newCmds = joinedCmds;
}
} else {
newCmds = newCmds.join(" ");
}
if (skipOneLevel) {
return newCmds;
}
enclose = true;
}
if (typeof newCmds !== "string") {
throw new Error(`Bad commands`);
}
var encloseString = "";
var encloseStartString = "";
var encloseEndString = "";
var escapeString = "";
var stringsToEscape = [];
var invalidStrings = [];
var replacements = {};
var expansion = options.expansion;
switch (options.shell) {
case "bash":
// Reference: http://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Quoting
var literal = !expansion;
if (!expansion) {
// Check if invalid strings are present.
for (let invalidString of kInvalidBashSingleQuoteStrings) {
if (newCmds.indexOf(invalidString) >= 0) {
// Invalid string found, use double quotes
expansion = true;
break;
}
}
}
if (expansion) {
encloseString = '"';
escapeString = "\\";
stringsToEscape = ["\\", '"'];
if (literal) {
// Escape special characters
stringsToEscape = stringsToEscape.concat([
'$', '`', '!', '\n'
]);
}
} else {
encloseString = "'";
escapeString = "\\";
invalidStrings = kInvalidBashSingleQuoteStrings;
}
break;
case "cmd":
// Reference: https://ss64.com/nt/syntax-esc.html
// if (program === 'echo') {
// // echo prints double quotes,
// // escape delimiters instead of wrapping.
// escapeString = '^';
// stringsToEscape = [
// ' ', ',', ';', '=', '\t', '\r', '\n'
// ];
// if (program !== 'echo') {
// replacements['\\^"'] = '\\^"\\^"\\^"';
// replacements['"'] = '\\^"';
// }
// if (!expansion) {
// stringsToEscape = stringsToEscape.concat([
// '\\', '&', '<', '>', '^', '|', '%', '!'
// ,'(', ')'
// ]);
// replacements['!'] = '^^!'; // Escape delayed expansion
// }
// } else {
// encloseString = '"';
// escapeString = '^';
// stringsToEscape = ['\r\n', '\n'];
// replacements = {
// '"': '"""', // Double up quotes to escape inside quotes
// };
// if (!expansion) {
// Object.assign(replacements, {
// '%': '%%', // Double percent to escape inside quotes
// });
// }
// }
// No double quotes present.
// Avoid enclosing in quotes as this potentially
// adds quotes to the passed argument, which then
// need to be dequoted.
escapeString = '^';
stringsToEscape = [
' ', ',', ';', '=', '\t', '\r', '\n'
];
if (program !== 'echo') {
// echo prints double quotes,
// otherwise, quotes need to be escaped.
replacements['\\^"'] = '\\^"\\^"\\^"';
replacements['"'] = '\\^"';
}
if (!expansion) {
stringsToEscape = stringsToEscape.concat([
'\\', '&', '<', '>', '^', '|', '%', '!'
,'(', ')'
]);
replacements['!'] = '^^!'; // Escape delayed expansion
}
break;
case "powershell":
// References:
// https://adamtheautomator.com/powershell-escape-double-quotes/
// https://www.red-gate.com/simple-talk/sysadmin/powershell/when-to-quote-in-powershell/
if (expansion) {
encloseString = '"';
escapeString = "`";
// TODO: When a region delimited by single quotes
// is found, only escape double quotes
// (and make no replacements?)
stringsToEscape = ["`", '"'];
replacements = {
'\\`"': '\\`"\\`"\\`"',
'`"': '\\`"',
};
} else {
encloseString = "'";
escapeString = "'";
stringsToEscape = ["'"];
}
break;
default:
throw new Error("Unsupported shell: " + options.shell);
}
if (encloseString) {
if (!encloseStartString) {
encloseStartString = encloseString;
}
if (!encloseEndString) {
encloseEndString = encloseString;
}
}
var allReplacements = {};
invalidStrings.forEach((invalidString) => {
if (newCmds.indexOf(invalidString) >= 0) {
throw new Error(
`Invalid shell command: "${newCmds}". Given the options ${JSON.stringify(
options
)}, the following are invalid: ${JSON.stringify(
invalidStrings
)}`
);
}
});
Object.keys(replacements).forEach((stringToReplace) => {
allReplacements[stringToReplace] = replacements[stringToReplace];
});
stringsToEscape.forEach((stringToEscape) => {
allReplacements[stringToEscape] = escapeString + stringToEscape;
});
let allStringsToReplace = Object.keys(allReplacements);
let len = newCmds.length;
var stringToReplaceNow = "";
var escapedCmds = "";
// Note: We avoid global replace because some of
// the replacements overlap
for (var i = 0; i < len; i += 1) {
stringToReplaceNow = allStringsToReplace.find((stringToReplace) => {
return (
stringToReplace === newCmds.slice(i, i + stringToReplace.length)
);
});
if (stringToReplaceNow) {
escapedCmds += allReplacements[stringToReplaceNow];
i += stringToReplaceNow.length - 1;
continue;
}
escapedCmds += newCmds[i];
}
if (enclose) {
escapedCmds = encloseStartString + escapedCmds + encloseEndString;
}
return escapedCmds;
}
shellEncode.getDefaults = getDefaults;
shellEncode.setDefaults = setDefaults;
module.exports = shellEncode;