diff --git a/.vitepress/config.mts b/.vitepress/config.mts index ceea130b85..48c198bb85 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -35,8 +35,19 @@ export default defineConfig({ items: [ {text: 'Getting Started', link: '/getting-started'}, {text: 'Process Promise', link: '/process-promise'}, + {text: 'API Reference', link: '/api'}, + {text: 'Configuration', link: '/configuration'}, + {text: 'CLI Usage', link: '/cli'}, + ], + }, + { + text: 'FAQ', + link: '/faq', + items: [ {text: 'Quotes', link: '/quotes'}, + {text: 'TypeScript', link: '/typescript'}, {text: 'Markdown Scripts', link: '/markdown-scripts'}, + {text: 'Known Issues', link: '/known-issues'}, ], }, ], @@ -45,6 +56,10 @@ export default defineConfig({ {icon: 'github', link: 'https://github.com/google/zx'}, ], + editLink: { + pattern: 'https://github.com/google/zx/blob/gh-pages/:path', + }, + footer: { message: 'Disclaimer: This is not an officially supported Google product.', }, diff --git a/api.md b/api.md new file mode 100644 index 0000000000..76e7c0b07d --- /dev/null +++ b/api.md @@ -0,0 +1,200 @@ +# API Reference + +## cd() + +Changes the current working directory. + +```js +cd('/tmp') +await $`pwd` // => /tmp +``` + +Like `echo`, in addition to `string` arguments, `cd` accepts and trims +trailing newlines from `ProcessOutput` enabling common idioms like: + +```js +cd(await $`mktemp -d`) +``` + +## 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 +await $`node --version` // => v20.2.0 + +let version = await within(async () => { + $.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; nvm use 16;' + + return $`node --version` +}) + +echo(version) // => v16.20.0 +``` + +## retry() + +Retries a callback for a few times. Will return after the first +successful attempt, or will throw after specifies attempts count. + +```js +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 +await spinner(() => $`long-running command`) + +// With a message. +await spinner('working...', () => $`sleep 99`) +``` + +## glob() + +The [globby](https://github.com/sindresorhus/globby) package. + +```js +let packages = await glob(['package.json', 'packages/*/package.json']) +``` + +## which() + +The [which](https://github.com/npm/node-which) package. + +```js +let node = await which('node') +``` + +## argv + +The [minimist](https://www.npmjs.com/package/minimist) package. + +A minimist-parsed version of the `process.argv` as `argv`. + +```js +if (argv.someFlag) { + echo('yes') +} +``` + +Use minimist options to customize the parsing: + +```js +const myCustomArgv = minimist(process.argv.slice(2), { + boolean: [ + 'force', + 'help', + ], + alias: { + h: 'help', + }, +}) +``` + +## chalk + +The [chalk](https://www.npmjs.com/package/chalk) package. + +```js +console.log(chalk.blue('Hello world!')) +``` + +## fs + +The [fs-extra](https://www.npmjs.com/package/fs-extra) package. + +```js +let {version} = await fs.readJson('./package.json') +``` + +## os + +The [os](https://nodejs.org/api/os.html) package. + +```js +await $`cd ${os.homedir()} && mkdir example` +``` + +## path + +The [path](https://nodejs.org/api/path.html) package. + +```js +await $`mkdir ${path.join(basedir, 'output')}` +``` + +## yaml + +The [yaml](https://www.npmjs.com/package/yaml) package. + +```js +console.log(YAML.parse('foo: bar').foo) +``` diff --git a/cli.md b/cli.md new file mode 100644 index 0000000000..78403dd874 --- /dev/null +++ b/cli.md @@ -0,0 +1,96 @@ +# CLI Usage + +Zx provides a CLI for running scripts. It is installed with the package and can be used as `zx` executable. + +```sh +zx script.mjs +``` + +## 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. + +```bash +zx docs/markdown.md +``` + +## 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 +``` + +## Executing commands on remote hosts + +The `zx` uses [webpod](https://github.com/webpod/webpod) to execute commands on +remote hosts. + +```js +import {ssh} from 'zx' + +await ssh('user@host')`echo Hello, world!` +``` + +## `__filename` and `__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') +``` diff --git a/configuration.md b/configuration.md new file mode 100644 index 0000000000..ad5b968543 --- /dev/null +++ b/configuration.md @@ -0,0 +1,69 @@ +# 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) + } +} +``` diff --git a/faq.md b/faq.md new file mode 100644 index 0000000000..f582971811 --- /dev/null +++ b/faq.md @@ -0,0 +1,73 @@ +# 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` +``` + +## 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 ⚠️️__beta__ in every sense. + +```bash +npm i zx@dev +npx zx@dev --install --quiet <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)' +``` diff --git a/getting-started.md b/getting-started.md index c4a1fd75e9..7049799320 100644 --- a/getting-started.md +++ b/getting-started.md @@ -1,16 +1,39 @@ # Getting Started -## Install +## Overview -```bash -npm i -g 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}` ``` -**Requirement**: Node version >= 16.0.0 +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 -## Documentation +```bash +npm install zx +``` -Webpod - deploy JavaScript apps +## Usage 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, @@ -29,7 +52,7 @@ chmod +x ./script.mjs ./script.mjs ``` -Or via the `zx` executable: +Or via the [CLI](cli.md): ```bash zx ./script.mjs @@ -47,7 +70,7 @@ import 'zx/globals' ### ``$`command` `` Executes a given command using the `spawn` func -and returns [`ProcessPromise`](#processpromise). +and returns [`ProcessPromise`](process-promise.md). Everything passed through `${...}` will be automatically escaped and quoted. @@ -82,27 +105,6 @@ try { } ``` -### `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](process-promise.md). - ### `ProcessOutput` ```ts @@ -126,460 +128,6 @@ let date = await $`date` await $`echo Current date is ${date}.` ``` -## Functions - -### `cd()` - -Changes the current working directory. - -```js -cd('/tmp') -await $`pwd` // => /tmp -``` - -Like `echo`, in addition to `string` arguments, `cd` accepts and trims -trailing newlines from `ProcessOutput` enabling common idioms like: - -```js -cd(await $`mktemp -d`) -``` - -### `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 -await $`node --version` // => v20.2.0 - -let version = await within(async () => { - $.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; nvm use 16;' - - return $`node --version` -}) - -echo(version) // => v16.20.0 -``` - -### `retry()` - -Retries a callback for a few times. Will return after the first -successful attempt, or will throw after specifies attempts count. - -```js -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 -await spinner(() => $`long-running command`) - -// With a message. -await spinner('working...', () => $`sleep 99`) -``` - -## 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. - -```js -let myCustomArgv = minimist(process.argv.slice(2), { boolean: ["force", "help"] }) -``` - -A minimist-parsed version of the process args as `argv` (parsed without any config). - -```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') -``` - -## 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](markdown-scripts.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 -``` - -### Executing commands on remote hosts - -The `zx` uses [webpod](https://github.com/webpod/webpod) to execute commands on -remote hosts. - -```js -import { ssh } from 'zx' - -await ssh('user@host')`echo Hello, world!` -``` - - - -### 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 ⚠️️__beta__ 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](https://github.com/google/zx/blob/main/LICENSE) diff --git a/markdown-scripts.md b/markdown-scripts.md index 6005bcabed..3dfb11f09a 100644 --- a/markdown-scripts.md +++ b/markdown-scripts.md @@ -37,6 +37,6 @@ Other code blocks are ignored: ```css body .hero { - margin: 42px; + margin: 42px; } ``` diff --git a/process-promise.md b/process-promise.md index ea86fe8746..e5435783cf 100644 --- a/process-promise.md +++ b/process-promise.md @@ -1,4 +1,4 @@ -# ProcessPromise +# Process Promise The `$` returns a `ProcessPromise` instance. @@ -40,7 +40,7 @@ Returns a promise which resolves to the exit code of the process. ```js if (await $`[[ -d path ]]`.exitCode == 0) { - ... +... } ``` @@ -120,13 +120,13 @@ If only the `exitCode` is needed, you can use [`exitCode`](#exitcode) directly: ```js if (await $`[[ -d path ]]`.exitCode == 0) { - ... +... } // Equivalent of: if ((await $`[[ -d path ]]`.nothrow()).exitCode == 0) { - ... +... } ``` diff --git a/typescript.md b/typescript.md new file mode 100644 index 0000000000..83a08c2ba7 --- /dev/null +++ b/typescript.md @@ -0,0 +1,28 @@ +# TypeScript + +Configure your project to use [ES modules](https://nodejs.org/api/packages.html#packages_type): + +- Set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_type) +in **package.json** +- Set [`"module": "ESNext"`](https://www.typescriptlang.org/tsconfig/#module) +in **tsconfig.json**. + +It is possible to make use of `$` and other functions via explicit imports: + +```ts +import { $ } from 'zx' +``` + +Or import globals explicitly: + +```ts +import 'zx/globals' +``` + +Wrap your code in an async function and call it immediately: + +```ts +void async function () { + await $`ls -la` +}() +```