Skip to content

Latest commit

 

History

History
676 lines (522 loc) · 10.9 KB

scripts.md

File metadata and controls

676 lines (522 loc) · 10.9 KB

Node.js scripts

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($`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}`;

Summary

This file describes the differences between Bash, Execa, and zx (which inspired this feature).

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, signal, 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

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.

Details

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 $`echo example`;
// Execa
import {$} from 'execa';

await $`echo example`;

Command execution

# Bash
echo example
// zx
await $`echo example`;
// Execa
await $`echo example`;

Subcommands

# Bash
echo "$(echo example)"
// zx
const example = await $`echo example`;
await $`echo ${example}`;
// Execa
const example = await $`echo example`;
await $`echo ${example}`;

Concatenation

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

Parallel commands

# Bash
echo one &
echo two &
// zx
await Promise.all([$`echo one`, $`echo two`]);
// Execa
await Promise.all([$`echo one`, $`echo two`]);

Serial commands

# Bash
echo one && echo two
// zx
await $`echo one && echo two`;
// Execa
await $`echo one`;
await $`echo two`;

Local binaries

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

Builtin utilities

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

const content = await getStdin();

Variable substitution

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

Environment variables

# Bash
EXAMPLE=1 example_command
// zx
$.env.EXAMPLE = '1';
await $`example_command`;
delete $.env.EXAMPLE;
// Execa
await $({env: {EXAMPLE: '1'}})`example_command`;

Escaping

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

Escaping multiple arguments

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

Current filename

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

Verbose mode

# 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

Current directory

# Bash
cd project
// zx
cd('project');

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

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

Exit codes

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

Timeouts

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

PID

# Bash
echo example &
echo $!
// zx does not return `childProcess.pid`
// Execa
const {pid} = $`echo example`;

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,
  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
// }

Shared options

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

Background processes

# Bash
echo one &
// zx does not allow setting the `detached` option
// Execa
await $({detached: true})`echo one`;

Printing to stdout

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

Piping stdout to another command

# Bash
echo example | cat
// zx
await $`echo example | cat`;
// Execa
await $`echo example`.pipeStdout($`cat`);

Piping stdout and stderr to another command

# 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 $`echo example`.pipeAll($`cat`);

Piping stdout to a file

# Bash
echo example > file.txt
// zx
await $`echo example`.pipe(fs.createWriteStream('file.txt'));
// Execa
await $`echo example`.pipeStdout('file.txt');

Piping stdin from a file

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

Silent stderr

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