Skip to content

ronitsharma03/zx-issue-618

Repository files navigation

🐚 zx

#!/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. Thezxpackage provides useful wrappers aroundchild_process,escapes arguments and gives sensible defaults.

Install

npm i -g zx

Requirement:Node version >= 16.0.0

Goods

$·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.

Documentation

Write your scripts in a file with an.mjsextension in order to useawaitat the top level. If you prefer the.jsextension, wrap your scripts in something likevoid async function () {...}().

Add the following shebang to the beginning of yourzxscripts:

#!/usr/bin/env zx

Now you will be able to run your script like so:

chmod +x./script.mjs
./script.mjs

Or via thezxexecutable:

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'

$`command`

Executes a given command using thespawnfunc 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, ProcessOutputwill be thrown.

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

ProcessPromise

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.

ProcessOutput

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\nat the end. IfProcessOutputis 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}.`

Functions

cd()

Changes the current working directory.

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

fetch()

A wrapper around thenode-fetch package.

letresp=awaitfetch('https://medv.io')

question()

A wrapper around thereadlinepackage.

letbear=awaitquestion('What kind of bear is best? ')

sleep()

A wrapper around thesetTimeoutfunction.

awaitsleep(1000)

echo()

Aconsole.log()alternative which can takeProcessOutput.

letbranch=await$`git branch --show-current`

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

stdin()

Returns the stdin as a string.

letcontent=JSON.parse(awaitstdin())

within()

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`
})

retry()

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`)

spinner()

Starts a simple CLI spinner.

awaitspinner(()=>$`long-running command`)

// With a message.
awaitspinner('working...',()=>$`sleep 99`)

Packages

The following packages are available without importing inside scripts.

chalkpackage

Thechalkpackage.

console.log(chalk.blue('Hello world!'))

fspackage

Thefs-extrapackage.

let{version}=awaitfs.readJson('./package.json')

ospackage

Theospackage.

await$`cd${os.homedir()}&& mkdir example`

pathpackage

Thepathpackage.

await$`mkdir${path.join(basedir,'output')}`

globbypackage

Theglobbypackage.

letpackages=awaitglob(['package.json','packages/*/package.json'])

yamlpackage

Theyamlpackage.

console.log(YAML.parse('foo: bar').foo)

minimistpackage

Theminimistpackage available as global constargv.

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

whichpackage

Thewhichpackage.

letnode=awaitwhich('node')

Configuration

$.shell

Specifies what shell is used. Default iswhich bash.

$.shell='/usr/bin/bash'

Or use a CLI argument:--shell=/bin/bash

$.spawn

Specifies aspawnapi. Defaults torequire('child_process').spawn.

$.prefix

Specifies the command that will be prefixed to all commands run.

Default isset -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 istrue.

In verbose mode,zxprints all executed commands alongside with their outputs.

Or use the CLI argument--quietto set$.verbose = false.

$.env

Specifies an environment variables map.

Defaults toprocess.env.

$.cwd

Specifies a current working directory of all processes created with the$.

Thecd()func changes onlyprocess.cwd()and if no$.cwdspecified, all$processes useprocess.cwd()by default (same asspawnbehavior).

$.log

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)
}
}

Polyfills

__filename&__dirname

InESMmodules, Node.js does not provide __filenameand__dirnameglobals. As such globals are really handy in scripts, zxprovides these for use in.mjsfiles (when using thezxexecutable).

require()

InESM modules, therequire()function is not defined. Thezxprovidesrequire()function, so it can be used with imports in.mjs files (when usingzxexecutable).

let{version}=require('./package.json')

FAQ

Passing env variables

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:

letfiles=[...]
await$`tar cz${files}`

Importing into other scripts

It is possible to make use of$and other functions via explicit imports:

#!/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 anESM module.

Markdown scripts

Thezxcan executescripts written as markdown:

zx docs/markdown.md

TypeScript scripts

import{$}from'zx'
// Or
import'zx/globals'

voidasyncfunction(){
await$`ls -la`
}()

Set"type": "module" inpackage.json and"module": "ESNext" intsconfig.json.

Executing remote scripts

If the argument to thezxexecutable starts withhttps://,the file will be downloaded and executed.

zx https://medv.io/game-of-life.js

Executing scripts from stdin

Thezxsupports executing scripts from stdin.

zx<<'EOF'
await$`pwd`
EOF

Executing scripts via --eval

Evaluate the following argument as a script.

cat package.json|zx --eval'let v = JSON.parse(await stdin()).version; echo(v)'

Installing dependencies via --install

// script.mjs:
importshfrom'tinysh'

sh.say('Hello, world!')

Add--installflag to thezxcommand 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

Executing commands on remote hosts

Thezxuseswebpodto execute commands on remote hosts.

import{ssh}from'zx'

awaitssh('user@host')`echo Hello, world!`

Attaching a profile

By defaultchild_processdoes 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`

Using GitHub Actions

The default GitHub Action runner comes withnpxinstalled.

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.

npm i zx@dev
npx zx@dev --install --quiet<<<'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)'

License

Apache-2.0

Disclaimer:This is not an officially supported Google product.

About

A tool for writing better scripts.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 50.4%
  • TypeScript 49.6%