Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/execa
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.0.1
Choose a base ref
...
head repository: sindresorhus/execa
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.1.0
Choose a head ref
  • 2 commits
  • 8 files changed
  • 2 contributors

Commits on Jun 2, 2021

  1. Add .escapedCommand property (#466)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    ehmicky and sindresorhus authored Jun 2, 2021

    Verified

    This commit was signed with the committer’s verified signature.
    ssbarnea Sorin Sbarnea
    Copy the full SHA
    712bafc View commit details
  2. 5.1.0

    sindresorhus committed Jun 2, 2021

    Verified

    This commit was signed with the committer’s verified signature.
    ssbarnea Sorin Sbarnea
    Copy the full SHA
    b3e96b0 View commit details
Showing with 87 additions and 9 deletions.
  1. +11 −1 index.d.ts
  2. +9 −1 index.js
  3. +2 −0 index.test-d.ts
  4. +25 −5 lib/command.js
  5. +2 −0 lib/error.js
  6. +1 −1 package.json
  7. +14 −1 readme.md
  8. +23 −0 test/command.js
12 changes: 11 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -252,10 +252,20 @@ declare namespace execa {

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

/**
Same as `command` but escaped.
This is meant to be copy and pasted into a shell, for debugging purposes.
Since the escaping is fairly basic, this should not be executed directly as a process, including using `execa()` or `execa.command()`.
*/
escapedCommand: string;

/**
The numeric exit code of the process that was run.
*/
10 changes: 9 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ const normalizeStdio = require('./lib/stdio');
const {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} = require('./lib/kill');
const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = require('./lib/stream');
const {mergePromise, getSpawnedPromise} = require('./lib/promise');
const {joinCommand, parseCommand} = require('./lib/command');
const {joinCommand, parseCommand, getEscapedCommand} = require('./lib/command');

const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100;

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

validateTimeout(parsed.options);

@@ -89,6 +90,7 @@ const execa = (file, args, options) => {
stderr: '',
all: '',
command,
escapedCommand,
parsed,
timedOut: false,
isCanceled: false,
@@ -121,6 +123,7 @@ const execa = (file, args, options) => {
stderr,
all,
command,
escapedCommand,
parsed,
timedOut,
isCanceled: context.isCanceled,
@@ -136,6 +139,7 @@ const execa = (file, args, options) => {

return {
command,
escapedCommand,
exitCode: 0,
stdout,
stderr,
@@ -161,6 +165,7 @@ module.exports = execa;
module.exports.sync = (file, args, options) => {
const parsed = handleArguments(file, args, options);
const command = joinCommand(file, args);
const escapedCommand = getEscapedCommand(file, args);

validateInputSync(parsed.options);

@@ -174,6 +179,7 @@ module.exports.sync = (file, args, options) => {
stderr: '',
all: '',
command,
escapedCommand,
parsed,
timedOut: false,
isCanceled: false,
@@ -192,6 +198,7 @@ module.exports.sync = (file, args, options) => {
signal: result.signal,
exitCode: result.status,
command,
escapedCommand,
parsed,
timedOut: result.error && result.error.code === 'ETIMEDOUT',
isCanceled: false,
@@ -207,6 +214,7 @@ module.exports.sync = (file, args, options) => {

return {
command,
escapedCommand,
exitCode: 0,
stdout,
stderr,
2 changes: 2 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ try {

const unicornsResult = await execaPromise;
expectType<string>(unicornsResult.command);
expectType<string>(unicornsResult.escapedCommand);
expectType<number>(unicornsResult.exitCode);
expectType<string>(unicornsResult.stdout);
expectType<string>(unicornsResult.stderr);
@@ -47,6 +48,7 @@ try {
try {
const unicornsResult = execa.sync('unicorns');
expectType<string>(unicornsResult.command);
expectType<string>(unicornsResult.escapedCommand);
expectType<number>(unicornsResult.exitCode);
expectType<string>(unicornsResult.stdout);
expectType<string>(unicornsResult.stderr);
30 changes: 25 additions & 5 deletions lib/command.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
'use strict';
const SPACES_REGEXP = / +/g;

const joinCommand = (file, args = []) => {
const normalizeArgs = (file, args = []) => {
if (!Array.isArray(args)) {
return file;
return [file];
}

return [file, ...args];
};

const NO_ESCAPE_REGEXP = /^[\w.-]+$/;
const DOUBLE_QUOTES_REGEXP = /"/g;

const escapeArg = arg => {
if (NO_ESCAPE_REGEXP.test(arg)) {
return arg;
}

return [file, ...args].join(' ');
return `"${arg.replace(DOUBLE_QUOTES_REGEXP, '\\"')}"`;
};

const joinCommand = (file, args) => {
return normalizeArgs(file, args).join(' ');
};

const getEscapedCommand = (file, args) => {
return normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' ');
};

const SPACES_REGEXP = / +/g;

// Handle `execa.command()`
const parseCommand = command => {
const tokens = [];
@@ -28,5 +47,6 @@ const parseCommand = command => {

module.exports = {
joinCommand,
getEscapedCommand,
parseCommand
};
2 changes: 2 additions & 0 deletions lib/error.js
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@ const makeError = ({
signal,
exitCode,
command,
escapedCommand,
timedOut,
isCanceled,
killed,
@@ -61,6 +62,7 @@ const makeError = ({

error.shortMessage = shortMessage;
error.command = command;
error.escapedCommand = escapedCommand;
error.exitCode = exitCode;
error.signal = signal;
error.signalDescription = signalDescription;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "execa",
"version": "5.0.1",
"version": "5.1.0",
"description": "Process execution for humans",
"license": "MIT",
"repository": "sindresorhus/execa",
15 changes: 14 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
@@ -68,6 +68,7 @@ const execa = require('execa');
originalMessage: 'spawn unknown ENOENT',
shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT',
command: 'unknown command',
escapedCommand: 'unknown command',
stdout: '',
stderr: '',
all: '',
@@ -121,6 +122,7 @@ try {
originalMessage: 'spawnSync unknown ENOENT',
shortMessage: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT',
command: 'unknown command',
escapedCommand: 'unknown command',
stdout: '',
stderr: '',
all: '',
@@ -234,7 +236,18 @@ The child process [fails](#failed) when:

Type: `string`

The file and arguments that were run.
The file and arguments that were run, for logging purposes.

This is not escaped and should not be executed directly as a process, including using [`execa()`](#execafile-arguments-options) or [`execa.command()`](#execacommandcommand-options).

#### escapedCommand

Type: `string`

Same as [`command`](#command) but escaped.

This is meant to be copy and pasted into a shell, for debugging purposes.
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).

#### exitCode

23 changes: 23 additions & 0 deletions test/command.js
Original file line number Diff line number Diff line change
@@ -18,6 +18,29 @@ test(command, ' foo bar', 'foo', 'bar');
test(command, ' baz quz', 'baz', 'quz');
test(command, '');

const testEscapedCommand = async (t, expected, args) => {
const {escapedCommand: failEscapedCommand} = await t.throwsAsync(execa('fail', args));
t.is(failEscapedCommand, `fail ${expected}`);

const {escapedCommand: failEscapedCommandSync} = t.throws(() => {
execa.sync('fail', args);
});
t.is(failEscapedCommandSync, `fail ${expected}`);

const {escapedCommand} = await execa('noop', args);
t.is(escapedCommand, `noop ${expected}`);

const {escapedCommand: escapedCommandSync} = execa.sync('noop', args);
t.is(escapedCommandSync, `noop ${expected}`);
};

testEscapedCommand.title = (message, expected) => `escapedCommand is: ${JSON.stringify(expected)}`;

test(testEscapedCommand, 'foo bar', ['foo', 'bar']);
test(testEscapedCommand, '"foo bar"', ['foo bar']);
test(testEscapedCommand, '"\\"foo\\""', ['"foo"']);
test(testEscapedCommand, '"*"', ['*']);

test('allow commands with spaces and no array arguments', async t => {
const {stdout} = await execa('command with space');
t.is(stdout, '');