mirror of
https://github.com/deployphp/action.git
synced 2024-11-23 12:29:03 +00:00
577 lines
12 KiB
Markdown
577 lines
12 KiB
Markdown
# 🐚 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<ProcessOutput> {
|
|
stdin: Writable
|
|
stdout: Readable
|
|
stderr: Readable
|
|
exitCode: Promise<number>
|
|
pipe(dest): ProcessPromise
|
|
kill(): Promise<void>
|
|
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._
|