execa logo
# 🔍 Differences with Bash and zx This page describes the differences between [Bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)), Execa, and [zx](https://github.com/google/zx). Execa intends to be more: - [Simple](#simplicity): minimalistic API, no [globals](#global-variables), no [binary](#main-binary), no builtin CLI utilities. - [Cross-platform](#shell): [no shell](shell.md) is used, only JavaScript. - [Secure](#escaping): no shell injection. - [Featureful](#simplicity): all Execa features are available ([text lines iteration](#iterate-over-output-lines), [advanced piping](#piping-stdout-to-another-command), [simple IPC](#ipc), [passing any input type](#pass-any-input-type), [returning any output type](#return-any-output-type), [transforms](#transforms), [web streams](#web-streams), [convert to Duplex stream](#convert-to-duplex-stream), [cleanup on exit](termination.md#current-process-exit), [graceful termination](#graceful-termination), [forceful termination](termination.md#forceful-termination), and [more](../readme.md#documentation)). - [Easy to debug](#debugging): [verbose mode](#verbose-mode-single-command), [detailed errors](#detailed-errors), [messages and stack traces](#cancelation), stateless API. - [Performant](#performance) ## Flexibility 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](#parallel-commands)) to be expressed easily. This also lets you use any Node.js package. ## Shell The main difference between Execa and zx is that Execa does not require any shell. Shell-specific keywords and features are [written in JavaScript](#variable-substitution) 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`](shell.md) option can be used. ## Simplicity Execa's scripting API mostly consists of only two methods: [`` $`command` ``](shell.md) and [`$(options)`](execution.md#globalshared-options). [No special binary](#main-binary) is recommended, no [global variable](#global-variables) is injected: scripts are regular Node.js files. Execa is a thin wrapper around the core Node.js [`child_process` module](https://nodejs.org/api/child_process.html). It lets you use any of its native features. ## Modularity zx includes many builtin utilities: [`fetch()`](#http-requests), [`question()`](#cli-prompts), [`sleep()`](#sleep), [`echo()`](#printing-to-stdout), [`stdin()`](#retrieve-stdin), [`retry()`](#retry-on-error), [`spinner()`](#cli-spinner), [`globby`](#globbing), [`chalk`](https://github.com/chalk/chalk), [`fs`](https://github.com/jprichardson/node-fs-extra), [`os`](https://nodejs.org/api/os.html), [`path`](https://nodejs.org/api/path.html), [`yaml`](https://github.com/eemeli/yaml), [`which`](https://github.com/npm/node-which), [`ps`](https://github.com/webpod/ps), [`tmpfile()`](#temporary-file), [`argv`](#cli-arguments), Markdown scripts, remote scripts. Execa does not include any utility: it focuses on being small and modular instead. [Any Node.js package](https://github.com/sindresorhus/awesome-nodejs#command-line-utilities) can be used in your scripts. ## Performance Spawning a shell for every command comes at a performance cost, which Execa avoids. ## Debugging Subprocesses can be hard to debug, which is why Execa includes a [`verbose`](#verbose-mode-single-command) option. It includes [more information](debugging.md#full-mode) than zx: timestamps, command completion and duration, interleaved commands, IPC messages. Also, Execa's error messages and [properties](#detailed-errors) 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)`](termination.md#error-message-and-stack-trace). Finally, unlike Bash and zx, which are stateful (options, current directory, etc.), Execa is [purely functional](#current-directory), which also helps with debugging. ## Examples ### Main binary ```sh # Bash bash file.sh ``` ```js // zx zx file.js // or a shebang can be used: // #!/usr/bin/env zx ``` ```js // Execa scripts are just regular Node.js files node file.js ``` ### Global variables ```js // zx await $`npm run build`; ``` ```js // Execa import {$} from 'execa'; await $`npm run build`; ``` [More info.](execution.md) ### Command execution ```sh # Bash npm run build ``` ```js // zx await $`npm run build`; ``` ```js // Execa await $`npm run build`; ``` [More info.](execution.md) ### Multiline commands ```sh # Bash npm run build \ --example-flag-one \ --example-flag-two ``` ```js // zx await $`npm run build ${[ '--example-flag-one', '--example-flag-two', ]}`; ``` ```js // Execa await $`npm run build --example-flag-one --example-flag-two`; ``` [More info.](execution.md#multiple-lines) ### Concatenation ```sh # Bash tmpDirectory="/tmp" mkdir "$tmpDirectory/filename" ``` ```js // zx const tmpDirectory = '/tmp' await $`mkdir ${tmpDirectory}/filename`; ``` ```js // Execa const tmpDirectory = '/tmp' await $`mkdir ${tmpDirectory}/filename`; ``` [More info.](execution.md#concatenation) ### Variable substitution ```sh # Bash echo $LANG ``` ```js // zx await $`echo $LANG`; ``` ```js // Execa await $`echo ${process.env.LANG}`; ``` [More info.](input.md#environment-variables) ### Escaping ```sh # Bash echo 'one two' ``` ```js // zx await $`echo ${'one two'}`; ``` ```js // Execa await $`echo ${'one two'}`; ``` [More info.](escaping.md) ### Escaping multiple arguments ```sh # Bash echo 'one two' '$' ``` ```js // zx await $`echo ${['one two', '$']}`; ``` ```js // Execa await $`echo ${['one two', '$']}`; ``` [More info.](execution.md#multiple-arguments) ### Subcommands ```sh # Bash echo "$(npm run build)" ``` ```js // zx const result = await $`npm run build`; await $`echo ${result}`; ``` ```js // Execa const result = await $`npm run build`; await $`echo ${result}`; ``` [More info.](execution.md#subcommands) ### Serial commands ```sh # Bash npm run build && npm run test ``` ```js // zx await $`npm run build && npm run test`; ``` ```js // Execa await $`npm run build`; await $`npm run test`; ``` ### Parallel commands ```sh # Bash npm run build & npm run test & ``` ```js // zx await Promise.all([$`npm run build`, $`npm run test`]); ``` ```js // Execa await Promise.all([$`npm run build`, $`npm run test`]); ``` ### Global/shared options ```sh # Bash options="timeout 5" $options npm run init $options npm run build $options npm run test ``` ```js // zx const $$ = $({verbose: true}); await $$`npm run init`; await $$`npm run build`; await $$`npm run test`; ``` ```js // Execa import {$ as $_} from 'execa'; const $ = $_({verbose: true}); await $`npm run init`; await $`npm run build`; await $`npm run test`; ``` [More info.](execution.md#globalshared-options) ### Environment variables ```sh # Bash EXAMPLE=1 npm run build ``` ```js // zx await $({env: {EXAMPLE: '1'}})`npm run build`; ``` ```js // Execa await $({env: {EXAMPLE: '1'}})`npm run build`; ``` [More info.](input.md#environment-variables) ### Local binaries ```sh # Bash npx tsc --version ``` ```js // zx await $({preferLocal: true})`tsc --version`; ``` ```js // Execa await $({preferLocal: true})`tsc --version`; ``` [More info.](environment.md#local-binaries) ### Retrieve stdin ```sh # Bash read content ``` ```js // zx const content = await stdin(); ``` ```js // Execa import getStdin from 'get-stdin'; const content = await getStdin(); ``` [More info.](https://github.com/sindresorhus/get-stdin) ### Pass input to stdin ```sh # Bash cat <<<"example" ``` ```js // zx $({input: 'example'})`cat`; ``` ```js // Execa $({input: 'example'})`cat`; ``` ### Pass any input type ```sh # Bash only allows passing strings as input ``` ```js // zx only allows passing specific input types ``` ```js // Execa - main.js const ipcInput = [ {task: 'lint', ignore: /test\.js/}, {task: 'copy', files: new Set(['main.js', 'index.js']), }]; await $({ipcInput})`node build.js`; ``` ```js // Execa - build.js import {getOneMessage} from 'execa'; const ipcInput = await getOneMessage(); ``` [More info.](ipc.md#send-an-initial-message) ### Return any output type ```sh # Bash only allows returning strings as output ``` ```js // zx only allows returning specific output types ``` ```js // Execa - main.js const {ipcOutput} = await $({ipc: true})`node build.js`; console.log(ipcOutput[0]); // {kind: 'start', timestamp: date} console.log(ipcOutput[1]); // {kind: 'stop', timestamp: date} ``` ```js // Execa - build.js import {sendMessage} from 'execa'; await sendMessage({kind: 'start', timestamp: new Date()}); await runBuild(); await sendMessage({kind: 'stop', timestamp: new Date()}); ``` [More info.](ipc.md#retrieve-all-messages) ### Printing to stdout ```sh # Bash echo example ``` ```js // zx echo`example`; ``` ```js // Execa console.log('example'); ``` ### Silent stdout ```sh # Bash npm run build > /dev/null ``` ```js // zx await $`npm run build`.quiet(); ``` ```js // Execa does not print stdout by default await $`npm run build`; ``` ### Binary output ```sh # Bash usually requires redirecting binary output zip -r - input.txt > output.txt ``` ```js // zx const stdout = await $`zip -r - input.txt`.buffer(); ``` ```js // Execa const {stdout} = await $({encoding: 'buffer'})`zip -r - input.txt`; ``` [More info.](binary.md#binary-output) ### Verbose mode (single command) ```sh # Bash set -v npm run build set +v ``` ```js // zx await $`npm run build`.verbose(); ``` ```js // Execa await $({verbose: 'full'})`npm run build`; ``` [More info.](debugging.md#verbose-mode) ### Verbose mode (global) ```sh # Bash set -v npm run build ``` ``` // zx $ zx --verbose file.js $ npm run build Building... Done. ``` ``` $ NODE_DEBUG=execa node file.js [19:49:00.360] [0] $ npm run build [19:49:00.360] [0] Building... [19:49:00.360] [0] Done. [19:49:00.383] [0] √ (done in 23ms) ``` [More info.](debugging.md#global-mode) ### Piping stdout to another command ```sh # Bash echo npm run build | sort | head -n2 ``` ```js // zx await $`npm run build` .pipe($`sort`) .pipe($`head -n2`); ``` ```js // Execa await $`npm run build` .pipe`sort` .pipe`head -n2`; ``` [More info.](pipe.md) ### Piping stdout and stderr to another command ```sh # Bash npm run build |& cat ``` ```js // zx const subprocess = $`npm run build`; const cat = $`cat`; subprocess.pipe(cat); subprocess.stderr.pipe(cat.stdin); await Promise.all([subprocess, cat]); ``` ```js // Execa await $({all: true})`npm run build` .pipe({from: 'all'})`cat`; ``` [More info.](pipe.md#source-file-descriptor) ### Piping stdout to a file ```sh # Bash npm run build > output.txt ``` ```js // zx import {createWriteStream} from 'node:fs'; await $`npm run build`.pipe(createWriteStream('output.txt')); ``` ```js // Execa await $({stdout: {file: 'output.txt'}})`npm run build`; ``` [More info.](output.md#file-output) ### Append stdout to a file ```sh # Bash npm run build >> output.txt ``` ```js // zx import {createWriteStream} from 'node:fs'; await $`npm run build`.pipe(createWriteStream('output.txt', {flags: 'a'})); ``` ```js // Execa await $({stdout: {file: 'output.txt', append: true}})`npm run build`; ``` [More info.](output.md#file-output) ### Piping interleaved stdout and stderr to a file ```sh # Bash npm run build &> output.txt ``` ```js // 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; ``` ```js // Execa const output = {file: 'output.txt'}; await $({stdout: output, stderr: output})`npm run build`; ``` [More info.](output.md#file-output) ### Piping stdin from a file ```sh # Bash cat < input.txt ``` ```js // zx const cat = $`cat`; fs.createReadStream('input.txt').pipe(cat.stdin); await cat; ``` ```js // Execa await $({inputFile: 'input.txt'})`cat`; ``` [More info.](input.md#file-input) ### Web streams ```js // zx does not support web streams ``` ```js // Execa const response = await fetch('https://example.com'); await $({stdin: response.body})`npm run build`; ``` [More info.](streams.md#web-streams) ### Convert to Duplex stream ```js // zx does not support converting subprocesses to streams ``` ```js // Execa import {pipeline} from 'node:stream/promises'; import {createReadStream, createWriteStream} from 'node:fs'; await pipeline( createReadStream('./input.txt'), $`node ./transform.js`.duplex(), createWriteStream('./output.txt'), ); ``` [More info.](streams.md#convert) ### Handle pipeline errors ```sh # Bash set -e npm run crash | sort | head -n2 ``` ```js // zx try { await $`npm run crash` .pipe($`sort`) .pipe($`head -n2`); // This is never reached. // The process crashes instead. } catch (error) { console.error(error); } ``` ```js // Execa try { await $`npm run build` .pipe`sort` .pipe`head -n2`; } catch (error) { console.error(error); } ``` [More info.](pipe.md#errors) ### Return all pipeline results ```sh # Bash only allows returning each command's exit code npm run crash | sort | head -n2 # 1 0 0 echo "${PIPESTATUS[@]}" ``` ```js // zx only returns the last command's result ``` ```js // Execa const destinationResult = await execa`npm run build` .pipe`head -n 2`; console.log(destinationResult.stdout); // First 2 lines of `npm run build` const sourceResult = destinationResult.pipedFrom[0]; console.log(sourceResult.stdout); // Full output of `npm run build` ``` [More info.](pipe.md#result) ### Split output into lines ```sh # Bash npm run build | IFS='\n' read -ra lines ``` ```js // zx const lines = await $`npm run build`.lines(); ``` ```js // Execa const lines = await $({lines: true})`npm run build`; ``` [More info.](lines.md#simple-splitting) ### Iterate over output lines ```sh # Bash while read do if [[ "$REPLY" == *ERROR* ]] then echo "$REPLY" fi done < <(npm run build) ``` ```js // zx does not allow easily iterating over output lines. // Also, the iteration does not handle subprocess errors. ``` ```js // Execa for await (const line of $`npm run build`) { if (line.includes('ERROR')) { console.log(line); } } ``` [More info.](lines.md#progressive-splitting) ### Detailed errors ```sh # Bash communicates errors only through the exit code and stderr timeout 1 sleep 2 echo $? ``` ```js // zx await $`sleep 2`.timeout('1ms'); // Error: // at file:///home/me/Desktop/example.js:6:12 // exit code: null // signal: SIGTERM ``` ```js // Execa 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: [] // } ``` [More info.](errors.md) ### Exit codes ```sh # Bash npm run build echo $? ``` ```js // zx const {exitCode} = await $`npm run build`.nothrow(); ``` ```js // Execa const {exitCode} = await $({reject: false})`npm run build`; ``` [More info.](errors.md#exit-code) ### Timeouts ```sh # Bash timeout 5 npm run build ``` ```js // zx await $`npm run build`.timeout('5s'); ``` ```js // Execa await $({timeout: 5000})`npm run build`; ``` [More info.](termination.md#timeout) ### Current filename ```sh # Bash echo "$(basename "$0")" ``` ```js // zx await $`echo ${__filename}`; ``` ```js // Execa await $`echo ${import.meta.filename}`; ``` ### Current directory ```sh # Bash cd project ``` ```js // zx const $$ = $({cwd: 'project'}); // Or: cd('project'); ``` ```js // Execa const $$ = $({cwd: 'project'}); ``` [More info.](environment.md#current-directory) ### Background subprocess ```sh # Bash npm run build & ``` ```js // zx await $({detached: true})`npm run build`; ``` ```js // Execa await $({detached: true})`npm run build`; ``` [More info.](environment.md#background-subprocess) ### IPC ```sh # Bash does not allow simple IPC ``` ```js // zx does not allow simple IPC ``` ```js // Execa const subprocess = $({node: true})`script.js`; for await (const message of subprocess.getEachMessage()) { if (message === 'ping') { await subprocess.sendMessage('pong'); } }); ``` [More info.](ipc.md) ### Transforms ```sh # Bash does not allow transforms ``` ```js // zx does not allow transforms ``` ```js // Execa const transform = function * (line) { if (!line.includes('secret')) { yield line; } }; await $({stdout: [transform, 'inherit']})`echo ${'This is a secret.'}`; ``` [More info.](transform.md) ### Signal termination ```sh # Bash kill $PID ``` ```js // zx subprocess.kill(); ``` ```js // Execa subprocess.kill(); // Or with an error message and stack trace: subprocess.kill(error); ``` [More info.](termination.md#signal-termination) ### Default signal ```sh # Bash does not allow changing the default termination signal ``` ```js // zx only allows changing the signal used for timeouts const $$ = $({timeoutSignal: 'SIGINT'}); ``` ```js // Execa const $ = $_({killSignal: 'SIGINT'}); ``` [More info.](termination.md#default-signal) ### Cancelation ```sh # Bash kill $PID ``` ```js // zx const controller = new AbortController(); await $({signal: controller.signal})`node long-script.js`; ``` ```js // Execa const controller = new AbortController(); await $({cancelSignal: controller.signal})`node long-script.js`; ``` [More info.](termination.md#canceling) ### Graceful termination ```sh # Bash trap cleanup SIGTERM ``` ```js // zx // This does not work on Windows process.on('SIGTERM', () => { // ... }); ``` ```js // Execa - main.js const controller = new AbortController(); await $({ cancelSignal: controller.signal, gracefulCancel: true, })`node build.js`; ``` ```js // Execa - build.js import {getCancelSignal} from 'execa'; const cancelSignal = await getCancelSignal(); await fetch('https://example.com', {signal: cancelSignal}); ``` ### Interleaved output ```sh # Bash prints stdout and stderr interleaved ``` ```js // zx const all = String(await $`node example.js`); ``` ```js // Execa const {all} = await $({all: true})`node example.js`; ``` [More info.](output.md#interleaved-output) ### PID ```sh # Bash npm run build & echo $! ``` ```js // zx does not return `subprocess.pid` ``` ```js // Execa const {pid} = $`npm run build`; ``` [More info.](termination.md#inter-process-termination) ### CLI arguments ```js // zx const {myCliFlag} = argv; ``` ```js // Execa import {parseArgs} from 'node:util'; const {myCliFlag} = parseArgs({strict: false}).values; ``` [More info.](https://nodejs.org/api/util.html#utilparseargsconfig) ### CLI prompts ```sh # Bash read -p "Question? " answer ``` ```js // zx const answer = await question('Question? '); ``` ```js // Execa import input from '@inquirer/input'; const answer = await input({message: 'Question?'}); ``` [More info.](https://github.com/SBoudrias/Inquirer.js) ### CLI spinner ```sh # Bash does not provide with a builtin spinner ``` ```js // zx await spinner(() => $`node script.js`); ``` ```js // Execa import {oraPromise} from 'ora'; await oraPromise($`node script.js`); ``` [More info.](https://github.com/sindresorhus/ora) ### Sleep ```sh # Bash sleep 5 ``` ```js // zx await sleep(5000); ``` ```js // Execa import {setTimeout} from 'node:timers/promises'; await setTimeout(5000); ``` [More info.](https://nodejs.org/api/timers.html#timerspromisessettimeoutdelay-value-options) ### Globbing ```sh # Bash ls packages/* ``` ```js // zx const files = await glob(['packages/*']); ``` ```js // Execa import {glob} from 'node:fs/promises'; const files = await Array.fromAsync(glob('packages/*')); ``` [More info.](https://nodejs.org/api/fs.html#fspromisesglobpattern-options) ### Temporary file ```js // zx const filePath = tmpfile(); ``` ```js // Execa import tempfile from 'tempfile'; const filePath = tempfile(); ``` [More info.](https://github.com/sindresorhus/tempfile) ### HTTP requests ```sh # Bash curl https://github.com ``` ```js // zx await fetch('https://github.com'); ``` ```js // Execa await fetch('https://github.com'); ``` [More info.](https://nodejs.org/api/globals.html#fetch) ### Retry on error ```js // zx await retry( 5, () => $`curl -sSL https://sindresorhus.com/unicorn`, ) ``` ```js // Execa import pRetry from 'p-retry'; await pRetry( () => $`curl -sSL https://sindresorhus.com/unicorn`, {retries: 5}, ); ``` [More info.](https://github.com/sindresorhus/p-retry)
[**Next**: 🐭 Small packages](small.md)\ [**Previous**: 📎 Windows](windows.md)\ [**Top**: Table of contents](../readme.md#documentation)