From 99fbd7d355f9118cde7f91004cb88b8aa43abd93 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Tue, 5 Sep 2023 16:26:53 +0200 Subject: [PATCH] Update README.md --- README.md | 581 +--------------------------------------- docs/known-issues.md | 27 -- docs/markdown.md | 42 --- docs/process-promise.md | 151 ----------- docs/quotes.md | 75 ------ 5 files changed, 2 insertions(+), 874 deletions(-) delete mode 100644 docs/known-issues.md delete mode 100755 docs/markdown.md delete mode 100755 docs/process-promise.md delete mode 100644 docs/quotes.md diff --git a/README.md b/README.md index a26dbd64d7..e494a4da68 100644 --- a/README.md +++ b/README.md @@ -30,589 +30,12 @@ gives sensible defaults. ## Install ```bash -npm i -g zx +npm install zx ``` -**Requirement**: Node version >= 16.0.0 - -## Goods - -[$](#command-) · [cd()](#cd) · [fetch()](#fetch) · [question()](#question) · [sleep()](#sleep) · [echo()](#echo) · [stdin()](#stdin) · [within()](#within) · [retry()](#retry) · [spinner()](#spinner) · -[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) - -For running commands on remote hosts, -see [webpod](https://github.com/webpod/webpod). - ## 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 -``` - -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](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 -``` - -### 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)' -``` +Read documentation on [google.github.io/zx](https://google.github.io/zx/). ## License diff --git a/docs/known-issues.md b/docs/known-issues.md deleted file mode 100644 index 70a794130a..0000000000 --- a/docs/known-issues.md +++ /dev/null @@ -1,27 +0,0 @@ -# Known Issues - -## Output gets truncated - -This is a known issue with `console.log()` (see [nodejs/node#6379](https://github.com/nodejs/node/issues/6379)). -It's caused by different behaviour of `console.log()` writing to the terminal vs -to a file. If a process calls `process.exit()`, buffered output will be truncated. -To prevent this, the process should use `process.exitCode = 1` and wait for the -process to exit itself. Or use something like [exit](https://www.npmjs.com/package/exit) package. - -Workaround is to write to a temp file: -```js -let tmp = await $`mktemp` // Creates a temp file. -let {stdout} = await $`cmd > ${tmp}; cat ${tmp}` -``` - -## Colors in subprocess - -You may see what tools invoked with `await $` don't show colors, compared to -what you see in a terminal. This is because, the subprocess does not think it's -a TTY and the subprocess turns off colors. Usually there is a way force -the subprocess to add colors. - -```js -process.env.FORCE_COLOR='1' -await $`cmd` -``` diff --git a/docs/markdown.md b/docs/markdown.md deleted file mode 100755 index 6005bcabed..0000000000 --- a/docs/markdown.md +++ /dev/null @@ -1,42 +0,0 @@ -# Markdown Scripts - -It's possible to write scripts using markdown. Only code blocks will be executed -by zx. - -> You can run this markdown file: -> -> ``` -> zx docs/markdown.md -> ``` - -```js -await $`whoami` -await $`echo ${__dirname}` -``` - -The `__filename` will be pointed to **markdown.md**: - -```js -console.log(chalk.yellowBright(__filename)) -``` - -We can use imports here as well: - -```js -await import('chalk') -``` - -A bash code (with `bash` or `sh` language tags) also will be executed: - -```bash -VAR=$(date) -echo "$VAR" | wc -c -``` - -Other code blocks are ignored: - -```css -body .hero { - margin: 42px; -} -``` diff --git a/docs/process-promise.md b/docs/process-promise.md deleted file mode 100755 index 061b0d81c0..0000000000 --- a/docs/process-promise.md +++ /dev/null @@ -1,151 +0,0 @@ -# ProcessPromise - -The `$` returns a `ProcessPromise` instance. - -```js -let p = $`command` - -await p -``` - -## `stdin` - -Returns a writable stream of the stdin process. Accessing -this getter will trigger execution of a subprocess with [`stdio('pipe')`](#stdio). - -Do not forget to end the stream. - -```js -let p = $`while read; do echo $REPLY; done` -p.stdin.write('Hello, World!\n') -p.stdin.end() -``` - -By default, each process is created with stdin in _inherit_ mode. - -## `stdout`/`stderr` - -Returns a readable streams of stdout/stderr process. - -```js -const p = $`npm init` -for await (const chunk of p.stdout) { - echo(chunk) -} -``` - -## `exitCode` - -Returns a promise which resolves to the exit code of the process. - -```js -if (await $`[[ -d path ]]`.exitCode == 0) { - ... -} -``` - -## `pipe()` - -Redirects the stdout of the process. - -```js -await $`echo "Hello, stdout!"` - .pipe(fs.createWriteStream('/tmp/output.txt')) - -await $`cat /tmp/output.txt` -``` - -Pipes can be used to show a real-time output of the process: - -```js -await $`echo 1; sleep 1; echo 2; sleep 1; echo 3;` - .pipe(process.stdout) -``` - -The `pipe()` method can combine `$` processes. Same as `|` in bash: - -```js -let greeting = await $`printf "hello"` - .pipe($`awk '{printf $1", world!"}'`) - .pipe($`tr '[a-z]' '[A-Z]'`) - -echo(greeting) -``` - -Use combinations of `pipe()` and [`nothrow()`](#nothrow): - -```js -await $`find ./examples -type f -print0` - .pipe($`xargs -0 grep ${'missing' + 'part'}`.nothrow()) - .pipe($`wc -l`) -``` - -## `kill()` - -Kills the process and all children. - -By default, signal `SIGTERM` is sent. You can specify a signal via an argument. - -```js -let p = $`sleep 999` -setTimeout(() => p.kill('SIGINT'), 100) -await p -``` - -## `stdio()` - -Specifies a stdio for the process. - -Default is `.stdio('inherit', 'pipe', 'pipe')`. - -```js -let p = $`read`.stdio('pipe') -``` - -## `nothrow()` - -Changes behavior of `$` to not throw an exception on non-zero exit codes. - -```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`) -``` - -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) { - ... -} -``` - -## `quiet()` - -Changes behavior of `$` to disable verbose output. - -```js -// Command and output will not be displayed. -await $`grep something from-file`.quiet() -``` - -## `timeout()` - -Kills the process after a specified timeout. - -```js -await $`sleep 999`.timeout('5s') - -// Or with a specific signal. -await $`sleep 999`.timeout('5s', 'SIGKILL') -``` diff --git a/docs/quotes.md b/docs/quotes.md deleted file mode 100644 index 9cfc0fe53d..0000000000 --- a/docs/quotes.md +++ /dev/null @@ -1,75 +0,0 @@ -# Quotes - -When passing arguments to `${...}` there is no need to add quotes. **Quotes will -be added automatically if needed.** - -```js -let name = 'foo & bar' -await $`mkdir ${name}` -``` - -For quotes **zx** uses special bash syntax (next commands are valid bash): - -```bash -mkdir $'foo & bar' -$'ls' $'-la' -``` - -If you add quotes `"${name}"`, it will produce a wrong command. - -If you need to add something extra, consider putting it inside curly brackets. - -```js -await $`mkdir ${'path/to-dir/' + name}` -``` - -This will also work properly: - -```js -await $`mkdir path/to-dir/${name}` -``` - -### Array of arguments - -The `zx` can also take an array or arguments in the `${...}`. Items of the array -will be quoted separately and concatenated via a space. - -Do **not** add a `.join(' ')`. - -```js -let flags = [ - '--oneline', - '--decorate', - '--color', -] -await $`git log ${flags}` -``` - -If you already have a string with arrays, it's your responsibility -to correctly parse it and distinguish separate arguments. For example like this: - -```js -await $`git log ${'--oneline --decorate --color'.split(' ')}` -``` - -### globbing and `~` - -As everything passed through `${...}` will be escaped, you can't use `~` or glob -syntax. - -In order for this to work the zx provides -[globby package](../README.md#globby-package). - -For instead of this: - -```js -let files = '~/dev/**/*.md' // wrong -await $`ls ${files}` -``` - -Use `glob` function and `os` package: - -```js -let files = await glob(os.homedir() + '/dev/**/*.md') -await $`ls ${files}` -```