@antora/run-command-helper
v1.0.0-rc.5
Published
Provides a helper function to run an external command uniformly across platforms with numerous options to control how stdio is handled.
Downloads
725
Readme
@antora/run-command-helper
A Node.js module that provides a helper function to run an external command consistently across platforms. This function is a Promise-based wrapper around the spawn function in Node.js. It’s goal is to eliminate the boilerplate code needed to use the spawn function correctly. Developed for use in Antora.
Install
$ npm i @antora/run-command-helper
API
function runCommand (cmd[, args][, opts])
Runs the specified command. If the command exits with a zero status, the function will return an object containing information from the subprocess (e.g., exit status, stdout data, etc). If the command exits with a non-zero status, or the command fails to start, an Error is thrown.
cmd
<string> - The external command to run. This parameter is required.args
<string[]> - List of string arguments to pass to command. This parameter is optional. If this parameter is not provided, the arguments list is parsed from the value of thecmd
argument (unless theparse
option isfalse
). If any arguments are parsed from the value of thecmd
argument, this list of arguments gets appended to those. Default:undefined
.opts
<Object> - Named parameters to control how the command is run. All named parameters are optional. Default:{}
.cwd
<string> - The directory in which the subprocess will be run. This value also controls the search path for the command if local (or any bare command on Windows, regardless). Default:process.cwd()
.shell
<boolean> | <string> - Specifies whether to run the external command in a shell (uses '/bin/sh' on Unix and process.env.ComSpec on Windows). If theshell
option isfalse
or not set, the command only has to be quoted if it’s being parsed. Otherwise, the command and its arguments must be properly escaped for use in a shell, regardless of how they are specified. In this case, the command and its arguments will also be subject to shell expansion as well. On Unix, you can use a string value to specify the shell explicitly (e.g., ’/bin/bash') When using a shell, environment variable references in the form
$NAMEwill be resolved in the command and its arguments, even on Windows. **Default:**
false`.parse
<boolean> - Specifies whether the value of thecmd
argument should be parsed as a command string (meaning the list of arguments are extracted from the command). If the value of thecmd
argument is parsed, then the base call must be properly quoted (e.g., it must be quoted if it contains spaces). Default:false
if theargs
parameter is specified,true
otherwise.local
<boolean> - Specifies whether to look for the command locally instead of on the PATH. Setting this option totrue
is equivalent to prepending./
to the command. This option is only relevant for a bare command (i.e., a command with no directory separators). If the command (i.e., the base call) contains directory separators, the valuetrue
is implied. Default:false
.stdin
<string> | <Buffer> | <Stream.Readable> - Input to pass to the stdin of the process, if specified and not null, false, or undefined. If an unknown value type is specified, it will be coerced to a string. Default:undefined
.stdout
<string> - Specifies how to handle the stdout of the subprocess. A value of ’print'will pass through the data to the stdout stream of the current process. A value of ’ignore'
will discard the stdout data. A value of ’buffer'will cause the function to return the stdout data as a
Stringif the
encodingoption is set and not ’buffer'
, otherwise aBuffer
. All other values (such asfalse
) are the equivalent of ’ignore'. **Default:** ’buffer'
stderr
<string> - Specifies how to handle the stderr stream of the subprocess. A value of ’print'will pass through the data to the stderr of the current process. A value of ’ignore'
will discard the stderr data. A value of ’buffer'will append the stderr data to the end of the message of the Error if the subprocess fails, otherwise it’s ignored. A value of ’stdout
will redirect the stderr data to the stdout destination. All other values (such asfalse
) are the equivalent of ’ignore'. **Default:** ’buffer'
encoding
<string> - The encoding used for all stdio outputs in the return value. If set and not ’buffer', each stdio output buffer will be encoded to a string using the specified encoding. If the value of the
stdoutoption is ’buffer'
, the return value of the function will change from aBuffer
to aString
. Note that this option does not impact the data type passed to the data event handler on the process. Default: ’buffer'`
- Returns: <Object> | <Buffer> - Process information.
If the
stdout
option is set tobuffer
, this object will be built on a copy of the stdout object for convenience.stdout
<Buffer> | <null> - The stdout data as aBuffer
(orString
if theencoding
option is notbuffer
) if thestdout
option is set tobuffer
, otherwise null.stderr
<Buffer> | <null> - The stderr data as aBuffer
(orString
if theencoding
option is notbuffer
) if thestderr
option is set tobuffer
, otherwise null.status
<number> | <null> - The exit status of the subprocess or null if the subprocess was terminated by a signal.signal
<string> | <null> - The exit signal of the subprocess if the subprocess was terminated by a signal, otherwise null.
All other options, including env
, are passed through to the underlying spawn function call.
Windows considerations
On Windows, the command may use forward slashes, but the resolved command will always have backslashes.
Also on Windows, the command can be resolved in the specified cwd even if the local
option is false
or not set (matching the native behavior on Windows).
On Windows, when the shell
option is not set, the function will attempt to resolve the command without a file extension using the .com
, .exe
, .bat
, and .cmd
file extensions, in that order.
As a result, the base call shown in messages will have a file extension appended even though it was not originally specified.
This modification is necessary in order to ensure the spawn function invokes the correct command.
(The spawn function in Node.js does not resolve the base call using the PATHEXT environment variable when the shell option is not true).
If a match is not found, the function will throw a command not found error.
On Windows, the shell will be used under the covers (even when not specified) to run batch scripts due to a security restriction in Node.js.
The function hides this detail by automatically escaping the arguments and thus does not impose additional escaping requirements when the shell option is false
or not set.
On Windows, builtins like dir
and set
can only be used when shell: true
(this is a limitation of Node.js’ spawn function).
Usage
Here’s an example of running a command that routes the stdout to the stdout stream of the current process.
const runCommand = require('@antora/run-command-helper')
;(async () => {
await runCommand('echo hi', { stdout: 'print', stderr: 'print' })
})()
//=> hi
Here’s an example of running a command that returns the stdout data as a Buffer.
const runCommand = require('@antora/run-command-helper')
;(async () => {
const whoiam = await runCommand('whoami')
process.stdout.write(whoiam)
// or console.log(whoiam.stdout.toString().trimEnd())
})()
//=> myusername
Here’s an example of running a command that returns the stdout data as a String.
const runCommand = require('@antora/run-command-helper')
;(async () => {
const whoiam = await runCommand('whoami', { encoding: 'utf8' })
console.log(whoiam.trimEnd())
// or process.stdout.write(Buffer.from(whoami))
})()
//=> myusername
Here’s an example of how to pass in the list of arguments instead of relying on them being parsed from the command string:
const runCommand = require('@antora/run-command-helper')
;(async () => {
await runCommand(process.execPath, ['-p', '1'], { stdout: 'print' })
})()
//=> 1
Here’s an example of how to specify a local command instead of looking for the command on the PATH:
const runCommand = require('@antora/run-command-helper')
;(async () => {
await runCommand('gradlew --status', { local: true, stdout: 'print', stderr: 'print' })
})()
//=> No Gradle daemons are running.
Here’s an example of how to pass input to the stdin stream of the command:
const runCommand = require('@antora/run-command-helper')
const { EOL } = require('node:os')
;(async () => {
const cat = process.platform === 'win32' ? 'findstr x*' : 'cat'
await runCommand(cat, { stdin: 'hi there!' + EOL, stdout: 'print' })
})()
//=> hi there!
Stdio Stream Handling
When the value of the stdout or stderr option is ’print'`, this function routes the respective stream directly to the current process (i.e., parent, invoking process). This is accomplished by passing the process.stdout and process.stderr streams to the respective stdio option provided by the spawn function. (This is roughly equivalent to using the inherit option, except it allows the tests to intercept stream writes). In other words, this function does not attempt to capture the stdout and stderr streams and emit them to the current process in this case.
If the function were to capture and emit the data from the two streams, it would alter how the lines are interleaved in the terminal. The reason that happens is because the stdout buffer is much larger than the stderr buffer, which results in the stderr data being emitted earlier relative to the stdout data than as received. (See https://stackoverflow.com/questions/57046930/node-js-spawn-keep-stdout-and-stderr-in-the-original-order). This function avoids that problem by connecting the streams directly to the current process when configured to print them.
This behavior is only relevant when both streams are printed. Otherwise, how the streams are wired does not matter.
Copyright and License
Copyright (C) 2024-present by OpenDevise Inc. and the individual contributors to Antora.
Use of this software is granted under the terms of the Mozilla Public License Version 2.0 (MPL-2.0).