# zx User Guide

`zx` is a tool for writing better shell scripts in JavaScript and TypeScript. Bash is great for quick automation, but as scripts grow, you often want real loops, error handling, parallelism, and data structures. `zx` gives you exactly that: it wraps Node.js `child_process` in an ergonomic `` $`command` `` template literal, automatically escapes arguments, and ships sensible cross-platform defaults — so you keep the power of the shell while writing in a language you already know.

```js
#!/usr/bin/env zx

await $`cat package.json | grep name`

const 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`,
])

const name = 'foo bar'
await $`mkdir /tmp/${name}`
```

> [!NOTE]
> `zx` is an open-source project published by Google under the Apache-2.0 license. It is **not** an officially supported Google product. The canonical online documentation lives at [google.github.io/zx](https://google.github.io/zx/).

---

## 1. Quick Start

The fastest way to try `zx` is to run a script with `npx` — no install required.

=== "npx (no install)"

    ```bash
    npx zx ./script.mjs
    ```

=== "Global install"

    ```bash
    npm install -g zx
    zx ./script.mjs
    ```

=== "Project dependency"

    ```bash
    npm install zx
    ```

Create a file named `hello.mjs`:

```js
#!/usr/bin/env zx

await $`echo "Hello from zx!"`

const files = await $`ls`
echo(`This directory has files:\n${files}`)
```

Run it:

```bash
zx hello.mjs
```

```text
Hello from zx!
This directory has files:
hello.mjs
package.json
```

> [!TIP]
> Use the `.mjs` extension so you can use top-level `await` without any boilerplate. All `zx` helpers (`$`, `cd`, `echo`, `fetch`, …) are injected as globals automatically when you run a script through the `zx` executable — you do not need any `import` statements.

---

## 2. Installation

### 2.1 Requirements

Before installing, make sure your environment meets these requirements:

| Requirement | Details |
|-------------|---------|
| Operating system | Linux, macOS, or Windows |
| JavaScript runtime | Node.js `>= 12.17.0`, Bun `>= 1.0.0`, Deno `1.x`/`2.x`, or GraalVM Node.js |
| Shell | Some form of **bash** (default) or **PowerShell** |
| Modules | Works as both CommonJS (CJS) and ECMAScript Modules (ESM), in JavaScript or TypeScript |

> [!IMPORTANT]
> `zx` relies on **bash** by default. On Windows, install [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install) or [Git Bash](https://git-scm.com/downloads), or switch to PowerShell with `usePowerShell()` / `usePwsh()` (see [§8 Choosing a shell](#8-choosing-a-shell)).

### 2.2 Installation methods

`zx` can be installed through every major package manager and several distribution channels.

=== "npm"

    ```bash
    npm install zx        # add -g to install globally
    ```

=== "npx"

    ```bash
    npx zx script.js          # run without installing
    npx zx@8.6.0 script.js    # pin to a specific version
    ```

=== "yarn"

    ```bash
    yarn add zx
    ```

=== "pnpm"

    ```bash
    pnpm add zx
    ```

=== "bun"

    ```bash
    bun install zx
    ```

=== "deno"

    ```bash
    deno install -A npm:zx
    # zx needs: --allow-read --allow-sys --allow-env --allow-run
    ```

=== "jsr"

    ```bash
    npx jsr add @webpod/zx
    deno add jsr:@webpod/zx
    ```

=== "Docker"

    ```bash
    docker pull ghcr.io/google/zx:8.5.0
    docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`"
    docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js
    ```

=== "Homebrew"

    ```bash
    brew install zx
    ```

### 2.3 Installing directly from GitHub

```bash
# Install via git
npm i google/zx
npm i git@github.com:google/zx.git

# Fetch from the GitHub package registry
npm i --registry=https://npm.pkg.github.com @google/zx
```

> [!CAUTION]
> Always verify the source you install from. Official distribution points are [npmjs](https://www.npmjs.com/package/zx), the [GitHub repo](https://github.com/google/zx), the [GitHub container registry](https://github.com/google/zx/pkgs/container/zx), [JSR](https://jsr.io/@webpod/zx), and [Homebrew](https://github.com/Homebrew/homebrew-core/blob/master/Formula/z/zx.rb).

### 2.4 Distribution channels

`zx` ships in several channels, each tuned for a different use case:

| Channel | Description | Install |
|---------|-------------|---------|
| `latest` | Stable, full-featured releases (the default). | `npm i zx` |
| `lite` | A minimal core build (~7× smaller, no CLI). Ideal for custom toolkits. | `npm i zx@lite` |
| `dev` | Experimental snapshots and release candidates — may be unstable. | `npm i zx@dev` |
| `legacy` | Older major versions; bugfixes only, no new features. | `npm i zx@<version>` |

> [!NOTE]
> The `lite` channel omits the CLI, `argv`, `echo`, `fetch`, `glob`, and several other extras. See [§13 Reference](#13-reference) for the full feature matrix. Pick `lite` when you embed `zx` inside another library rather than running scripts directly.

---

## 3. Writing Your First Script

### 3.1 Script files and the shebang

Write your script in a file with the `.mjs` extension so top-level `await` works out of the box. Add a shebang at the top to make the file directly executable:

```js
#!/usr/bin/env zx

await $`whoami`
await $`date`
```

Make it executable and run it directly:

```bash
chmod +x ./script.mjs
./script.mjs
```

Or run it through the CLI:

```bash
zx ./script.mjs
```

> [!TIP]
> If you prefer the `.js` extension in a project that is not ESM, wrap your code in an async IIFE: `void async function () { /* ... */ }()`. TypeScript (`.ts`, `.mts`, `.cts`) is also supported — see [§11 TypeScript](#11-typescript).

### 3.2 Globals vs. explicit imports

When you run a script through the `zx` executable, every helper is available globally. If you run your script with plain `node` (or want editor autocompletion), import the globals explicitly:

```js
import 'zx/globals'

await $`echo "globals are now available"`
```

Or import only what you need:

```js
import { $, cd, fetch, echo } from 'zx'

await $`echo "selective import"`
```

---

## 4. Running Commands with `$`

The `` $`...` `` template literal is the heart of `zx`. It spawns a command and returns a [`ProcessPromise`](#7-the-processpromise-object) that resolves to a [`ProcessOutput`](#6-reading-command-output).

```js
const list = await $`ls -la`   // async → ProcessPromise → ProcessOutput
const dir = $.sync`pwd`        // sync → ProcessOutput immediately
```

### 4.1 Interpolation is automatically escaped

Anything you place inside `${...}` is automatically escaped and quoted. **You never add your own quotes.**

```js
const name = 'foo & bar'
await $`mkdir ${name}`   // safely creates a directory literally named "foo & bar"
```

> [!WARNING]
> Because `zx` escapes interpolated values for you, wrapping them in extra quotes can actually *re-enable* injection. Do **not** write `` $`echo "$'${userInput}'"` ``. Let `zx` handle the quoting. See [§5 Working with Arguments and Quoting](#5-working-with-arguments-and-quoting).

### 4.2 Passing arrays of arguments

Pass an array and each element is escaped individually, then joined with spaces:

```js
const flags = ['--oneline', '--decorate', '--color']
await $`git log ${flags}`
```

### 4.3 Awaiting interpolated values

In async mode, `zx` awaits any `thenable` inside the template before running the command:

```js
const a1 = $`echo foo`
const a2 = new Promise((resolve) => setTimeout(resolve, 20, ['bar', 'baz']))

await $`echo ${a1} ${a2}`   // prints: foo bar baz
```

### 4.4 Handling failures

If the command exits with a non-zero code, the awaited promise **throws** a `ProcessOutput`:

```js
try {
  await $`exit 1`
} catch (p) {
  console.log(`Exit code: ${p.exitCode}`)
  console.log(`Error: ${p.stderr}`)
}
```

To opt out of throwing, use [`.nothrow()`](#74-suppressing-errors-with-nothrow).

### 4.5 Synchronous execution

For quick, blocking calls, use `$.sync`:

```js
const branch = $.sync`git branch --show-current`
echo(`On branch ${branch}`)
```

> [!NOTE]
> Sync mode cannot resolve async-interpolated values inside the template. If you interpolate a `ProcessPromise` or `Promise`, use async `$` instead.

---

## 5. Working with Arguments and Quoting

`zx` prefers bash C-style quoting (`$'...'`) and escapes everything inside `${...}`. This protects you from injection but changes a few habits you may carry over from bash.

### 5.1 The right way to build arguments

The following examples all produce the same, correct result:

```js
await $`mkdir ${'path/to-dir/' + name}`
await $`mkdir path/to-dir/${name}`
```

### 5.2 Glob patterns

Because everything inside `${...}` is escaped, glob syntax will not expand. Use the built-in [`glob()`](#94-finding-files-with-glob) helper instead:

```js
// ❌ Incorrect — the pattern is escaped literally
const files = './**/*.md'
await $`ls ${files}`

// ✅ Correct
const files = await glob('./**/*.md')
await $`ls ${files}`
```

### 5.3 The home directory `~`

`zx` will not expand `~` inside `${...}`. Use `os.homedir()`:

```js
// ❌ Incorrect
const dir = `~/Downloads`
await $`ls ${dir}`

// ✅ Correct
await $`ls ${os.homedir()}/Downloads`

// ✅ Also correct — ~ is outside ${...}
await $`ls ~/Downloads`
```

### 5.4 Assembling commands dynamically

You cannot build a command by concatenating a string and interpolating the whole thing — `zx` will escape the entire string and break the command. Build an **array of arguments** instead:

```js
// ❌ Incorrect
let cmd = 'rm'
if (force) cmd += ' -f'
if (recursive) cmd += ' -r'
cmd += ' ' + file
await $`${cmd}`

// ✅ Correct
const args = []
if (force) args.push('-f')
if (recursive) args.push('-r')
args.push(file)
await $`rm ${args}`
```

### 5.5 PowerShell quoting

When using PowerShell, switch to the PowerShell quoting function:

```js
import { quotePowerShell } from 'zx'

$.quote = quotePowerShell
```

> [!TIP]
> The `useBash()`, `usePowerShell()`, and `usePwsh()` helpers set both the shell **and** the matching quote function for you. Prefer those over setting `$.quote` by hand. See [§8 Choosing a shell](#8-choosing-a-shell).

---

## 6. Reading Command Output

Awaiting a command yields a `ProcessOutput` object. It behaves like a string (its `toString()` is the combined output) and exposes structured accessors.

```js
const o = await $`echo "hello world"`

o.stdout    // 'hello world\n'
o.stderr    // ''
o.exitCode  // 0
o.signal    // null
String(o)   // combined stdout + stderr
```

### 6.1 Output formatters

`ProcessOutput` (and `ProcessPromise` directly) provide convenient formatters:

| Method | Returns | Example result |
|--------|---------|----------------|
| `text(encoding?)` | The combined output as a string | `'foo\nbar\n'` |
| `json<T>()` | The output parsed as JSON | `{ foo: 'bar' }` |
| `lines(delimiter?)` | The output split into an array of lines | `['foo', 'bar']` |
| `buffer()` | The output as a `Buffer` | `Buffer.from('foo\n')` |
| `blob(type?)` | The output as a `Blob` | `Blob` (default `text/plain`) |

```js
const p = $`echo 'foo\nbar'`

await p.text()        // 'foo\nbar\n'
await p.text('hex')   // hex-encoded output
await p.buffer()      // Buffer
await p.lines()       // ['foo', 'bar']

// Custom line delimiter (e.g. null-separated):
await $`find ./ -type f -print0`.lines('\0')

// Parse JSON output in place:
await $`echo '{"foo": "bar"}'`.json()   // { foo: 'bar' }
```

> [!NOTE]
> When you pass a `ProcessOutput` as an argument into another `$` command, `zx` uses its `stdout` and **trims the trailing newline** automatically:
>
> ```js
> const date = await $`date`
> await $`echo Current date is ${date}.`
> ```

### 6.2 Checking only the exit code

```js
if (await $`[[ -d path ]]`.exitCode == 0) {
  echo('directory exists')
}
```

---

## 7. The `ProcessPromise` Object

`` $`cmd` `` returns a `ProcessPromise` — a real `Promise` subclass with extra methods for controlling the running process. By default a process starts immediately, but you can delay it (see [`halt`](#76-deferring-execution-with-halt)).

```js
const p = $`command`   // ProcessPromise (already running)
const o = await p      // ProcessOutput
```

### 7.1 Streaming output

`stdout` and `stderr` are readable streams; `stdin` is writable. You can also iterate the process output line by line:

```js
// Iterate stdout chunks
const p = $`npm init`
for await (const chunk of p.stdout) {
  echo(chunk)
}

// Iterate line by line (uses $.delimiter)
for await (const line of $`echo "Line1\nLine2\nLine3"`) {
  console.log(line)
}
```

Writing to stdin:

```js
const p = $`while read; do echo $REPLY; done`
p.stdin.write('Hello, World!\n')
p.stdin.end()
```

> [!IMPORTANT]
> Always end the stdin stream (`p.stdin.end()`) when you are done writing, or the process will wait forever.

### 7.2 Process metadata

| Getter | Description |
|--------|-------------|
| `pid` | Process ID |
| `cwd` | Working directory of the process |
| `cmd` | The command as built (e.g. `"sleep 1"`) |
| `fullCmd` | The command including prefix and postfix (e.g. `"set -euo pipefail;sleep 1"`) |
| `stage` | Current lifecycle stage: `initial`, `halted`, `running`, `fulfilled`, or `rejected` |
| `exitCode` | A promise that resolves to the exit code |

### 7.3 Piping with `pipe()`

`pipe()` connects the output of one process to another process, a writable stream, or a file — like `|` in bash, but more powerful.

```js
// Chain processes
const greeting = await $`printf "hello"`
  .pipe($`awk '{printf $1", world!"}'`)
  .pipe($`tr '[a-z]' '[A-Z]'`)

// Pipe to a file (string path or stream)
await $`echo "Hello, stdout!"`.pipe('/tmp/output.txt')
await $`echo "Hello, stdout!"`.pipe(fs.createWriteStream('/tmp/output.txt'))

// Show real-time output
await $`echo 1; sleep 1; echo 2`.pipe(process.stdout)

// Template-literal shorthand
await $`printf "hello"`
  .pipe`awk '{printf $1", world!"}'`
  .pipe`tr '[a-z]' '[A-Z]'`
```

Pipe a specific stream (`stdout` is the default; `stderr` and `stdall` are also available):

```js
const p = $`echo foo >&2; echo bar`
const errOut = (await p.pipe.stderr`cat`).toString()   // 'foo\n'
const stdOut = (await p.pipe.stdout`cat`).toString()   // 'bar\n'
```

> [!TIP]
> `pipe()` buffers and replays output, so you can attach a consumer at **any** point — even after the source process has finished:
>
> ```js
> const p = $`echo 1; sleep 1; echo 2`
> await p
> await p.pipe`cat`   // still works; all output is replayed in order
> ```

Use `unpipe()` to detach a destination from the pipeline:

```js
p1.pipe(p3)
setTimeout(() => p1.unpipe(p3), 105)
```

### 7.4 Suppressing errors with `nothrow()`

`nothrow()` makes a command resolve (instead of throwing) on a non-zero exit code:

```js
await $`grep something from-file`.nothrow()

// Inside a pipe
await $`find ./examples -type f -print0`
  .pipe($`xargs -0 grep something`.nothrow())
  .pipe($`wc -l`)
```

### 7.5 Controlling verbosity per command

```js
await $`grep something from-file`.quiet()    // suppress this command's output
await $`grep something from-file`.verbose()  // print this command and its output

// When a global flag is on, turn it off for one command:
$.quiet = true
await $`echo foo`.quiet(false)
```

### 7.6 Deferring execution with `halt`

Create a process but do not start it until you call `.run()`:

```js
const p = $({ halt: true })`command`
// ... set things up ...
const o = await p.run()
```

This is useful for configuring `stdio` before launch or for merging multiple sources into one pipe.

### 7.7 Stopping a process

```js
// kill() — sends SIGTERM by default (or a signal you choose)
const p = $`sleep 999`
setTimeout(() => p.kill('SIGINT'), 100)
await p

// abort() — terminate via an AbortController signal
const ac = new AbortController()
const p2 = $({ signal: ac.signal })`sleep 999`
setTimeout(() => ac.abort('reason'), 100)
await p2
```

> [!WARNING]
> Calling `kill()` or `abort()` on a process that has already finished throws an error (`Too late to kill the process.` / `Too late to abort the process.`). Guard with the process `stage` if you might be racing completion.

### 7.8 Setting timeouts

```js
await $`sleep 999`.timeout('5s')              // SIGTERM after 5 seconds
await $`sleep 999`.timeout('5s', 'SIGKILL')   // custom signal
```

### 7.9 Configuring `stdio`

```js
const h$ = $({ halt: true })
const p1 = h$`read`.stdio('inherit', 'pipe', null).run()
const p2 = h$`read`.stdio('pipe').run()   // expands to ['pipe', 'pipe', 'pipe']

// Or via the preset, which is set before the process starts:
await $({ stdio: ['pipe', 'pipe', 'pipe'] })`read`
```

---

## 8. Choosing a Shell

`zx` defaults to bash. Switch shells with built-in helpers, which also set the correct quoting function:

```js
import { useBash, usePowerShell, usePwsh } from 'zx'

usePowerShell()  // Windows PowerShell (powershell.exe)
usePwsh()        // PowerShell v7+ (pwsh binary)
useBash()        // bash (the default)
```

You can also set the shell directly via JavaScript, the CLI, or an environment variable:

```js
$.shell = '/bin/zsh'
```

```bash
zx --shell /bin/zsh script.js
ZX_SHELL=/bin/zsh zx script.js
```

> [!NOTE]
> Since v8, `zx` does **not** auto-detect PowerShell on Windows. If you need it, call `usePowerShell()` or `usePwsh()` explicitly. The `ssh` API was also removed in v8 — install the [webpod](https://github.com/webpod/webpod) package for remote execution.

---

## 9. Built-in Helpers

When running through the `zx` executable (or after `import 'zx/globals'`), these helpers are available globally.

### 9.1 `cd()` — change directory

```js
cd('/tmp')
await $`pwd`   // => /tmp

// Accepts a ProcessOutput and trims trailing newlines:
cd(await $`mktemp -d`)
```

> [!WARNING]
> `cd()` calls `process.chdir()` internally, so it changes the global working directory. To keep `process.cwd()` in sync across separate `$` calls, enable [`syncProcessCwd()`](#911-syncprocesscwd).

### 9.2 `fetch()` — HTTP requests

A wrapper around [node-fetch-native](https://www.npmjs.com/package/node-fetch-native):

```js
const r1 = await fetch('https://example.com')
const json = await r1.json()

const r2 = await fetch('https://example.com', {
  signal: AbortSignal.timeout(5000),
})
```

For large responses, `fetch()` results are pipe-friendly:

```js
const p1 = fetch('https://example.com').pipe($`cat`)
const p2 = fetch('https://example.com').pipe`cat`
```

### 9.3 `question()` — interactive prompts

A wrapper around the Node `readline` API:

```js
const bear = await question('What kind of bear is best? ')

const selected = await question('Select an option: ', {
  choices: ['A', 'B', 'C'],
})
```

### 9.4 Finding files with `glob()`

The [globby](https://github.com/sindresorhus/globby) package:

```js
const packages = await glob(['package.json', 'packages/*/package.json'])
const markdowns = glob.sync('*.md')   // synchronous shortcut
```

> [!NOTE]
> `glob` is also exported under the alias `globby`.

### 9.5 `which()` — locate a binary

```js
const node = await which('node')
const maybe = await which('node', { nothrow: true })   // null if not found
```

### 9.6 `sleep()` and `retry()`

```js
await sleep(1000)          // milliseconds, or a duration string like '1s'

// Retry up to N times, returning the first success
const p1 = await retry(10, () => $`curl https://medv.io`)

// With a fixed delay between attempts
const p2 = await retry(20, '1s', () => $`curl https://medv.io`)

// With exponential backoff
const p3 = await retry(30, expBackoff(), () => $`curl https://medv.io`)
```

`expBackoff(max?, delay?)` yields increasing delays (defaults: `max='60s'`, `delay='100ms'`).

### 9.7 `spinner()` — progress indicator

```js
await spinner(() => $`long-running command`)
await spinner('working...', () => $`sleep 99`)
```

> [!NOTE]
> The spinner is automatically disabled in CI environments (when the `CI` environment variable is set) and when `$.quiet` is enabled.

### 9.8 `echo()` — log to stdout

A `console.log()` alternative that understands `ProcessOutput`:

```js
const branch = await $`git branch --show-current`

echo`Current branch is ${branch}.`
echo('Current branch is', branch)
```

### 9.9 `stdin()` — read standard input

```js
const content = JSON.parse(await stdin())
```

### 9.10 `within()` — scoped configuration

`within()` runs a callback in a new async context where changes to `$` (and `cd`) are isolated:

```js
await $`pwd`   // => /home/path
$.foo = 'bar'

within(async () => {
  $.cwd = '/tmp'
  $.foo = 'baz'
  await $`pwd`   // => /tmp
})

await $`pwd`   // => /home/path (unchanged)
$.foo          // still 'bar'
```

A common use is loading a Node version manager just for one command:

```js
const version = await within(async () => {
  $.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; nvm use 16;'
  return $`node --version`
})
echo(version)   // => v16.x.x
```

### 9.11 `syncProcessCwd()`

Keeps `process.cwd()` in sync with the internal `$` working directory when you use `cd()`:

```js
import { syncProcessCwd } from 'zx'

syncProcessCwd()        // enable
syncProcessCwd(false)   // disable
```

> [!NOTE]
> This hook is **disabled by default** because of its performance overhead.

### 9.12 Temporary files and directories

```js
tempdir()         // /os/tmp/zx-<random>/
tempdir('foo')    // /os/tmp/zx-<random>/foo/

tempfile()                            // empty temp file
tempfile('f.txt')                     // named temp file
tempfile('f.txt', 'contents')         // with contents
tempfile('f.sh', 'echo "hi"', 0o744)  // executable
```

> [!NOTE]
> `tempdir`/`tempfile` are also exported under the aliases `tmpdir`/`tmpfile`.

### 9.13 Process listing and killing

```js
const all = await ps.lookup()
const node = await ps.lookup({ command: 'node' })
const tree = await ps.tree({ pid: 123, recursive: true })

await kill(123)
await kill(123, 'SIGKILL')
```

### 9.14 Bundled utility packages

`zx` re-exports a curated set of popular packages so you don't have to install them yourself:

| Global | Package | Typical use |
|--------|---------|-------------|
| `chalk` | [chalk](https://www.npmjs.com/package/chalk) | Terminal colors: `chalk.blue('hi')` |
| `fs` | [fs-extra](https://www.npmjs.com/package/fs-extra) | File system: `await fs.readJson('./package.json')` |
| `os` | Node `os` | `os.homedir()`, `os.tmpdir()` |
| `path` | Node `path` | `path.join(a, b)` |
| `YAML` | [yaml](https://www.npmjs.com/package/yaml) | `YAML.parse('foo: bar')` |
| `MAML` | [maml](https://www.npmjs.com/package/maml) | `MAML.parse(text)` (minimal, readable config format) |
| `dotenv` | [envapi](https://www.npmjs.com/package/envapi) | `.env` parsing/loading (see below) |
| `minimist` | [minimist](https://www.npmjs.com/package/minimist) | Manual argv parsing |

```js
console.log(chalk.blue('Hello world!'))
const { version } = await fs.readJson('./package.json')
console.log(YAML.parse('foo: bar').foo)   // 'bar'
```

Working with `.env` files via `dotenv`:

```js
// Parse a raw string
const data = dotenv.parse('FOO=BAR\nBAZ=QUX')   // { FOO: 'BAR', BAZ: 'QUX' }

// Load a file into an object and inject it into a single command
const env = dotenv.load('.env')
await $({ env })`echo $FOO`

// Or load into process.env globally
dotenv.config('.env')
process.env.FOO
```

### 9.15 Command-line arguments with `argv`

`argv` is a [minimist](https://www.npmjs.com/package/minimist)-parsed version of `process.argv`:

```js
if (argv.someFlag) {
  echo('yes')
}
```

For custom parsing, call `minimist` (or `parseArgv`) yourself:

```js
const myArgv = minimist(process.argv.slice(2), {
  boolean: ['force', 'help'],
  alias: { h: 'help' },
})
```

### 9.16 `versions`

Inspect the versions of `zx` and its bundled dependencies:

```js
import { versions } from 'zx'

versions.zx      // e.g. '8.9.0'
versions.chalk   // e.g. '5.6.2'
```

---

## 10. Common Workflows

### 10.1 Run commands in parallel

```js
import { spinner } from 'zx'

const tests = await glob('test/*.test.js')
await spinner('running tests', async () => {
  const res = await Promise.all(tests.map((file) => $`npx uvu . ${file}`))
  res.forEach((r) => console.log(r.toString()))
  console.log(chalk.bgGreen.black(' SUCCESS '))
})
```

### 10.2 Drive an interactive program

```js
const p = $`npm init`.stdio('pipe')

for await (const chunk of p.stdout) {
  if (chunk.includes('package name:')) p.stdin.write('test\n')
  if (chunk.includes('version:')) p.stdin.write('1.0.0\n')
  if (chunk.includes('Is this OK?')) p.stdin.write('yes\n')
}
```

### 10.3 Manage a background process

```js
const serve = $`npx serve`

for await (const chunk of serve.stdout) {
  if (chunk.includes('Accepting connections')) break
}

await $`curl http://localhost:3000`
serve.kill('SIGINT')
```

### 10.4 Fetch and display remote data

```js
const data = await spinner(`Fetching weather...`, async () => {
  const res = await fetch('https://wttr.in/London?format=j1', {
    signal: AbortSignal.timeout(5000),
  })
  if (!res.ok) throw `API error: ${res.status}`
  return res.json()
})

const current = data.current_condition[0]
echo(chalk.yellow(`🌡️  ${current.temp_C}°C, humidity ${current.humidity}%`))
```

### 10.5 Clone a list of repositories

```js
const username = await question('GitHub username? ')
const res = await fetch(`https://api.github.com/users/${username}/repos?per_page=100`)
const data = await res.json()

await $`mkdir -p backups`
cd('./backups')

for (const repo of data) {
  await $`git clone ${repo.git_url}`
}
```

### 10.6 Apply a configuration preset with `$({...})`

The `$` object holds the default configuration. Call it as a factory to create a customized, reusable runner. Presets are **chainable**:

```js
const $$ = $({ verbose: false, env: { NODE_ENV: 'production' } })
await $$`node -e 'console.log(process.env.NODE_ENV)'`

// Chaining merges options
const $1 = $({ nothrow: true })
const $2 = $1({ sync: true })   // both nothrow + sync apply
```

Pipe `stdin` into a command:

```js
const p = $({ input: 'foo' })`cat`
const p2 = $({ input: Buffer.from('baz') })`cat`
const p3 = $({ input: anotherProcessPromise })`cat`
```

---

## 11. TypeScript

`zx` is written in TypeScript and ships its own type definitions (compatible with TS 4.0+). Write scripts in `.ts`, `.mts`, or `.cts`.

```ts
// script.ts
import { $ } from 'zx'

const list = await $`ls -la`
```

To run TypeScript with Node.js, configure your project for ES modules (`"type": "module"` in `package.json`, `"module": "ESNext"` in `tsconfig.json`), then use one of:

=== "node (strip types)"

    ```bash
    # Node.js v22.6.0+
    node --experimental-strip-types script.ts
    ```

=== "tsx"

    ```bash
    npm install tsx
    tsx script.ts
    ```

=== "ts-node"

    ```bash
    npm install ts-node
    node --loader ts-node/esm script.ts
    ```

=== "bun"

    ```bash
    bun script.ts
    ```

=== "deno"

    ```bash
    deno run --allow-read --allow-sys --allow-env --allow-run script.ts
    ```

> [!IMPORTANT]
> The bundled type definitions require you to install `@types/node` and `@types/fs-extra` on your side:
>
> ```bash
> npm i -D @types/fs-extra @types/node
> ```

The `Options` interface is exported for typing presets:

```ts
import { type Options } from 'zx'

const opts: Options = { quiet: true, timeout: '5s' }
```

---

## 12. Markdown Scripts

`zx` can execute a Markdown file directly, running its fenced code blocks as a single script. Blocks tagged `js`, `javascript`, `ts`, `typescript`, `sh`, `shell`, or `bash` are executed; all other languages (and prose) are treated as comments.

````text
# Deploy script
First, check the current directory:

```js
const {stdout} = await $`ls -l`
console.log('contents:', stdout)
```

Then run a bash step:

```bash
ls -l
```
````

Run it:

```bash
zx script.md
```

> [!NOTE]
> Inside a Markdown script, `__filename` points to the Markdown file itself. You can use dynamic `import()` for additional packages.

---

## 13. Command-Line Interface

Run a script by passing it to the `zx` executable. `npx` and `node` invocations also work:

```bash
zx script.mjs
npx zx script.mjs
node -r zx/globals script.mjs
node --import zx/globals script.mjs
```

### 13.1 Input sources

`zx` can read a script from a file, a URL, standard input, or an inline string.

```bash
# Remote script (downloaded and executed)
zx https://raw.githubusercontent.com/google/zx/refs/heads/main/examples/hello.mjs

# From stdin
zx << 'EOF'
await $`pwd`
EOF

# Inline evaluation
cat package.json | zx --eval 'const v = JSON.parse(await stdin()).version; echo(v)'
```

> [!CAUTION]
> When running a **remote script**, make sure you trust the source and have reviewed the code. `zx` downloads and executes it directly.

### 13.2 File extensions

- A script with **no extension** (e.g. `.git/hooks/pre-commit`) is treated as ESM, unless `--ext` is given.
- For a non-JS-like extension, pass `--ext` to tell `zx` how to load it:

```bash
zx script.zx            # Error: Unknown file extension "\.zx"
zx --ext=mjs script.zx  # OK
```

### 13.3 CLI options

| Option | Alias | Description |
|--------|-------|-------------|
| `--quiet` | | Suppress all output |
| `--verbose` | | Enable verbose mode (print commands and output) |
| `--shell=<path>` | | Use a custom shell binary (default: `bash`) |
| `--prefix=<command>` | | Prepend a command to every command run |
| `--postfix=<command>` | | Append a command to every command run |
| `--prefer-local[=<dir>]` | `-l` | Prefer locally installed packages and binaries |
| `--cwd=<path>` | | Set the working directory |
| `--eval=<js>` | `-e` | Evaluate the argument as a script |
| `--ext=<.mjs>` | | Override the default script extension (`.mjs`) |
| `--install` | `-i` | Auto-install dependencies imported by the script |
| `--registry=<URL>` | | npm registry (default `https://registry.npmjs.org/`) |
| `--env=<path>` | `--env-file` | Path to an env file to load |
| `--repl` | | Start an interactive REPL |
| `--version` | `-v` | Print the `zx` version |
| `--help` | `-h` | Print help |

> [!NOTE]
> The `--experimental` flag still parses but is **deprecated** and no longer required.

### 13.4 Auto-installing dependencies

With `--install`, `zx` reads the imports in your script and installs missing packages before running:

```js
// script.mjs
import sh from 'tinysh'
sh.say('Hello, world!')
```

```bash
zx --install script.mjs
```

Pin a version with a trailing comment after the import:

```js
import sh from 'tinysh' // @^1
```

Use a custom registry:

```bash
zx --registry=https://registry.yarnpkg.com script.mjs
```

### 13.5 Prefix, postfix, cwd, and env

```bash
zx --prefix='echo foo;' --postfix='; echo bar' script.mjs
zx --cwd=/foo/bar script.mjs
zx --env=/path/to/some.env script.mjs
```

> [!TIP]
> When both `--cwd` and a relative `--env` are given, `--cwd` is the base path: `--cwd='/foo/bar' --env='../.env'` resolves to `/foo/.env`.

### 13.6 Configuring via environment variables

Every option above can be set with a `ZX_`-prefixed environment variable — handy in CI:

```bash
ZX_VERBOSE=true ZX_SHELL='/bin/bash' zx script.mjs
```

```yaml
steps:
  - name: Run script
    run: zx script.mjs
    env:
      ZX_VERBOSE: true
      ZX_SHELL: '/bin/bash'
```

### 13.7 ESM conveniences: `__filename`, `__dirname`, `require()`

In ESM modules these globals are normally undefined. When you run a `.mjs` file through the `zx` executable, `zx` provides them:

```js
const { version } = require('./package.json')
echo(`${__filename} in ${__dirname}`)
```

### 13.8 REPL mode

```bash
zx --repl
```

Starts an interactive Read-Eval-Print Loop with all `zx` globals available. History is stored in `~/.zx_repl_history` (override with the `ZX_REPL_HISTORY` environment variable).

---

## 14. Troubleshooting

### Error: `Output gets truncated`

**Cause:** Calling `process.exit()` can cut off buffered `console.log()` output, because terminal vs. file output buffering differs in Node.js ([nodejs/node#6379](https://github.com/nodejs/node/issues/6379)).

**Solution:** Set `process.exitCode = 1` and let the process exit on its own instead of calling `process.exit()`. As a workaround, write to a temp file and read it back:

```js
const tmp = await $`mktemp`
const { stdout } = await $`cmd > ${tmp}; cat ${tmp}`
```

**Prevention:** Avoid `process.exit()` in scripts that print output right before exiting.

### Error: Subprocess output has no colors

**Cause:** A subprocess detects it is not attached to a TTY and disables colors.

**Solution:** Force colors via an environment variable:

```js
process.env.FORCE_COLOR = '1'
await $`cmd`
```

**Prevention:** Set `FORCE_COLOR` early in scripts whose subprocess output you display.

### Error: `Too late to kill the process.` / `Too late to abort the process.`

**Cause:** You called `kill()` or `abort()` after the process had already settled.

**Solution:** Only kill/abort while the process is running. Check `p.stage === 'running'` before acting, or attach the kill inside a `setTimeout` shorter than the command's runtime.

**Prevention:** Design timeouts with `.timeout()` instead of manual kill races where possible.

### Error: A command "doesn't work" with a glob, `~`, or a built string

**Cause:** `zx` escapes everything inside `${...}`, so glob patterns, `~`, and pre-assembled command strings are taken literally.

**Solution:** Use `glob()` for patterns, `os.homedir()` for `~`, and pass an **array** of arguments rather than a concatenated string. See [§5 Working with Arguments and Quoting](#5-working-with-arguments-and-quoting).

**Prevention:** Treat each interpolation as a single, pre-escaped value — never as raw shell syntax.

### Error: PowerShell commands fail on Windows

**Cause:** Since v8, `zx` no longer auto-detects PowerShell; it defaults to bash and bash quoting.

**Solution:** Call `usePowerShell()` (Windows PowerShell) or `usePwsh()` (PowerShell v7+) at the top of your script.

**Prevention:** Set the shell explicitly when targeting Windows; consider WSL or Git Bash for bash compatibility.

### Issue: `child_process` does not see my aliases or shell functions

**Cause:** Spawned processes do not load your interactive shell profile.

**Solution:** Append the needed setup to `$.prefix`:

```js
$.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; '
await $`nvm -v`
```

---

## 15. Frequently Asked Questions

**Q: Do I need to import anything to use `$`?**
No — when you run a script through the `zx` executable, all helpers are global. To run with plain `node` (or get editor autocomplete), add `import 'zx/globals'`, or import individual helpers from `'zx'`.

**Q: How do I pass environment variables to a command?**
Set `process.env.FOO = 'bar'` before the call, or scope it with a preset: `$({ env: { FOO: 'bar' } })\`echo $FOO\``.

**Q: Why is there no output from my commands?**
By default `$.verbose` is `false`, so successful command output is captured but not echoed to the terminal — only `stderr` is shown. Set `$.verbose = true` (or use `.verbose()` per command) to see everything. The output is always available on the returned `ProcessOutput`.

**Q: How do I completely silence a script?**
Set `$.quiet = true` (or use `.quiet()` per command). This suppresses `stdout` *and* `stderr` logging.

**Q: How do I run a command without throwing on failure?**
Use `.nothrow()` or the `$({ nothrow: true })` preset, then inspect `o.exitCode` and `o.ok`.

**Q: Can I use `zx` in GitHub Actions?**
Yes. The default runner has `npx`, so you can run `npx zx <<'EOF' ... EOF` directly. Set `FORCE_COLOR: 3` in `env` if you want colored output.

**Q: What happened to the `ssh` API?**
It was removed in v8. Install the [webpod](https://github.com/webpod/webpod) package and use its `ssh()` helper instead.

**Q: Is `zx@lite` the same as `zx`?**
It shares the core (`$`, `ProcessPromise`, `ProcessOutput`, `cd`, `chalk`, …) but drops the CLI, `argv`, `echo`, `fetch`, `glob`, and several extras. Use it for embedding `zx` in your own tooling. See the feature matrix in the [versions docs](https://google.github.io/zx/versions).

**Q: How do I migrate a v7 script to v8?**
Set `$.verbose = true` to restore v7-style logging, enable `syncProcessCwd()` for the old cwd behavior, replace `ssh` with the webpod package, and call `usePowerShell()`/`usePwsh()` if you relied on Windows PowerShell. See the [migration guide](https://google.github.io/zx/migration-from-v7).

**Q: Which runtimes are supported?**
Node.js `>= 12.17.0`, Bun `>= 1.0.0`, Deno `1.x`/`2.x`, and GraalVM Node.js, on Linux, macOS, and Windows.

**Q: Can I try experimental builds?**
Yes: `npm i zx@dev`. These are beta in every sense — don't use them in production.

---

## 16. Reference

### 16.1 Global functions and objects

| Name | Kind | Summary |
|------|------|---------|
| `$` | function/object | Run a command; holds default config; usable as a preset factory |
| `$.sync` | function | Run a command synchronously |
| `cd()` | function | Change the working directory |
| `within()` | function | Run a callback in an isolated config context |
| `syncProcessCwd()` | function | Keep `process.cwd()` in sync with `cd()` |
| `fetch()` | function | HTTP requests (pipe-friendly) |
| `question()` | function | Interactive prompt |
| `stdin()` | function | Read standard input as a string |
| `echo()` | function | Print to stdout (ProcessOutput-aware) |
| `sleep()` | function | Delay for a duration |
| `retry()` | function | Retry a callback N times |
| `expBackoff()` | generator | Exponential backoff delays for `retry()` |
| `spinner()` | function | CLI progress spinner |
| `glob()` / `globby` | function | File globbing |
| `which()` | function | Locate a binary |
| `kill()` | function | Kill a process by PID |
| `ps` | object | Cross-platform process listing |
| `tempdir()` / `tmpdir()` | function | Create a temp directory |
| `tempfile()` / `tmpfile()` | function | Create a temp file |
| `argv` | object | Parsed CLI arguments (minimist) |
| `minimist` | function | Argument parser |
| `chalk` | object | Terminal colors |
| `fs` | object | fs-extra |
| `os`, `path` | object | Node `os` / `path` |
| `YAML`, `MAML` | object | YAML / MAML parsers |
| `dotenv` | object | `.env` parsing and loading |
| `quote()`, `quotePowerShell()` | function | Argument quoting functions |
| `useBash()`, `usePowerShell()`, `usePwsh()` | function | Select shell + quoting |
| `versions` | object | Versions of zx and bundled deps |
| `nothrow()`, `quiet()` | function | **Deprecated** — use `$\`cmd\`.nothrow()` / `.quiet()` |

### 16.2 `ProcessPromise` methods and getters

| Member | Description |
|--------|-------------|
| `pipe()` / `.pipe.stdout` / `.pipe.stderr` / `.pipe.stdall` | Redirect output to a process, stream, or file |
| `unpipe()` | Remove a destination from the pipeline |
| `nothrow()` | Do not throw on non-zero exit |
| `quiet()` | Suppress output |
| `verbose()` | Print command and output |
| `timeout(duration, signal?)` | Kill after a duration |
| `kill(signal?)` | Kill the process (default `SIGTERM`) |
| `abort(reason?)` | Abort via `AbortController` |
| `stdio(in, out?, err?)` | Configure standard streams |
| `run()` | Start a halted process |
| `json()`, `text()`, `lines()`, `buffer()`, `blob()` | Output formatters |
| `stdin`, `stdout`, `stderr` | Process streams |
| `exitCode` | Promise resolving to the exit code |
| `pid`, `cwd`, `cmd`, `fullCmd`, `stage` | Process metadata |
| `[Symbol.asyncIterator]` | Iterate stdout line by line |

### 16.3 `ProcessOutput` members

| Member | Description |
|--------|-------------|
| `stdout`, `stderr`, `stdall` | Captured output streams |
| `exitCode` | Exit code (`0` = success) |
| `signal` | Terminating signal, or `null` |
| `ok` | `true` if exit code is `0` and no error |
| `json()`, `text(encoding?)`, `lines(delimiter?)`, `buffer()`, `blob(type?)` | Output formatters |
| `toString()` | Combined stdout + stderr |
| `valueOf()` | Trimmed `toString()` |

### 16.4 Duration format

Durations accept a number of milliseconds or a string: `'500ms'`, `'30s'`, `'5m'`, or a bare number string like `'1000'`.

### 16.5 Useful environment variables

| Variable | Effect |
|----------|--------|
| `ZX_VERBOSE`, `ZX_QUIET`, `ZX_SHELL`, `ZX_PREFIX`, `ZX_POSTFIX`, `ZX_CWD`, … | Set the matching `$` option (any CLI option has a `ZX_`-prefixed form) |
| `ZX_REPL_HISTORY` | Path to the REPL history file (default `~/.zx_repl_history`) |
| `FORCE_COLOR` | Force colored output in subprocesses |
| `CI` | When set, disables the `spinner()` animation |

---

_Generated June 2026 from google/zx at b6bc975. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
