With Execa, you can write scripts with Node.js instead of a shell language. It is secure, performant, simple and cross-platform.
import {$} from 'execa';
const {stdout: name} = await $`cat package.json`
.pipeStdout($({stdin: 'pipe'})`grep name`);
console.log(name);
const branch = await $`git branch --show-current`;
await $`dep deploy --branch=${branch}`;
await Promise.all([
$`sleep 1`,
$`sleep 2`,
$`sleep 3`,
]);
const dirName = 'foo bar';
await $`mkdir /tmp/${dirName}`;
This file describes the differences between Bash, Execa, and zx (which inspired this feature).
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
, signal
, 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
.
Child processes 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 process failed.
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 $`echo example`;
// Execa
import {$} from 'execa';
await $`echo example`;
# Bash
echo example
// zx
await $`echo example`;
// Execa
await $`echo example`;
# Bash
echo "$(echo example)"
// zx
const example = await $`echo example`;
await $`echo ${example}`;
// Execa
const example = await $`echo example`;
await $`echo ${example}`;
# Bash
tmpDir="/tmp"
mkdir "$tmpDir/filename"
// zx
const tmpDir = '/tmp'
await $`mkdir ${tmpDir}/filename`;
// Execa
const tmpDir = '/tmp'
await $`mkdir ${tmpDir}/filename`;
# Bash
echo one &
echo two &
// zx
await Promise.all([$`echo one`, $`echo two`]);
// Execa
await Promise.all([$`echo one`, $`echo two`]);
# Bash
echo one && echo two
// zx
await $`echo one && echo two`;
// Execa
await $`echo one`;
await $`echo two`;
# 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 $LANG
// zx
await $`echo $LANG`;
// Execa
await $`echo ${process.env.LANG}`;
# Bash
EXAMPLE=1 example_command
// zx
$.env.EXAMPLE = '1';
await $`example_command`;
delete $.env.EXAMPLE;
// Execa
await $({env: {EXAMPLE: '1'}})`example_command`;
# 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 "$(basename "$0")"
// zx
await $`echo ${__filename}`;
// Execa
import {fileURLToPath} from 'node:url';
import path from 'node:path';
const __filename = path.basename(fileURLToPath(import.meta.url));
await $`echo ${__filename}`;
# Bash
set -v
echo example
// zx >=8
await $`echo example`.verbose();
// or:
$.verbose = true;
// Execa
const $$ = $({verbose: true});
await $$`echo example`;
Or:
NODE_DEBUG=execa node file.js
# 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
false
echo $?
// zx
const {exitCode} = await $`false`.nothrow();
echo`${exitCode}`;
// Execa
const {exitCode} = await $({reject: false})`false`;
console.log(exitCode);
# Bash
timeout 5 echo example
// zx
await $`echo example`.timeout('5s');
// Execa
await $({timeout: 5000})`echo example`;
# Bash
echo example &
echo $!
// zx does not return `childProcess.pid`
// Execa
const {pid} = $`echo example`;
# 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,
killed,
// and other error-related properties: code, etc.
} = await $({timeout: 1})`sleep 2`;
// file:///home/me/code/execa/lib/kill.js:60
// reject(Object.assign(new Error('Timed out'), {timedOut: true, signal}));
// ^
// Error: Command timed out after 1 milliseconds: sleep 2
// Timed out
// at file:///home/me/Desktop/example.js:2:20
// timedOut: true,
// signal: 'SIGTERM',
// originalMessage: 'Timed out',
// shortMessage: 'Command timed out after 1 milliseconds: sleep 2\nTimed out',
// command: 'sleep 2',
// escapedCommand: 'sleep 2',
// exitCode: undefined,
// signalDescription: 'Termination',
// stdout: '',
// stderr: '',
// failed: true,
// isCanceled: false,
// killed: false
// }
# Bash
options="timeout 5"
$options echo one
$options echo two
$options echo three
// zx
const timeout = '5s';
await $`echo one`.timeout(timeout);
await $`echo two`.timeout(timeout);
await $`echo three`.timeout(timeout);
// Execa
const $$ = $({timeout: 5000});
await $$`echo one`;
await $$`echo two`;
await $$`echo three`;
# Bash
echo one &
// zx does not allow setting the `detached` option
// Execa
await $({detached: true})`echo one`;
# Bash
echo example
// zx
echo`example`;
// Execa
console.log('example');
# Bash
echo example | cat
// zx
await $`echo example | cat`;
// Execa
await $`echo example`.pipeStdout($({stdin: 'pipe'})`cat`);
# Bash
echo example |& cat
// zx
const echo = $`echo example`;
const cat = $`cat`;
echo.pipe(cat)
echo.stderr.pipe(cat.stdin);
await Promise.all([echo, cat]);
// Execa
await $({all: true})`echo example`.pipeAll($({stdin: 'pipe'})`cat`);
# Bash
echo example > file.txt
// zx
await $`echo example`.pipe(fs.createWriteStream('file.txt'));
// Execa
await $`echo example`.pipeStdout('file.txt');
# Bash
echo example < file.txt
// zx
const cat = $`cat`
fs.createReadStream('file.txt').pipe(cat.stdin)
await cat
// Execa
await $({inputFile: 'file.txt'})`cat`
# Bash
echo example 2> /dev/null
// zx
await $`echo example`.stdio('inherit', 'pipe', 'ignore');
// Execa does not forward stdout/stderr by default
await $`echo example`;