This page describes the differences between Bash, Execa, and zx (which inspired this feature). Execa intends to be more:
- Performant
- Cross-platform: no shell is used, only JavaScript.
- Secure: no shell injection.
- Simple: minimalistic API, no globals, no binary, no builtin CLI utilities.
- Featureful: all Execa features are available (text lines iteration, subprocess piping, IPC, transforms, background subprocesses, cancelation, local binaries, cleanup on exit, interleaved output, forceful termination, and more).
- Easy to debug: verbose mode, detailed errors, messages and stack traces, stateless API.
Unlike shell languages like Bash, libraries like Execa and zx enable you to write scripts with a more featureful programming language (JavaScript). This allows complex logic (such as parallel execution) to be expressed easily. This also lets you use any Node.js package.
The main difference between Execa and zx is that Execa does not require any shell. Shell-specific keywords and features are written in JavaScript instead.
This is more cross-platform. For example, your code works the same on Windows machines without Bash installed.
Also, there is no shell syntax to remember: everything is just plain JavaScript.
If you really need a shell though, the shell
option can be used.
Execa's scripting API mostly consists of only two methods: $`command`
and $(options)
.
No special binary is recommended, no global variable is injected: scripts are regular Node.js files.
Execa is a thin wrapper around the core Node.js child_process
module. Unlike zx, it lets you use any of its native features: pid
, IPC, unref()
, detached
, uid
, gid
, cancelSignal
, etc.
zx includes many builtin utilities: fetch()
, question()
, sleep()
, stdin()
, retry()
, spinner()
, chalk
, fs-extra
, os
, path
, globby
, yaml
, minimist
, which
, Markdown scripts, remote scripts.
Execa does not include any utility: it focuses on being small and modular instead. Any Node.js package can be used in your scripts.
Spawning a shell for every command comes at a performance cost, which Execa avoids.
Also, local binaries can be directly executed without using npx
.
Subprocesses can be hard to debug, which is why Execa includes a verbose
option.
Also, Execa's error messages and properties are very detailed to make it clear to determine why a subprocess failed. Error messages and stack traces can be set with subprocess.kill(error)
.
Finally, unlike Bash and zx, which are stateful (options, current directory, etc.), Execa is purely functional, which also helps with debugging.
# Bash
bash file.sh
// zx
zx file.js
// or a shebang can be used:
// #!/usr/bin/env zx
// Execa scripts are just regular Node.js files
node file.js
// zx
await $`npm run build`;
// Execa
import {$} from 'execa';
await $`npm run build`;
# Bash
npm run build
// zx
await $`npm run build`;
// Execa
await $`npm run build`;
# Bash
npm run build \
--example-flag-one \
--example-flag-two
// zx
await $`npm run build ${[
'--example-flag-one',
'--example-flag-two',
]}`;
// Execa
await $`npm run build
--example-flag-one
--example-flag-two`;
# Bash
tmpDirectory="/tmp"
mkdir "$tmpDirectory/filename"
// zx
const tmpDirectory = '/tmp'
await $`mkdir ${tmpDirectory}/filename`;
// Execa
const tmpDirectory = '/tmp'
await $`mkdir ${tmpDirectory}/filename`;
# Bash
echo $LANG
// zx
await $`echo $LANG`;
// Execa
await $`echo ${process.env.LANG}`;
# Bash
echo 'one two'
// zx
await $`echo ${'one two'}`;
// Execa
await $`echo ${'one two'}`;
# Bash
echo 'one two' '$'
// zx
await $`echo ${['one two', '$']}`;
// Execa
await $`echo ${['one two', '$']}`;
# Bash
echo "$(npm run build)"
// zx
const result = await $`npm run build`;
await $`echo ${result}`;
// Execa
const result = await $`npm run build`;
await $`echo ${result}`;
# Bash
npm run build && npm run test
// zx
await $`npm run build && npm run test`;
// Execa
await $`npm run build`;
await $`npm run test`;
# Bash
npm run build &
npm run test &
// zx
await Promise.all([$`npm run build`, $`npm run test`]);
// Execa
await Promise.all([$`npm run build`, $`npm run test`]);
# Bash
options="timeout 5"
$options npm run init
$options npm run build
$options npm run test
// zx
const timeout = '5s';
await $`npm run init`.timeout(timeout);
await $`npm run build`.timeout(timeout);
await $`npm run test`.timeout(timeout);
// Execa
import {$ as $_} from 'execa';
const $ = $_({timeout: 5000});
await $`npm run init`;
await $`npm run build`;
await $`npm run test`;
# Bash
EXAMPLE=1 npm run build
// zx
$.env.EXAMPLE = '1';
await $`npm run build`;
delete $.env.EXAMPLE;
// Execa
await $({env: {EXAMPLE: '1'}})`npm run build`;
# Bash
npx tsc --version
// zx
await $`npx tsc --version`;
// Execa
await $`tsc --version`;
// zx
const content = await stdin();
// Execa
import getStdin from 'get-stdin';
const content = await getStdin();
# Bash
echo example
// zx
echo`example`;
// Execa
console.log('example');
# Bash
npm run build 2> /dev/null
// zx
await $`npm run build`.stdio('inherit', 'pipe', 'ignore');
// Execa does not print stdout/stderr by default
await $`npm run build`;
# Bash
set -v
npm run build
// zx >=8
await $`npm run build`.verbose();
// or:
$.verbose = true;
// Execa
await $({verbose: 'full'})`npm run build`;
Or:
NODE_DEBUG=execa node file.js
Which prints:
[19:49:00.360] [0] $ npm run build
Building...
Done.
[19:49:00.383] [0] √ (done in 23ms)
# Bash
echo npm run build | sort | head -n2
// zx
await $`npm run build | sort | head -n2`;
// Execa
await $`npm run build`
.pipe`sort`
.pipe`head -n2`;
# Bash
npm run build |& cat
// zx
const subprocess = $`npm run build`;
const cat = $`cat`;
subprocess.pipe(cat);
subprocess.stderr.pipe(cat.stdin);
await Promise.all([subprocess, cat]);
// Execa
await $({all: true})`npm run build`
.pipe({from: 'all'})`cat`;
# Bash
npm run build > output.txt
// zx
import {createWriteStream} from 'node:fs';
await $`npm run build`.pipe(createWriteStream('output.txt'));
// Execa
await $({stdout: {file: 'output.txt'}})`npm run build`;
# Bash
npm run build &> output.txt
// zx
import {createWriteStream} from 'node:fs';
const subprocess = $`npm run build`;
const fileStream = createWriteStream('output.txt');
subprocess.pipe(fileStream);
subprocess.stderr.pipe(fileStream);
await subprocess;
// Execa
const output = {file: 'output.txt'};
await $({stdout: output, stderr: output})`npm run build`;
# Bash
cat < input.txt
// zx
const cat = $`cat`;
fs.createReadStream('input.txt').pipe(cat.stdin);
await cat;
// Execa
await $({inputFile: 'input.txt'})`cat`;
# Bash
while read
do
if [[ "$REPLY" == *ERROR* ]]
then
echo "$REPLY"
fi
done < <(npm run build)
// zx does not allow proper iteration.
// For example, the iteration does not handle subprocess errors.
// Execa
for await (const line of $`npm run build`) {
if (line.includes('ERROR')) {
console.log(line);
}
}
# Bash communicates errors only through the exit code and stderr
timeout 1 sleep 2
echo $?
// zx
const {
stdout,
stderr,
exitCode,
signal,
} = await $`sleep 2`.timeout('1s');
// file:///home/me/Desktop/node_modules/zx/build/core.js:146
// let output = new ProcessOutput(code, signal, stdout, stderr, combined, message);
// ^
// ProcessOutput [Error]:
// at file:///home/me/Desktop/example.js:2:20
// exit code: null
// signal: SIGTERM
// at ChildProcess.<anonymous> (file:///home/me/Desktop/node_modules/zx/build/core.js:146:26)
// at ChildProcess.emit (node:events:512:28)
// at maybeClose (node:internal/child_process:1098:16)
// at Socket.<anonymous> (node:internal/child_process:456:11)
// at Socket.emit (node:events:512:28)
// at Pipe.<anonymous> (node:net:316:12)
// at Pipe.callbackTrampoline (node:internal/async_hooks:130:17) {
// _code: null,
// _signal: 'SIGTERM',
// _stdout: '',
// _stderr: '',
// _combined: ''
// }
// Execa
const {
stdout,
stderr,
exitCode,
signal,
signalDescription,
originalMessage,
shortMessage,
command,
escapedCommand,
failed,
timedOut,
isCanceled,
isTerminated,
isMaxBuffer,
// and other error-related properties: code, etc.
} = await $({timeout: 1})`sleep 2`;
// ExecaError: Command timed out after 1 milliseconds: sleep 2
// at file:///home/me/Desktop/example.js:2:20
// at ... {
// shortMessage: 'Command timed out after 1 milliseconds: sleep 2\nTimed out',
// originalMessage: '',
// command: 'sleep 2',
// escapedCommand: 'sleep 2',
// cwd: '/path/to/cwd',
// durationMs: 19.95693,
// failed: true,
// timedOut: true,
// isCanceled: false,
// isTerminated: true,
// isMaxBuffer: false,
// signal: 'SIGTERM',
// signalDescription: 'Termination',
// stdout: '',
// stderr: '',
// stdio: [undefined, '', ''],
// pipedFrom: []
// }
# Bash
npm run build
echo $?
// zx
const {exitCode} = await $`npm run build`.nothrow();
echo`${exitCode}`;
// Execa
const {exitCode} = await $({reject: false})`npm run build`;
console.log(exitCode);
# Bash
timeout 5 npm run build
// zx
await $`npm run build`.timeout('5s');
// Execa
await $({timeout: 5000})`npm run build`;
# Bash
echo "$(basename "$0")"
// zx
await $`echo ${__filename}`;
// Execa
await $`echo ${import.meta.filename}`;
# Bash
cd project
// zx
cd('project');
// or:
$.cwd = 'project';
// Execa
const $$ = $({cwd: 'project'});
# Bash
pushd project
pwd
popd
pwd
// zx
within(async () => {
cd('project');
await $`pwd`;
});
await $`pwd`;
// Execa
await $({cwd: 'project'})`pwd`;
await $`pwd`;
# Bash
npm run build &
// zx does not allow setting the `detached` option
// Execa
await $({detached: true})`npm run build`;
# Bash does not allow simple IPC
// zx does not allow simple IPC
// Execa
const subprocess = $({ipc: true})`node script.js`;
subprocess.on('message', message => {
if (message === 'ping') {
subprocess.send('pong');
}
});
# Bash does not allow transforms
// zx does not allow transforms
// Execa
const transform = function * (line) {
if (!line.includes('secret')) {
yield line;
}
};
await $({stdout: [transform, 'inherit']})`echo ${'This is a secret.'}`;
# Bash
kill $PID
// zx
subprocess.kill();
// Execa
// Can specify an error message and stack trace
subprocess.kill(error);
// Or use an `AbortSignal`
const controller = new AbortController();
await $({signal: controller.signal})`node long-script.js`;
# Bash prints stdout and stderr interleaved
// zx separates stdout and stderr
const {stdout, stderr} = await $`node example.js`;
// Execa can interleave stdout and stderr
const {all} = await $({all: true})`node example.js`;
# Bash
npm run build &
echo $!
// zx does not return `subprocess.pid`
// Execa
const {pid} = $`npm run build`;
Next: 🤓 TypeScript
Previous: 📎 Windows
Top: Table of contents