Skip to content

Commit 712bafc

Browse files
ehmickysindresorhus
andauthoredJun 2, 2021
Add .escapedCommand property (#466)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 6bc7a1c commit 712bafc

File tree

7 files changed

+86
-8
lines changed

7 files changed

+86
-8
lines changed
 

‎index.d.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,20 @@ declare namespace execa {
252252

253253
interface ExecaReturnBase<StdoutStderrType> {
254254
/**
255-
The file and arguments that were run.
255+
The file and arguments that were run, for logging purposes.
256+
257+
This is not escaped and should not be executed directly as a process, including using `execa()` or `execa.command()`.
256258
*/
257259
command: string;
258260

261+
/**
262+
Same as `command` but escaped.
263+
264+
This is meant to be copy and pasted into a shell, for debugging purposes.
265+
Since the escaping is fairly basic, this should not be executed directly as a process, including using `execa()` or `execa.command()`.
266+
*/
267+
escapedCommand: string;
268+
259269
/**
260270
The numeric exit code of the process that was run.
261271
*/

‎index.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const normalizeStdio = require('./lib/stdio');
1010
const {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} = require('./lib/kill');
1111
const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = require('./lib/stream');
1212
const {mergePromise, getSpawnedPromise} = require('./lib/promise');
13-
const {joinCommand, parseCommand} = require('./lib/command');
13+
const {joinCommand, parseCommand, getEscapedCommand} = require('./lib/command');
1414

1515
const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100;
1616

@@ -74,6 +74,7 @@ const handleOutput = (options, value, error) => {
7474
const execa = (file, args, options) => {
7575
const parsed = handleArguments(file, args, options);
7676
const command = joinCommand(file, args);
77+
const escapedCommand = getEscapedCommand(file, args);
7778

7879
validateTimeout(parsed.options);
7980

@@ -89,6 +90,7 @@ const execa = (file, args, options) => {
8990
stderr: '',
9091
all: '',
9192
command,
93+
escapedCommand,
9294
parsed,
9395
timedOut: false,
9496
isCanceled: false,
@@ -121,6 +123,7 @@ const execa = (file, args, options) => {
121123
stderr,
122124
all,
123125
command,
126+
escapedCommand,
124127
parsed,
125128
timedOut,
126129
isCanceled: context.isCanceled,
@@ -136,6 +139,7 @@ const execa = (file, args, options) => {
136139

137140
return {
138141
command,
142+
escapedCommand,
139143
exitCode: 0,
140144
stdout,
141145
stderr,
@@ -161,6 +165,7 @@ module.exports = execa;
161165
module.exports.sync = (file, args, options) => {
162166
const parsed = handleArguments(file, args, options);
163167
const command = joinCommand(file, args);
168+
const escapedCommand = getEscapedCommand(file, args);
164169

165170
validateInputSync(parsed.options);
166171

@@ -174,6 +179,7 @@ module.exports.sync = (file, args, options) => {
174179
stderr: '',
175180
all: '',
176181
command,
182+
escapedCommand,
177183
parsed,
178184
timedOut: false,
179185
isCanceled: false,
@@ -192,6 +198,7 @@ module.exports.sync = (file, args, options) => {
192198
signal: result.signal,
193199
exitCode: result.status,
194200
command,
201+
escapedCommand,
195202
parsed,
196203
timedOut: result.error && result.error.code === 'ETIMEDOUT',
197204
isCanceled: false,
@@ -207,6 +214,7 @@ module.exports.sync = (file, args, options) => {
207214

208215
return {
209216
command,
217+
escapedCommand,
210218
exitCode: 0,
211219
stdout,
212220
stderr,

‎index.test-d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ try {
1616

1717
const unicornsResult = await execaPromise;
1818
expectType<string>(unicornsResult.command);
19+
expectType<string>(unicornsResult.escapedCommand);
1920
expectType<number>(unicornsResult.exitCode);
2021
expectType<string>(unicornsResult.stdout);
2122
expectType<string>(unicornsResult.stderr);
@@ -47,6 +48,7 @@ try {
4748
try {
4849
const unicornsResult = execa.sync('unicorns');
4950
expectType<string>(unicornsResult.command);
51+
expectType<string>(unicornsResult.escapedCommand);
5052
expectType<number>(unicornsResult.exitCode);
5153
expectType<string>(unicornsResult.stdout);
5254
expectType<string>(unicornsResult.stderr);

‎lib/command.js

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
'use strict';
2-
const SPACES_REGEXP = / +/g;
3-
4-
const joinCommand = (file, args = []) => {
2+
const normalizeArgs = (file, args = []) => {
53
if (!Array.isArray(args)) {
6-
return file;
4+
return [file];
5+
}
6+
7+
return [file, ...args];
8+
};
9+
10+
const NO_ESCAPE_REGEXP = /^[\w.-]+$/;
11+
const DOUBLE_QUOTES_REGEXP = /"/g;
12+
13+
const escapeArg = arg => {
14+
if (NO_ESCAPE_REGEXP.test(arg)) {
15+
return arg;
716
}
817

9-
return [file, ...args].join(' ');
18+
return `"${arg.replace(DOUBLE_QUOTES_REGEXP, '\\"')}"`;
1019
};
1120

21+
const joinCommand = (file, args) => {
22+
return normalizeArgs(file, args).join(' ');
23+
};
24+
25+
const getEscapedCommand = (file, args) => {
26+
return normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' ');
27+
};
28+
29+
const SPACES_REGEXP = / +/g;
30+
1231
// Handle `execa.command()`
1332
const parseCommand = command => {
1433
const tokens = [];
@@ -28,5 +47,6 @@ const parseCommand = command => {
2847

2948
module.exports = {
3049
joinCommand,
50+
getEscapedCommand,
3151
parseCommand
3252
};

‎lib/error.js

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const makeError = ({
3333
signal,
3434
exitCode,
3535
command,
36+
escapedCommand,
3637
timedOut,
3738
isCanceled,
3839
killed,
@@ -61,6 +62,7 @@ const makeError = ({
6162

6263
error.shortMessage = shortMessage;
6364
error.command = command;
65+
error.escapedCommand = escapedCommand;
6466
error.exitCode = exitCode;
6567
error.signal = signal;
6668
error.signalDescription = signalDescription;

‎readme.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const execa = require('execa');
6868
originalMessage: 'spawn unknown ENOENT',
6969
shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT',
7070
command: 'unknown command',
71+
escapedCommand: 'unknown command',
7172
stdout: '',
7273
stderr: '',
7374
all: '',
@@ -121,6 +122,7 @@ try {
121122
originalMessage: 'spawnSync unknown ENOENT',
122123
shortMessage: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT',
123124
command: 'unknown command',
125+
escapedCommand: 'unknown command',
124126
stdout: '',
125127
stderr: '',
126128
all: '',
@@ -234,7 +236,18 @@ The child process [fails](#failed) when:
234236

235237
Type: `string`
236238

237-
The file and arguments that were run.
239+
The file and arguments that were run, for logging purposes.
240+
241+
This is not escaped and should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execa.command()`](#execacommandcommand-options).
242+
243+
#### escapedCommand
244+
245+
Type: `string`
246+
247+
Same as [`command`](#command) but escaped.
248+
249+
This is meant to be copy and pasted into a shell, for debugging purposes.
250+
Since the escaping is fairly basic, this should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execa.command()`](#execacommandcommand-options).
238251

239252
#### exitCode
240253

‎test/command.js

+23
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,29 @@ test(command, ' foo bar', 'foo', 'bar');
1818
test(command, ' baz quz', 'baz', 'quz');
1919
test(command, '');
2020

21+
const testEscapedCommand = async (t, expected, args) => {
22+
const {escapedCommand: failEscapedCommand} = await t.throwsAsync(execa('fail', args));
23+
t.is(failEscapedCommand, `fail ${expected}`);
24+
25+
const {escapedCommand: failEscapedCommandSync} = t.throws(() => {
26+
execa.sync('fail', args);
27+
});
28+
t.is(failEscapedCommandSync, `fail ${expected}`);
29+
30+
const {escapedCommand} = await execa('noop', args);
31+
t.is(escapedCommand, `noop ${expected}`);
32+
33+
const {escapedCommand: escapedCommandSync} = execa.sync('noop', args);
34+
t.is(escapedCommandSync, `noop ${expected}`);
35+
};
36+
37+
testEscapedCommand.title = (message, expected) => `escapedCommand is: ${JSON.stringify(expected)}`;
38+
39+
test(testEscapedCommand, 'foo bar', ['foo', 'bar']);
40+
test(testEscapedCommand, '"foo bar"', ['foo bar']);
41+
test(testEscapedCommand, '"\\"foo\\""', ['"foo"']);
42+
test(testEscapedCommand, '"*"', ['*']);
43+
2144
test('allow commands with spaces and no array arguments', async t => {
2245
const {stdout} = await execa('command with space');
2346
t.is(stdout, '');

0 commit comments

Comments
 (0)
Please sign in to comment.