#!/usr/bin/env zx
await$`cat package.json | grep name`
letbranch=await$`git branch --show-current`
await$`dep deploy --branch=${branch}`
awaitPromise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
letname='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. Thezx
package provides
useful wrappers aroundchild_process
,escapes arguments and
gives sensible defaults.
npm i -g zx
Requirement:Node version >= 16.0.0
$·cd()·fetch()·question()·sleep()·echo()·stdin()·within()·retry()·spinner()· chalk·fs·os·path·glob·yaml·minimist·which· __filename·__dirname·require()
For running commands on remote hosts, seewebpod.
Write your scripts in a file with an.mjs
extension in order to
useawait
at the top level. If you prefer the.js
extension,
wrap your scripts in something likevoid async function () {...}()
.
Add the following shebang to the beginning of yourzx
scripts:
#!/usr/bin/env zx
Now you will be able to run your script like so:
chmod +x./script.mjs
./script.mjs
Or via thezx
executable:
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).
import'zx/globals'
Executes a given command using thespawn
func
and returnsProcessPromise
.
Everything passed through${...}
will be automatically escaped and quoted.
letname='foo & bar'
await$`mkdir${name}`
There is no need to add extra quotes.Read more about it in quotes.
You can pass an array of arguments if needed:
letflags=[
'--oneline',
'--decorate',
'--color',
]
await$`git log${flags}`
If the executed program returns a non-zero exit code,
ProcessOutput
will be thrown.
try{
await$`exit 1`
}catch(p){
console.log(`Exit code:${p.exitCode}`)
console.log(`Error:${p.stderr}`)
}
classProcessPromiseextendsPromise<ProcessOutput>{
stdin:Writable
stdout:Readable
stderr:Readable
exitCode:Promise<number>
pipe(dest):ProcessPromise
kill():Promise<void>
nothrow():this
quiet():this
}
Read more about theProcessPromise.
classProcessOutput{
readonlystdout:string
readonlystderr:string
readonlysignal:string
readonlyexitCode: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.
IfProcessOutput
is used as an argument to some other$
process,
zxwill use stdout and trim the new line.
letdate=await$`date`
await$`echo Current date is${date}.`
Changes the current working directory.
cd('/tmp')
await$`pwd`// => /tmp
A wrapper around thenode-fetch package.
letresp=awaitfetch('https://medv.io')
A wrapper around thereadlinepackage.
letbear=awaitquestion('What kind of bear is best? ')
A wrapper around thesetTimeout
function.
awaitsleep(1000)
Aconsole.log()
alternative which can takeProcessOutput.
letbranch=await$`git branch --show-current`
echo`Current branch is${branch}.`
// or
echo('Current branch is',branch)
Returns the stdin as a string.
letcontent=JSON.parse(awaitstdin())
Creates a new async context.
await$`pwd`// => /home/path
within(async()=>{
cd('/tmp')
setTimeout(async()=>{
await$`pwd`// => /tmp
},1000)
})
await$`pwd`// => /home/path
letversion=awaitwithin(async()=>{
$.prefix+='export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; '
await$`nvm use 16`
return$`node -v`
})
Retries a callback for a few times. Will return after the first successful attempt, or will throw after specifies attempts count.
letp=awaitretry(10,()=>$`curl https://medv.io`)
// With a specified delay between attempts.
letp=awaitretry(20,'1s',()=>$`curl https://medv.io`)
// With an exponential backoff.
letp=awaitretry(30,expBackoff(),()=>$`curl https://medv.io`)
Starts a simple CLI spinner.
awaitspinner(()=>$`long-running command`)
// With a message.
awaitspinner('working...',()=>$`sleep 99`)
The following packages are available without importing inside scripts.
Thechalkpackage.
console.log(chalk.blue('Hello world!'))
Thefs-extrapackage.
let{version}=awaitfs.readJson('./package.json')
Theospackage.
await$`cd${os.homedir()}&& mkdir example`
Thepathpackage.
await$`mkdir${path.join(basedir,'output')}`
Theglobbypackage.
letpackages=awaitglob(['package.json','packages/*/package.json'])
Theyamlpackage.
console.log(YAML.parse('foo: bar').foo)
Theminimistpackage available
as global constargv
.
if(argv.someFlag){
echo('yes')
}
Thewhichpackage.
letnode=awaitwhich('node')
Specifies what shell is used. Default iswhich bash
.
$.shell='/usr/bin/bash'
Or use a CLI argument:--shell=/bin/bash
Specifies aspawn
api. Defaults torequire('child_process').spawn
.
Specifies the command that will be prefixed to all commands run.
Default isset -euo pipefail;
.
Or use a CLI argument:--prefix='set -e;'
Specifies a function for escaping special characters during command substitution.
Specifies verbosity. Default istrue
.
In verbose mode,zx
prints all executed commands alongside with their
outputs.
Or use the CLI argument--quiet
to set$.verbose = false
.
Specifies an environment variables map.
Defaults toprocess.env
.
Specifies a current working directory of all processes created with the$
.
Thecd()func changes onlyprocess.cwd()
and if no$.cwd
specified,
all$
processes useprocess.cwd()
by default (same asspawn
behavior).
Specifies alogging function.
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)
}
}
InESMmodules, 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 thezx
executable).
InESM
modules, therequire()
function is not defined.
Thezx
providesrequire()
function, so it can be used with imports in.mjs
files (when usingzx
executable).
let{version}=require('./package.json')
process.env.FOO='bar'
await$`echo $FOO`
When passing an array of values as an argument to$
,items of the array will
be escaped
individually and concatenated via space.
Example:
letfiles=[...]
await$`tar cz${files}`
It is possible to make use of$
and other functions via explicit imports:
#!/usr/bin/env node
import{$}from'zx'
await$`date`
If script does not have a file extension (like.git/hooks/pre-commit
), zx
assumes that it is
anESM
module.
Thezx
can executescripts written as markdown:
zx docs/markdown.md
import{$}from'zx'
// Or
import'zx/globals'
voidasyncfunction(){
await$`ls -la`
}()
Set"type": "module"
inpackage.json
and"module": "ESNext"
intsconfig.json.
If the argument to thezx
executable starts withhttps://
,the file will be
downloaded and executed.
zx https://medv.io/game-of-life.js
Thezx
supports executing scripts from stdin.
zx<<'EOF'
await$`pwd`
EOF
Evaluate the following argument as a script.
cat package.json|zx --eval'let v = JSON.parse(await stdin()).version; echo(v)'
// script.mjs:
importshfrom'tinysh'
sh.say('Hello, world!')
Add--install
flag to thezx
command to install missing dependencies
automatically.
zx --install script.mjs
You can also specify needed version by adding comment with@
after
the import.
importshfrom'tinysh'// @^1
Thezx
useswebpodto execute commands on
remote hosts.
import{ssh}from'zx'
awaitssh('user@host')`echo Hello, world!`
By defaultchild_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
.
$.prefix+='export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; '
await$`nvm -v`
The default GitHub Action runner comes withnpx
installed.
jobs:
build:
runs-on:ubuntu-latest
steps:
-uses:actions/checkout@v3
-name:Build
env:
FORCE_COLOR:3
run:|
npx zx <<'EOF'
await $`...`
EOF
Impatient early adopters can try the experimental zx versions.
But keep in mind: these builds are
npm i zx@dev
npx zx@dev --install --quiet<<<'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)'
Disclaimer:This is not an officially supported Google product.