Skip to content

Latest commit

 

History

History
894 lines (681 loc) · 15.1 KB

bash.md

File metadata and controls

894 lines (681 loc) · 15.1 KB
execa logo

🔍 Differences with Bash and zx

This page describes the differences between Bash, Execa, and zx (which inspired this feature). 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. Unlike zx, it lets you use any of its native features: pid, IPC, unref(), detached, uid, gid, cancelSignal, etc.

Modularity

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.

Performance

Spawning a shell for every command comes at a performance cost, which Execa avoids.

Also, local binaries can be directly executed without using npx.

Debugging

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.

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

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

More info.

Environment variables

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

More info.

Local binaries

# Bash
npx tsc --version
// zx
await $`npx tsc --version`;
// Execa
await $`tsc --version`;

More info.

Builtin utilities

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

const content = await getStdin();

Printing to stdout

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

Silent stderr

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

Verbose mode

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

More info.

Piping stdout to another command

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

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.

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.

Iterate over output lines

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

More info.

Errors

# 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: []
// }

More info.

Exit codes

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

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
cd('project');

// or:
$.cwd = 'project';
// Execa
const $$ = $({cwd: 'project'});

More info.

Multiple current directories

# Bash
pushd project
pwd
popd
pwd
// zx
within(async () => {
	cd('project');
	await $`pwd`;
});

await $`pwd`;
// Execa
await $({cwd: 'project'})`pwd`;
await $`pwd`;

More info.

Background subprocess

# Bash
npm run build &
// zx does not allow setting the `detached` option
// 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 = $({ipc: true})`node script.js`;

subprocess.on('message', message => {
	if (message === 'ping') {
		subprocess.send('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.

Cancelation

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

More info.

Interleaved output

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

More info.

PID

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

More info.


Next: 🤓 TypeScript
Previous: 📎 Windows
Top: Table of contents