Skip to content

Commit eda065c

Browse files
committed
Print output with verbose option
1 parent 231beaf commit eda065c

34 files changed

+554
-87
lines changed

docs/scripts.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@ $.verbose = true;
404404
// Execa
405405
import {$ as $_} from 'execa';
406406

407-
const $ = $_({verbose: true});
407+
// `verbose: 'short'` is also available
408+
const $ = $_({verbose: 'full'});
408409

409410
await $`echo example`;
410411
```
@@ -418,7 +419,8 @@ NODE_DEBUG=execa node file.js
418419
Which prints:
419420

420421
```
421-
[19:49:00.360] $ echo example
422+
[19:49:00.360] [0] $ echo example
423+
example
422424
```
423425

424426
### Piping stdout to another command

index.d.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -543,13 +543,18 @@ type CommonOptions<IsSync extends boolean = boolean> = {
543543
readonly windowsHide?: boolean;
544544

545545
/**
546-
Print each command on `stderr` before executing it.
546+
If `verbose` is `'short'` or `'full'`, prints each command on `stderr` before executing it.
547547
548-
This can also be enabled by setting the `NODE_DEBUG=execa` environment variable in the current process.
548+
If `verbose` is `'full'`, the command's `stdout` and `stderr` are printed too, unless either:
549+
- the `stdout`/`stderr` option is `ignore` or `inherit`.
550+
- the `stdout`/`stderr` is redirected to [a stream](https://nodejs.org/api/stream.html#readablepipedestination-options), a file, a file descriptor, or another child process.
551+
- the `encoding` option is set.
549552
550-
@default false
553+
This can also be set to `'full'` by setting the `NODE_DEBUG=execa` environment variable in the current process.
554+
555+
@default 'none'
551556
*/
552-
readonly verbose?: boolean;
557+
readonly verbose?: 'none' | 'short' | 'full';
553558

554559
/**
555560
Kill the spawned process when the parent process exits unless either:
@@ -1028,7 +1033,7 @@ export function execa<OptionsType extends Options = {}>(
10281033
/**
10291034
Same as `execa()` but synchronous.
10301035
1031-
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal` and `lines`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
1036+
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal`, `lines` and `verbose: 'full'`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable, a transform or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
10321037
10331038
Returns or throws a `childProcessResult`. The `childProcess` is not returned: its methods and properties are not available. This includes [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal), [`.pid`](https://nodejs.org/api/child_process.html#subprocesspid), `.pipe()` and the [`.stdin`/`.stdout`/`.stderr`](https://nodejs.org/api/child_process.html#subprocessstdout) streams.
10341039
@@ -1129,7 +1134,7 @@ export function execaCommand<OptionsType extends Options = {}>(
11291134
/**
11301135
Same as `execaCommand()` but synchronous.
11311136
1132-
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal` and `lines`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
1137+
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal`, `lines` and `verbose: 'full'`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable, a transform or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
11331138
11341139
Returns or throws a `childProcessResult`. The `childProcess` is not returned: its methods and properties are not available. This includes [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal), [`.pid`](https://nodejs.org/api/child_process.html#subprocesspid), `.pipe()` and the [`.stdin`/`.stdout`/`.stderr`](https://nodejs.org/api/child_process.html#subprocessstdout) streams.
11351140
@@ -1187,7 +1192,7 @@ type Execa$<OptionsType extends CommonOptions = {}> = {
11871192
/**
11881193
Same as $\`command\` but synchronous.
11891194
1190-
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal` and `lines`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
1195+
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal`, `lines` and `verbose: 'full'`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable, a transform or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
11911196
11921197
Returns or throws a `childProcessResult`. The `childProcess` is not returned: its methods and properties are not available. This includes [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal), [`.pid`](https://nodejs.org/api/child_process.html#subprocesspid), `.pipe()` and the [`.stdin`/`.stdout`/`.stderr`](https://nodejs.org/api/child_process.html#subprocessstdout) streams.
11931198
@@ -1241,7 +1246,7 @@ type Execa$<OptionsType extends CommonOptions = {}> = {
12411246
/**
12421247
Same as $\`command\` but synchronous.
12431248
1244-
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal` and `lines`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
1249+
Cannot use the following options: `all`, `cleanup`, `buffer`, `detached`, `ipc`, `serialization`, `cancelSignal`, `lines` and `verbose: 'full'`. Also, the `stdin`, `stdout`, `stderr`, `stdio` and `input` options cannot be an array, an iterable, a transform or a web stream. Node.js streams must have a file descriptor unless the `input` option is used.
12451250
12461251
Returns or throws a `childProcessResult`. The `childProcess` is not returned: its methods and properties are not available. This includes [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal), [`.pid`](https://nodejs.org/api/child_process.html#subprocesspid), `.pipe()` and the [`.stdin`/`.stdout`/`.stderr`](https://nodejs.org/api/child_process.html#subprocessstdout) streams.
12471252

index.test-d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,8 +1440,12 @@ execa('unicorns', {windowsVerbatimArguments: true});
14401440
execaSync('unicorns', {windowsVerbatimArguments: true});
14411441
execa('unicorns', {windowsHide: false});
14421442
execaSync('unicorns', {windowsHide: false});
1443-
execa('unicorns', {verbose: false});
1444-
execaSync('unicorns', {verbose: false});
1443+
execa('unicorns', {verbose: 'none'});
1444+
execaSync('unicorns', {verbose: 'none'});
1445+
execa('unicorns', {verbose: 'short'});
1446+
execaSync('unicorns', {verbose: 'short'});
1447+
execa('unicorns', {verbose: 'full'});
1448+
execaSync('unicorns', {verbose: 'full'});
14451449
expectError(execa('unicorns', {verbose: 'other'}));
14461450
expectError(execaSync('unicorns', {verbose: 'other'}));
14471451
/* eslint-enable @typescript-eslint/no-floating-promises */

lib/arguments/options.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import isPlainObject from 'is-plain-obj';
66
import {normalizeForceKillAfterDelay} from '../exit/kill.js';
77
import {validateTimeout} from '../exit/timeout.js';
88
import {logCommand} from '../verbose/start.js';
9+
import {getVerboseInfo} from '../verbose/info.js';
910
import {handleNodeOption} from './node.js';
1011
import {joinCommand} from './escape.js';
1112
import {normalizeCwd, safeNormalizeFileUrl, normalizeFileUrl} from './cwd.js';
@@ -39,8 +40,9 @@ export const normalizeArguments = (rawFile, rawArgs = [], rawOptions = {}) => {
3940

4041
export const handleCommand = (filePath, rawArgs, rawOptions) => {
4142
const {command, escapedCommand} = joinCommand(filePath, rawArgs);
42-
logCommand(escapedCommand, rawOptions);
43-
return {command, escapedCommand};
43+
const verboseInfo = getVerboseInfo(rawOptions);
44+
logCommand(escapedCommand, verboseInfo, rawOptions);
45+
return {command, escapedCommand, verboseInfo};
4446
};
4547

4648
export const handleArguments = (filePath, rawArgs, rawOptions) => {

lib/async.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {getSpawnedResult} from './stream/resolve.js';
1414
import {mergePromise} from './promise.js';
1515

1616
export const execa = (rawFile, rawArgs, rawOptions) => {
17-
const {file, args, command, escapedCommand, options, stdioStreamsGroups} = handleAsyncArguments(rawFile, rawArgs, rawOptions);
18-
const {spawned, promise} = spawnProcessAsync({file, args, options, command, escapedCommand, stdioStreamsGroups});
17+
const {file, args, command, escapedCommand, options, stdioStreamsGroups, stdioState} = handleAsyncArguments(rawFile, rawArgs, rawOptions);
18+
const {spawned, promise} = spawnProcessAsync({file, args, options, command, escapedCommand, stdioStreamsGroups, stdioState});
1919
spawned.pipe = pipeToProcess.bind(undefined, {source: spawned, sourcePromise: promise, stdioStreamsGroups, destinationOptions: {}});
2020
mergePromise(spawned, promise);
2121
PROCESS_OPTIONS.set(spawned, options);
@@ -24,11 +24,11 @@ export const execa = (rawFile, rawArgs, rawOptions) => {
2424

2525
const handleAsyncArguments = (rawFile, rawArgs, rawOptions) => {
2626
[rawFile, rawArgs, rawOptions] = normalizeArguments(rawFile, rawArgs, rawOptions);
27-
const {command, escapedCommand} = handleCommand(rawFile, rawArgs, rawOptions);
27+
const {command, escapedCommand, verboseInfo} = handleCommand(rawFile, rawArgs, rawOptions);
2828
const {file, args, options: normalizedOptions} = handleArguments(rawFile, rawArgs, rawOptions);
2929
const options = handleAsyncOptions(normalizedOptions);
30-
const stdioStreamsGroups = handleInputAsync(options);
31-
return {file, args, command, escapedCommand, options, stdioStreamsGroups};
30+
const {stdioStreamsGroups, stdioState} = handleInputAsync(options, verboseInfo);
31+
return {file, args, command, escapedCommand, options, stdioStreamsGroups, stdioState};
3232
};
3333

3434
// Prevent passing the `timeout` option directly to `child_process.spawn()`
@@ -40,7 +40,7 @@ const handleAsyncOptions = ({timeout, signal, cancelSignal, ...options}) => {
4040
return {...options, timeoutDuration: timeout, signal: cancelSignal};
4141
};
4242

43-
const spawnProcessAsync = ({file, args, options, command, escapedCommand, stdioStreamsGroups}) => {
43+
const spawnProcessAsync = ({file, args, options, command, escapedCommand, stdioStreamsGroups, stdioState}) => {
4444
let spawned;
4545
try {
4646
spawned = spawn(file, args, options);
@@ -52,7 +52,7 @@ const spawnProcessAsync = ({file, args, options, command, escapedCommand, stdioS
5252
setMaxListeners(Number.POSITIVE_INFINITY, controller.signal);
5353

5454
const originalStreams = [...spawned.stdio];
55-
pipeOutputAsync(spawned, stdioStreamsGroups, controller);
55+
pipeOutputAsync(spawned, stdioStreamsGroups, stdioState, controller);
5656
cleanupOnExit(spawned, options, controller);
5757

5858
spawned.kill = spawnedKill.bind(undefined, {kill: spawned.kill.bind(spawned), spawned, options, controller});

lib/stdio/async.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {TYPE_TO_MESSAGE} from './type.js';
88
import {generatorToDuplexStream, pipeGenerator} from './generator.js';
99

1010
// Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async mode
11-
export const handleInputAsync = options => handleInput(addPropertiesAsync, options, false);
11+
export const handleInputAsync = (options, verboseInfo) => handleInput(addPropertiesAsync, options, verboseInfo, false);
1212

1313
const forbiddenIfAsync = ({type, optionName}) => {
1414
throw new TypeError(`The \`${optionName}\` option cannot be ${TYPE_TO_MESSAGE[type]}.`);
@@ -36,7 +36,8 @@ const addPropertiesAsync = {
3636

3737
// Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, after spawning, in async mode
3838
// When multiple input streams are used, we merge them to ensure the output stream ends only once each input stream has ended
39-
export const pipeOutputAsync = (spawned, stdioStreamsGroups, controller) => {
39+
export const pipeOutputAsync = (spawned, stdioStreamsGroups, stdioState, controller) => {
40+
stdioState.spawned = spawned;
4041
const inputStreamsGroups = {};
4142

4243
for (const stdioStreams of stdioStreamsGroups) {

lib/stdio/forward.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const forwardStdio = stdioStreamsGroups => stdioStreamsGroups.map(stdioSt
66
// Whether `childProcess.std*` will be set
77
export const willPipeStreams = stdioStreams => PIPED_STDIO_VALUES.has(forwardStdioItem(stdioStreams));
88

9-
const PIPED_STDIO_VALUES = new Set(['pipe', 'overlapped', undefined, null]);
9+
export const PIPED_STDIO_VALUES = new Set(['pipe', 'overlapped', undefined, null]);
1010

1111
const forwardStdioItem = stdioStreams => {
1212
if (stdioStreams.length > 1) {

lib/stdio/handle.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {handleStreamsVerbose} from '../verbose/output.js';
12
import {getStdioOptionType, isRegularUrl, isUnknownStdioString} from './type.js';
23
import {addStreamDirection} from './direction.js';
34
import {normalizeStdio} from './option.js';
@@ -9,18 +10,20 @@ import {normalizeGenerators} from './generator.js';
910
import {forwardStdio} from './forward.js';
1011

1112
// Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async/sync mode
12-
export const handleInput = (addProperties, options, isSync) => {
13+
export const handleInput = (addProperties, options, verboseInfo, isSync) => {
14+
const stdioState = {};
1315
const stdio = normalizeStdio(options);
1416
const [stdinStreams, ...otherStreamsGroups] = stdio.map((stdioOption, fdNumber) => getStdioStreams(stdioOption, fdNumber));
1517
const stdioStreamsGroups = [[...stdinStreams, ...handleInputOptions(options)], ...otherStreamsGroups]
1618
.map(stdioStreams => validateStreams(stdioStreams))
1719
.map(stdioStreams => addStreamDirection(stdioStreams))
20+
.map(stdioStreams => handleStreamsVerbose({stdioStreams, options, isSync, stdioState, verboseInfo}))
1821
.map(stdioStreams => handleStreamsLines(stdioStreams, options, isSync))
1922
.map(stdioStreams => handleStreamsEncoding(stdioStreams, options, isSync))
2023
.map(stdioStreams => normalizeGenerators(stdioStreams))
2124
.map(stdioStreams => addStreamsProperties(stdioStreams, addProperties));
2225
options.stdio = forwardStdio(stdioStreamsGroups);
23-
return stdioStreamsGroups;
26+
return {stdioStreamsGroups, stdioState};
2427
};
2528

2629
// We make sure passing an array with a single item behaves the same as passing that item without an array.

lib/stdio/sync.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {handleInput} from './handle.js';
44
import {TYPE_TO_MESSAGE} from './type.js';
55

66
// Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in sync mode
7-
export const handleInputSync = options => {
8-
const stdioStreamsGroups = handleInput(addPropertiesSync, options, true);
7+
export const handleInputSync = (options, verboseInfo) => {
8+
const {stdioStreamsGroups} = handleInput(addPropertiesSync, options, verboseInfo, true);
99
addInputOptionSync(stdioStreamsGroups, options);
1010
return stdioStreamsGroups;
1111
};

lib/sync.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ export const execaSync = (rawFile, rawArgs, rawOptions) => {
1313

1414
const handleSyncArguments = (rawFile, rawArgs, rawOptions) => {
1515
[rawFile, rawArgs, rawOptions] = normalizeArguments(rawFile, rawArgs, rawOptions);
16-
const {command, escapedCommand} = handleCommand(rawFile, rawArgs, rawOptions);
16+
const {command, escapedCommand, verboseInfo} = handleCommand(rawFile, rawArgs, rawOptions);
1717
const syncOptions = normalizeSyncOptions(rawOptions);
1818
const {file, args, options} = handleArguments(rawFile, rawArgs, syncOptions);
1919
validateSyncOptions(options);
20-
const stdioStreamsGroups = handleInputSync(options);
20+
const stdioStreamsGroups = handleInputSync(options, verboseInfo);
2121
return {file, args, command, escapedCommand, options, stdioStreamsGroups};
2222
};
2323

lib/verbose/info.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {debuglog} from 'node:util';
2+
3+
export const getVerboseInfo = ({verbose = verboseDefault}) => {
4+
if (verbose === 'none') {
5+
return {verbose};
6+
}
7+
8+
const verboseId = VERBOSE_ID++;
9+
return {verbose, verboseId};
10+
};
11+
12+
const verboseDefault = debuglog('execa').enabled ? 'full' : 'none';
13+
14+
// Prepending the `pid` is useful when multiple commands print their output at the same time.
15+
// However, we cannot use the real PID since this is not available with `child_process.spawnSync()`.
16+
// Also, we cannot use the real PID if we want to print it before `child_process.spawn()` is run.
17+
// As a pro, it is shorter than a normal PID and never re-uses the same id.
18+
// As a con, it cannot be used to send signals.
19+
let VERBOSE_ID = 0n;

lib/verbose/log.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,25 @@ import {writeFileSync} from 'node:fs';
22
import process from 'node:process';
33

44
// Write synchronously to ensure lines are properly ordered and not interleaved with `stdout`
5-
export const verboseLog = (string, icon) => {
6-
writeFileSync(process.stderr.fd, `[${getTimestamp()}] ${ICONS[icon]} ${string}\n`);
5+
export const verboseLog = (string, verboseId, icon) => {
6+
const prefixedLines = addPrefix(string, verboseId, icon);
7+
writeFileSync(process.stderr.fd, `${prefixedLines}\n`);
78
};
89

10+
const addPrefix = (string, verboseId, icon) => string.includes('\n')
11+
? string
12+
.split('\n')
13+
.map(line => addPrefixToLine(line, verboseId, icon))
14+
.join('\n')
15+
: addPrefixToLine(string, verboseId, icon);
16+
17+
const addPrefixToLine = (line, verboseId, icon) => [
18+
`[${getTimestamp()}]`,
19+
`[${verboseId}]`,
20+
ICONS[icon],
21+
line,
22+
].join(' ');
23+
924
// Prepending the timestamp allows debugging the slow paths of a process
1025
const getTimestamp = () => {
1126
const date = new Date();
@@ -17,4 +32,5 @@ const padField = (field, padding) => String(field).padStart(padding, '0');
1732
const ICONS = {
1833
command: '$',
1934
pipedCommand: '|',
35+
output: ' ',
2036
};

0 commit comments

Comments
 (0)