Skip to content

Latest commit

 

History

History
1295 lines (971 loc) · 21 KB

bash.md

File metadata and controls

1295 lines (971 loc) · 21 KB
execa logo

🔍 Differences with Bash and zx

This page describes the differences between Bash, Execa, and zx. Execa intends to be more:

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) 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 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.

Simplicity

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. It lets you use any of its native features.

Modularity

zx includes many builtin utilities: fetch(), question(), sleep(), echo(), stdin(), retry(), spinner(), globby, chalk, fs, os, path, yaml, which, ps, tmpfile(), argv, 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.

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 option. It includes more information than zx: timestamps, command completion and duration, interleaved commands, IPC messages.

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.

Examples

Main binary

# 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

Global variables

// zx
await $`npm run build`;
// Execa
import {$} from 'execa';

await $`npm run build`;

More info.

Command execution

# Bash
npm run build
// zx
await $`npm run build`;
// Execa
await $`npm run build`;

More info.

Multiline commands

# 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`;

More info.

Concatenation

# Bash
tmpDirectory="/tmp"
mkdir "$tmpDirectory/filename"
// zx
const tmpDirectory = '/tmp'
await $`mkdir ${tmpDirectory}/filename`;
// Execa
const tmpDirectory = '/tmp'
await $`mkdir ${tmpDirectory}/filename`;

More info.

Variable substitution

# Bash
echo $LANG
// zx
await $`echo $LANG`;
// Execa
await $`echo ${process.env.LANG}`;

More info.

Escaping

# Bash
echo 'one two'
// zx
await $`echo ${'one two'}`;
// Execa
await $`echo ${'one two'}`;

More info.

Escaping multiple arguments

# Bash
echo 'one two' '$'
// zx
await $`echo ${['one two', '$']}`;
// Execa
await $`echo ${['one two', '$']}`;

More info.

Subcommands

# 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}`;

More info.

Serial commands

# Bash
npm run build && npm run test
// zx
await $`npm run build && npm run test`;
// Execa
await $`npm run build`;
await $`npm run test`;

Parallel commands

# 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`]);

Global/shared options

# Bash
options="timeout 5"
$options npm run init
$options npm run build
$options npm run test
// zx
const $$ = $({verbose: true});

await $$`npm run init`;
await $$`npm run build`;
await $$`npm run test`;
// Execa
import {$ as $_} from 'execa';

const $ = $_({verbose: true});

await $`npm run init`;
await $`npm run build`;
await $`npm run test`;

More info.

Environment variables

# Bash
EXAMPLE=1 npm run build
// zx
await $({env: {EXAMPLE: '1'}})`npm run build`;
// Execa
await $({env: {EXAMPLE: '1'}})`npm run build`;

More info.

Local binaries

# Bash
npx tsc --version
// zx
await $({preferLocal: true})`tsc --version`;
// Execa
await $({preferLocal: true})`tsc --version`;

More info.

Retrieve stdin

# Bash
read content
// zx
const content = await stdin();
// Execa
import getStdin from 'get-stdin';

const content = await getStdin();

More info.

Pass input to stdin

# Bash
cat <<<"example"
// zx
$({input: 'example'})`cat`;
// Execa
$({input: 'example'})`cat`;

Pass any input type

# Bash only allows passing strings as input
// zx only allows passing specific input types
// Execa - main.js
const ipcInput = [
	{task: 'lint', ignore: /test\.js/},
	{task: 'copy', files: new Set(['main.js', 'index.js']),
}];
await $({ipcInput})`node build.js`;
// Execa - build.js
import {getOneMessage} from 'execa';

const ipcInput = await getOneMessage();

More info.

Return any output type

# Bash only allows returning strings as output
// zx only allows returning specific output types
// 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}
// 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.

Printing to stdout

# Bash
echo example
// zx
echo`example`;
// Execa
console.log('example');

Silent stdout

# Bash
npm run build > /dev/null
// zx
await $`npm run build`.quiet();
// Execa does not print stdout by default
await $`npm run build`;

Binary output

# Bash usually requires redirecting binary output
zip -r - input.txt > output.txt
// zx
const stdout = await $`zip -r - input.txt`.buffer();
// Execa
const {stdout} = await $({encoding: 'buffer'})`zip -r - input.txt`;

More info.

Verbose mode (single command)

# Bash
set -v
npm run build
set +v
// zx
await $`npm run build`.verbose();
// Execa
await $({verbose: 'full'})`npm run build`;

More info.

Verbose mode (global)

# 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.

Piping stdout to another command

# Bash
echo npm run build | sort | head -n2
// zx
await $`npm run build`
	.pipe($`sort`)
	.pipe($`head -n2`);
// Execa
await $`npm run build`
	.pipe`sort`
	.pipe`head -n2`;

More info.

Piping stdout and stderr to another command

# 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`;

More info.

Piping stdout to a file

# 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`;

More info.

Append stdout to a file

# Bash
npm run build >> output.txt
// zx
import {createWriteStream} from 'node:fs';

await $`npm run build`.pipe(createWriteStream('output.txt', {flags: 'a'}));
// Execa
await $({stdout: {file: 'output.txt', append: true}})`npm run build`;

More info.

Piping interleaved stdout and stderr to a file

# 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`;

More info.

Piping stdin from a file

# Bash
cat < input.txt
// zx
const cat = $`cat`;
fs.createReadStream('input.txt').pipe(cat.stdin);
await cat;
// Execa
await $({inputFile: 'input.txt'})`cat`;

More info.

Web streams

// zx does not support web streams
// Execa
const response = await fetch('https://example.com');
await $({stdin: response.body})`npm run build`;

More info.

Convert to Duplex stream

// zx does not support converting subprocesses to streams
// 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.

Handle pipeline errors

# Bash
set -e
npm run crash | sort | head -n2
// zx
try {
	await $`npm run crash`
		.pipe($`sort`)
		.pipe($`head -n2`);
// This is never reached.
// The process crashes instead.
} catch (error) {
	console.error(error);
}
// Execa
try {
	await $`npm run build`
		.pipe`sort`
		.pipe`head -n2`;
} catch (error) {
	console.error(error);
}

More info.

Return all pipeline results

# Bash only allows returning each command's exit code
npm run crash | sort | head -n2
# 1 0 0
echo "${PIPESTATUS[@]}"
// zx only returns the last command's result
// 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.

Split output into lines

# Bash
npm run build | IFS='\n' read -ra lines
// zx
const lines = await $`npm run build`.lines();
// Execa
const lines = await $({lines: true})`npm run build`;

More info.

Iterate over output lines

# Bash
while read
do
	if [[ "$REPLY" == *ERROR* ]]
	then
		echo "$REPLY"
	fi
done < <(npm run build)
// zx does not allow easily iterating over output lines.
// Also, the iteration does not handle subprocess errors.
// Execa
for await (const line of $`npm run build`) {
	if (line.includes('ERROR')) {
		console.log(line);
	}
}

More info.

Detailed errors

# Bash communicates errors only through the exit code and stderr
timeout 1 sleep 2
echo $?
// zx
await $`sleep 2`.timeout('1ms');
// Error:
//   at file:///home/me/Desktop/example.js:6:12
//   exit code: null
//   signal: SIGTERM
// 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.

Exit codes

# Bash
npm run build
echo $?
// zx
const {exitCode} = await $`npm run build`.nothrow();
// Execa
const {exitCode} = await $({reject: false})`npm run build`;

More info.

Timeouts

# Bash
timeout 5 npm run build
// zx
await $`npm run build`.timeout('5s');
// Execa
await $({timeout: 5000})`npm run build`;

More info.

Current filename

# Bash
echo "$(basename "$0")"
// zx
await $`echo ${__filename}`;
// Execa
await $`echo ${import.meta.filename}`;

Current directory

# Bash
cd project
// zx
const $$ = $({cwd: 'project'});

// Or:
cd('project');
// Execa
const $$ = $({cwd: 'project'});

More info.

Background subprocess

# Bash
npm run build &
// zx
await $({detached: true})`npm run build`;
// Execa
await $({detached: true})`npm run build`;

More info.

IPC

# Bash does not allow simple IPC
// zx does not allow simple IPC
// Execa
const subprocess = $({node: true})`script.js`;

for await (const message of subprocess.getEachMessage()) {
	if (message === 'ping') {
		await subprocess.sendMessage('pong');
	}
});

More info.

Transforms

# 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.'}`;

More info.

Signal termination

# Bash
kill $PID
// zx
subprocess.kill();
// Execa
subprocess.kill();

// Or with an error message and stack trace:
subprocess.kill(error);

More info.

Default signal

# Bash does not allow changing the default termination signal
// zx only allows changing the signal used for timeouts
const $$ = $({timeoutSignal: 'SIGINT'});
// Execa
const $ = $_({killSignal: 'SIGINT'});

More info.

Cancelation

# Bash
kill $PID
// zx
const controller = new AbortController();
await $({signal: controller.signal})`node long-script.js`;
// Execa
const controller = new AbortController();
await $({cancelSignal: controller.signal})`node long-script.js`;

More info.

Graceful termination

# Bash
trap cleanup SIGTERM
// zx
// This does not work on Windows
process.on('SIGTERM', () => {
	// ...
});
// Execa - main.js
const controller = new AbortController();
await $({
	cancelSignal: controller.signal,
	gracefulCancel: true,
})`node build.js`;
// Execa - build.js
import {getCancelSignal} from 'execa';

const cancelSignal = await getCancelSignal();
await fetch('https://example.com', {signal: cancelSignal});

Interleaved output

# Bash prints stdout and stderr interleaved
// zx
const all = String(await $`node example.js`);
// Execa
const {all} = await $({all: true})`node example.js`;

More info.

PID

# Bash
npm run build &
echo $!
// zx does not return `subprocess.pid`
// Execa
const {pid} = $`npm run build`;

More info.

CLI arguments

// zx
const {myCliFlag} = argv;
// Execa
import {parseArgs} from 'node:util';

const {myCliFlag} = parseArgs({strict: false}).values;

More info.

CLI prompts

# Bash
read -p "Question? " answer
// zx
const answer = await question('Question? ');
// Execa
import input from '@inquirer/input';

const answer = await input({message: 'Question?'});

More info.

CLI spinner

# Bash does not provide with a builtin spinner
// zx
await spinner(() => $`node script.js`);
// Execa
import {oraPromise} from 'ora';

await oraPromise($`node script.js`);

More info.

Sleep

# Bash
sleep 5
// zx
await sleep(5000);
// Execa
import {setTimeout} from 'node:timers/promises';

await setTimeout(5000);

More info.

Globbing

# Bash
ls packages/*
// zx
const files = await glob(['packages/*']);
// Execa
import {glob} from 'node:fs/promises';

const files = await Array.fromAsync(glob('packages/*'));

More info.

Temporary file

// zx
const filePath = tmpfile();
// Execa
import tempfile from 'tempfile';

const filePath = tempfile();

More info.

HTTP requests

# Bash
curl https://github.com
// zx
await fetch('https://github.com');
// Execa
await fetch('https://github.com');

More info.

Retry on error

// zx
await retry(
	5,
	() => $`curl -sSL https://sindresorhus.com/unicorn`,
)
// Execa
import pRetry from 'p-retry';

await pRetry(
	() => $`curl -sSL https://sindresorhus.com/unicorn`,
	{retries: 5},
);

More info.


Next: 🐭 Small packages
Previous: 📎 Windows
Top: Table of contents