diff --git a/docs/scripts.md b/docs/scripts.md new file mode 100644 index 000000000..119f3a900 --- /dev/null +++ b/docs/scripts.md @@ -0,0 +1,635 @@ +# Node.js scripts + +With Execa, you can write scripts with Node.js instead of a shell language. It is [secure](#escaping), [performant](#performance), [simple](#simplicity) and [cross-platform](#shell). + +```js +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](https://github.com/google/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](#parallel-commands)) to be expressed easily. This also lets you use [any Node module](#builtin-utilities). + +### 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` option](../readme.md#shell) can be used. + +### Simplicity + +Execa's scripting API mostly consists of only two methods: [``$`command` ``](../readme.md#command) and +[`$(options)`](../readme.md#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). Unlike zx, it lets you use [any of its native features](#background-processes): [`pid`](#pid), [IPC](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback), [`unref()`](https://nodejs.org/api/child_process.html#subprocessunref), [`detached`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options), [`uid`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options), [`gid`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options), [`signal`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options), 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](#builtin-utilities): it focuses on being small and modular instead. Any Node module can be used in your scripts. + +### Performance + +Spawning a shell for every command comes at a performance cost, which Execa avoids. + +Also, [local binaries](#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](#verbose-mode). + +Also, Execa's error messages and [properties](#errors) 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](#current-directory), which also helps with debugging. + +## Details + +### 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 $`echo example`; +``` + +```js +// Execa +import {$} from 'execa'; +await $`echo example`; +``` + +### Command execution + +```sh +# Bash +echo example +``` + +```js +// zx +await $`echo example`; +``` + +```js +// Execa +await $`echo example`; +``` + +### Subcommands + +```sh +# Bash +echo "$(echo example)" +``` + +```js +// zx +const example = await $`echo example`; +await $`echo ${example}`; +``` + +```js +// Execa +const example = await $`echo example`; +await $`echo ${example}`; +``` + +### Parallel commands + +```sh +# Bash +echo one & +echo two & +``` + +```js +// zx +await Promise.all([$`echo one`, $`echo two`]); +``` + +```js +// Execa +await Promise.all([$`echo one`, $`echo two`]); +``` + +### Serial commands + +```sh +# Bash +echo one && echo two +``` + +```js +// zx +await $`echo one && echo two`; +``` + +```js +// Execa +await $`echo one`; +await $`echo two`; +``` + +### Local binaries + +```sh +# Bash +npx tsc --version +``` + +```js +// zx +await $`npx tsc --version`; +``` + +```js +// Execa +await $`tsc --version`; +``` + +### Builtin utilities + +```js +// zx +const content = await stdin(); +``` + +```js +// Execa +import getStdin from 'get-stdin'; +const content = await getStdin(); +``` + +### Variable substitution + +```sh +# Bash +echo $LANG +``` + +```js +// zx +await $`echo $LANG`; +``` + +```js +// Execa +await $`echo ${process.env.LANG}`; +``` + +### Environment variables + +```sh +# Bash +EXAMPLE=1 example_command +``` + +```js +// zx +$.env.EXAMPLE = '1'; +await $`example_command`; +delete $.env.EXAMPLE; +``` + +```js +// Execa +await $({env: {EXAMPLE: '1'}})`example_command`; +``` + +### Escaping + +```sh +# Bash +echo 'one two' +``` + +```js +// zx +await $`echo ${'one two'}`; +``` + +```js +// Execa +await $`echo ${'one two'}`; +``` + +### Escaping multiple arguments + +```sh +# Bash +echo 'one two' '$' +``` + +```js +// zx +await $`echo ${['one two', '$']}`; +``` + +```js +// Execa +await $`echo ${['one two', '$']}`; +``` + +### Current filename + +```sh +# Bash +echo "$(basename "$0")" +``` + +```js +// zx +await $`echo ${__filename}`; +``` + +```js +// Execa +import {fileURLToPath} from "node:url"; +import {basename} from "node:path"; +const __filename = basename(fileURLToPath(import.meta.url)); + +await $`echo ${__filename}`; +``` + +### Verbose mode + +```sh +# Bash +set -v +echo example +``` + +```js +// zx >=8 +await $`echo example`.verbose(); + +// or: +$.verbose = true; +``` + +```js +// Execa +const $$ = $({verbose: true}); +await $$`echo example`; +``` + +Or: + +``` +NODE_DEBUG=execa node file.js +``` + +### Current directory + +```sh +# Bash +cd project +``` + +```js +// zx +cd('project'); + +// or: +$.cwd = 'project'; +``` + +```js +// Execa +const $$ = $({cwd: 'project'}); +``` + +### Multiple current directories + +```sh +# Bash +pushd project +pwd +popd +pwd +``` + +```js +// zx +within(async () => { + cd('project'); + await $`pwd`; +}); + +await $`pwd`; +``` + +```js +// Execa +await $({cwd: 'project'})`pwd`; +await $`pwd`; +``` + +### Exit codes + +```sh +# Bash +false +echo $? +``` + +```js +// zx +const {exitCode} = await $`false`.nothrow(); +echo`${exitCode}`; +``` + +```js +// Execa +const {exitCode} = await $({reject: false})`false`; +console.log(exitCode); +``` + +### Timeouts + +```sh +# Bash +timeout 5 echo example +``` + +```js +// zx +await $`echo example`.timeout('5s'); +``` + +```js +// Execa +await $({timeout: 5000})`echo example`; +``` + +### PID + +```sh +# Bash +echo example & +echo $! +``` + +```js +// zx does not return `childProcess.pid` +``` + +```js +// Execa +const {pid} = $`echo example`; +``` + +### Errors + +```sh +# Bash communicates errors only through the exit code and stderr +timeout 1 sleep 2 +echo $? +``` + +```js +// 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. (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. (node:internal/child_process:456:11) +// at Socket.emit (node:events:512:28) +// at Pipe. (node:net:316:12) +// at Pipe.callbackTrampoline (node:internal/async_hooks:130:17) { +// _code: null, +// _signal: 'SIGTERM', +// _stdout: '', +// _stderr: '', +// _combined: '' +// } +``` + +```js +// 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 + +```sh +# Bash +options="timeout 5" +$options echo one +$options echo two +$options echo three +``` + +```js +// zx +const timeout = '5s'; +await $`echo one`.timeout(timeout); +await $`echo two`.timeout(timeout); +await $`echo three`.timeout(timeout); +``` + +```js +// Execa +const $$ = $({timeout: 5000}); +await $$`echo one`; +await $$`echo two`; +await $$`echo three`; +``` + +### Background processes + +```sh +# Bash +echo one & +``` + +```js +// zx does not allow setting the `detached` option +``` + +```js +// Execa +await $({detached: true})`echo one`; +``` + +### Printing to stdout + +```sh +# Bash +echo example +``` + +```js +// zx +echo`example`; +``` + +```js +// Execa +console.log('example'); +``` + +### Piping stdout to another command + +```sh +# Bash +echo example | cat +``` + +```js +// zx +await $`echo example | cat`; +``` + +```js +// Execa +await $`echo example`.pipeStdout($`cat`); +``` + +### Piping stdout and stderr to another command + +```sh +# Bash +echo example |& cat +``` + +```js +// zx +const echo = $`echo example`; +const cat = $`cat`; +echo.pipe(cat) +echo.stderr.pipe(cat.stdin); +await Promise.all([echo, cat]); +``` + +```js +// Execa +await $`echo example`.pipeAll($`cat`); +``` + +### Piping stdout to a file + +```sh +# Bash +echo example > file.txt +``` + +```js +// zx +await $`echo example`.pipe(fs.createWriteStream('file.txt')); +``` + +```js +// Execa +await $`echo example`.pipeStdout('file.txt'); +``` + +### Silent stderr + +```sh +# Bash +echo example 2> /dev/null +``` + +```js +// zx +await $`echo example`.stdio('inherit', 'pipe', 'ignore'); +``` + +```js +// Execa does not forward stdout/stderr by default +await $`echo example`; +``` diff --git a/readme.md b/readme.md index 89625df7e..28a6e8620 100644 --- a/readme.md +++ b/readme.md @@ -13,6 +13,7 @@ This package improves [`child_process`](https://nodejs.org/api/child_process.html) methods with: - Promise interface. +- [Scripts interface](#scripts-interface). - [Strips the final newline](#stripfinalnewline) from the output so you don't have to do `stdout.trim()`. - Supports [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) binaries cross-platform. - [Improved Windows support.](https://github.com/IndigoUnited/node-cross-spawn#why) @@ -39,7 +40,9 @@ console.log(stdout); //=> 'unicorns' ``` -### Using the tagged templates API +### Scripts interface + +For more information, please see [this page](docs/scripts.md). #### Basic @@ -252,6 +255,8 @@ The [`shell` option](#shell) must be used if the `command` uses shell-specific f Returns a `Promise` that resolves or rejects with a [`childProcessResult`](#childProcessResult). +For more information, please see [this page](docs/scripts.md). + ### $.sync\`command\` Same as [$\`command\`](#command) but synchronous like [`execaSync()`](#execasyncfile-arguments-options).