# 馃悮 zx ```js #!/usr/bin/env zx await $`cat package.json | grep name` let branch = await $`git branch --show-current` await $`dep deploy --branch=${branch}` await Promise.all([ $`sleep 1; echo 1`, $`sleep 2; echo 2`, $`sleep 3; echo 3`, ]) let name = 'foo bar' await $`mkdir /tmp/${name}` ``` Bash is great, but when it comes to writing more complex scripts, many people prefer a more convenient programming language. JavaScript is a perfect choice, but the Node.js standard library requires additional hassle before using. The `zx` package provides useful wrappers around `child_process`, escapes arguments and gives sensible defaults. ## Install ```bash npm i -g zx ``` **Requirement**: Node version >= 16.0.0 ## Goods [$](#command-) 路 [cd()](#cd) 路 [fetch()](#fetch) 路 [question()](#question) 路 [sleep()](#sleep) 路 [echo()](#echo) 路 [stdin()](#stdin) 路 [within()](#within) 路 [chalk](#chalk-package) 路 [fs](#fs-package) 路 [os](#os-package) 路 [path](#path-package) 路 [glob](#globby-package) 路 [yaml](#yaml-package) 路 [minimist](#minimist-package) 路 [which](#which-package) 路 [__filename](#__filename--__dirname) 路 [__dirname](#__filename--__dirname) 路 [require()](#require) ## Documentation Write your scripts in a file with an `.mjs` extension in order to use `await` at the top level. If you prefer the `.js` extension, wrap your scripts in something like `void async function () {...}()`. Add the following shebang to the beginning of your `zx` scripts: ```bash #!/usr/bin/env zx ``` Now you will be able to run your script like so: ```bash chmod +x ./script.mjs ./script.mjs ``` Or via the `zx` executable: ```bash zx ./script.mjs ``` All functions (`$`, `cd`, `fetch`, etc) are available straight away without any imports. Or import globals explicitly (for better autocomplete in VS Code). ```js import 'zx/globals' ``` ### ``$`command` `` Executes a given command using the `spawn` func and returns [`ProcessPromise`](#processpromise). Everything passed through `${...}` will be automatically escaped and quoted. ```js let name = 'foo & bar' await $`mkdir ${name}` ``` **There is no need to add extra quotes.** Read more about it in [quotes](docs/quotes.md). You can pass an array of arguments if needed: ```js let flags = [ '--oneline', '--decorate', '--color', ] await $`git log ${flags}` ``` If the executed program returns a non-zero exit code, [`ProcessOutput`](#processoutput) will be thrown. ```js try { await $`exit 1` } catch (p) { console.log(`Exit code: ${p.exitCode}`) console.log(`Error: ${p.stderr}`) } ``` ### `ProcessPromise` ```ts class ProcessPromise extends Promise { stdin: Writable stdout: Readable stderr: Readable exitCode: Promise pipe(dest): ProcessPromise kill(): Promise nothrow(): this quiet(): this } ``` Read more about the [ProcessPromise](docs/process-promise.md). ### `ProcessOutput` ```ts class ProcessOutput { readonly stdout: string readonly stderr: string readonly signal: string readonly exitCode: number toString(): string // Combined stdout & stderr. } ``` The output of the process is captured as-is. Usually, programs print a new line `\n` at the end. If `ProcessOutput` is used as an argument to some other `$` process, **zx** will use stdout and trim the new line. ```js let date = await $`date` await $`echo Current date is ${date}.` ``` ## Functions ### `cd()` Changes the current working directory. ```js cd('/tmp') await $`pwd` // => /tmp ``` ### `fetch()` A wrapper around the [node-fetch](https://www.npmjs.com/package/node-fetch) package. ```js let resp = await fetch('https://medv.io') ``` ### `question()` A wrapper around the [readline](https://nodejs.org/api/readline.html) package. ```js let bear = await question('What kind of bear is best? ') ``` ### `sleep()` A wrapper around the `setTimeout` function. ```js await sleep(1000) ``` ### `echo()` A `console.log()` alternative which can take [ProcessOutput](#processoutput). ```js let branch = await $`git branch --show-current` echo`Current branch is ${branch}.` // or echo('Current branch is', branch) ``` ### `stdin()` Returns the stdin as a string. ```js let content = JSON.parse(await stdin()) ``` ### `within()` Creates a new async context. ```js await $`pwd` // => /home/path within(async () => { cd('/tmp') setTimeout(async () => { await $`pwd` // => /tmp }, 1000) }) await $`pwd` // => /home/path ``` ```js let version = await within(async () => { $.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; ' await $`nvm use 16` return $`node -v` }) ``` ## Packages The following packages are available without importing inside scripts. ### `chalk` package The [chalk](https://www.npmjs.com/package/chalk) package. ```js console.log(chalk.blue('Hello world!')) ``` ### `fs` package The [fs-extra](https://www.npmjs.com/package/fs-extra) package. ```js let {version} = await fs.readJson('./package.json') ``` ### `os` package The [os](https://nodejs.org/api/os.html) package. ```js await $`cd ${os.homedir()} && mkdir example` ``` ### `path` package The [path](https://nodejs.org/api/path.html) package. ```js await $`mkdir ${path.join(basedir, 'output')}` ``` ### `globby` package The [globby](https://github.com/sindresorhus/globby) package. ```js let packages = await glob(['package.json', 'packages/*/package.json']) ``` ### `yaml` package The [yaml](https://www.npmjs.com/package/yaml) package. ```js console.log(YAML.parse('foo: bar').foo) ``` ### `minimist` package The [minimist](https://www.npmjs.com/package/minimist) package available as global const `argv`. ```js if( argv.someFlag ){ echo('yes') } ``` ### `which` package The [which](https://github.com/npm/node-which) package. ```js let node = await which('node') ``` ## Configuration ### `$.shell` Specifies what shell is used. Default is `which bash`. ```js $.shell = '/usr/bin/bash' ``` Or use a CLI argument: `--shell=/bin/bash` ### `$.spawn` Specifies a `spawn` api. Defaults to `require('child_process').spawn`. ### `$.prefix` Specifies the command that will be prefixed to all commands run. Default is `set -euo pipefail;`. Or use a CLI argument: `--prefix='set -e;'` ### `$.quote` Specifies a function for escaping special characters during command substitution. ### `$.verbose` Specifies verbosity. Default is `true`. In verbose mode, `zx` prints all executed commands alongside with their outputs. Or use the CLI argument `--quiet` to set `$.verbose = false`. ### `$.env` Specifies an environment variables map. Defaults to `process.env`. ### `$.cwd` Specifies a current working directory of all processes created with the `$`. The [cd()](#cd) func changes only `process.cwd()` and if no `$.cwd` specified, all `$` processes use `process.cwd()` by default (same as `spawn` behavior). ### `$.log` Specifies a [logging function](src/core.ts). ```ts import { LogEntry, log } from 'zx/core' $.log = (entry: LogEntry) => { switch (entry.kind) { case 'cmd': // for example, apply custom data masker for cmd printing process.stderr.write(masker(entry.cmd)) break default: log(entry) } } ``` ## Polyfills ### `__filename` & `__dirname` In [ESM](https://nodejs.org/api/esm.html) modules, Node.js does not provide `__filename` and `__dirname` globals. As such globals are really handy in scripts, `zx` provides these for use in `.mjs` files (when using the `zx` executable). ### `require()` In [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename) modules, the `require()` function is not defined. The `zx` provides `require()` function, so it can be used with imports in `.mjs` files (when using `zx` executable). ```js let {version} = require('./package.json') ``` ## Experimental The zx provides a few experimental functions. Please leave feedback about those features in [the discussion](https://github.com/google/zx/discussions/299). To enable new features via CLI pass `--experimental` flag. ### `retry()` Retries a callback for a few times. Will return after the first successful attempt, or will throw after specifies attempts count. ```js import { retry, expBackoff } from 'zx/experimental' let p = await retry(10, () => $`curl https://medv.io`) // With a specified delay between attempts. let p = await retry(20, '1s', () => $`curl https://medv.io`) // With an exponential backoff. let p = await retry(30, expBackoff(), () => $`curl https://medv.io`) ``` ### `spinner()` Starts a simple CLI spinner. ```js import { spinner } from 'zx/experimental' await spinner(() => $`long-running command`) // With a message. await spinner('working...', () => $`sleep 99`) ``` ## FAQ ### Passing env variables ```js process.env.FOO = 'bar' await $`echo $FOO` ``` ### Passing array of values When passing an array of values as an argument to `$`, items of the array will be escaped individually and concatenated via space. Example: ```js let files = [...] await $`tar cz ${files}` ``` ### Importing into other scripts It is possible to make use of `$` and other functions via explicit imports: ```js #!/usr/bin/env node import {$} from 'zx' await $`date` ``` ### Scripts without extensions If script does not have a file extension (like `.git/hooks/pre-commit`), zx assumes that it is an [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename) module. ### Markdown scripts The `zx` can execute [scripts written as markdown](docs/markdown.md): ```bash zx docs/markdown.md ``` ### TypeScript scripts ```ts import {$} from 'zx' // Or import 'zx/globals' void async function () { await $`ls -la` }() ``` Set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_type) in **package.json** and [`"module": "ESNext"`](https://www.typescriptlang.org/tsconfig/#module) in **tsconfig.json**. ### Executing remote scripts If the argument to the `zx` executable starts with `https://`, the file will be downloaded and executed. ```bash zx https://medv.io/game-of-life.js ``` ### Executing scripts from stdin The `zx` supports executing scripts from stdin. ```js zx <<'EOF' await $`pwd` EOF ``` ### Executing scripts via --eval Evaluate the following argument as a script. ```bash cat package.json | zx --eval 'let v = JSON.parse(await stdin()).version; echo(v)' ``` ### Installing dependencies via --install ```js // script.mjs: import sh from 'tinysh' sh.say('Hello, world!') ``` Add `--install` flag to the `zx` command to install missing dependencies automatically. ```bash zx --install script.mjs ``` You can also specify needed version by adding comment with `@` after the import. ```js import sh from 'tinysh' // @^1 ``` ### Attaching a profile By default `child_process` does not include aliases and bash functions. But you are still able to do it by hand. Just attach necessary directives to the `$.prefix`. ```js $.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; ' await $`nvm -v` ``` ### Using GitHub Actions The default GitHub Action runner comes with `npx` installed. ```yaml jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build env: FORCE_COLOR: 3 run: | npx zx <<'EOF' await $`...` EOF ``` ### Canary / Beta / RC builds Impatient early adopters can try the experimental zx versions. But keep in mind: these builds are 鈿狅笍锔徛燺_unstable__ in every sense. ```bash npm i zx@dev npx zx@dev --install --quiet <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)' ``` ## License [Apache-2.0](LICENSE) Disclaimer: _This is not an officially supported Google product._