zx を使って JavaScript でスクリプトを書いてみる

シェルで複雑な処理を書きたい場合はスクリプトファイルを作成することになります。

私の場合、JavaScript で書きたくなります。Node.js にはChild process というモジュールが存在しています。

これを経由することで子プロセスを作成して OS のコマンドを Node.js から実行することができます。

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

しかし非同期処理が callback 方式だったり、Promise.then や async await を使って処理をシンプルに書くためには promisify を通す必要があります。

zx を使うと Node.js でよりシンプルにOS コマンドを実行することができ、Node.js でスクリプトを書くときによく使うモジュールも内包されていて便利です。

この記事ではそんな zx がどんな便利なものかを紹介してみようかと思います。

zx

zx は Google の開発者であるAnton Medvedev 氏がオープンソースで公開しているプロジェクトです。

https://github.com/google/zx

まだ公開されてから半年くらいです。

主な特徴

以下のような特徴があります。

  • トップレベルで await が使える。(ESM, TypeScript の場合)
  • child_process.spawn をよりシンプルで Promise を返してくれる $`command` が用意されている
  • node-fetch, chalk, os, fs-extra, globby path, minimist など、よく使われるパッケージを require しなくても使うことができる
  • markdown ファイル内のコードブロックからスクリプトを実行できる
  • リモートスクリプトを実行できる

インストール

記事を書いた時点では Node.js のバージョンは 14.13.1 以上を推奨とされています。

npm で zx のインストールを済ませておきます。

npm install -g zx

global にインストールしたくない場合、一時的に使いたい場合などは npx を使って npx zx とかで実行すればいいかなと思います。

Hello world

インストールが完了したら早速 Hello world してみましょう。

zx script は ESM(ES Modules) で書くことができます。

.mjs をつけて script.mjs を作成しましょう。

#!/usr/bin/env zx
const arr = 'Hello world'
await $`echo ${arr}`

ESM か TypeScript(方法は後述)ならトップレベルで await を書くことができます。

shebang #!/usr/bin/env zx はnpmでグローバルに zx をインストールしている前提です。

プロジェクト単位で使いたい場合、あるいは node コマンドから実行したい場合などの実行方法の違いについては下記の記事でまとめてくださっているのでこちらを参考にしていただくのが良いかと思います。

ちょっと複雑なシェルスクリプトをJavaScriptで書く - lacolaco-engineering

ファイルの作成が完了したらファイルに実行権限を追加して、実行しましょう。

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

実行すると次のようなログが表示されます。

zx/script.mjs
$ echo $'Hello world'
Hello world

echo 'Hello world' が実行されて、 Hello world が出力されましたね!

基本機能

$commnad

Hello world がうまくいったところで先程のコード内で使われていた $`echo ${arr}` というコードについて見ていきます。

$() に文字列を渡すとchild_process.spawn 関数を使って与えられた文字列のコマンドを実行することができます。

配列を渡すと自動的に展開されます。コマンドのオプションを複数指定したいときなどに便利です。

let flags = [
  '--oneline',
  '--decorate',
  '--color',
]
await $`git log ${flags}`

// => git log --oneline --decorate --color

返り値には Promise を拡張して、stdout, stderr, exitCode などの参照ができる ProcessPromise が返ってきます。

class ProcessPromise<T> extends Promise<T> {
  readonly stdin: Writable
  readonly stdout: Readable
  readonly stderr: Readable
  readonly exitCode: Promise<number>
  pipe(dest): ProcessPromise<T>
  kill(signal = 'SIGTERM'): Promise<void>
}

pipe()

ProcessPromise.pipe を使うと、次の処理に標準出力の結果を標準入力として渡すことができます。

#!/usr/bin/env zx
await $`ls`.pipe($`echo $(cat)`)
//await $`ls | xargs echo` と同じ

パイプを使って処理をつなげていきたいときもこれなら見通しが良くなります。

最初から使える便利な関数

zx には特に追加でインストールしなくても使うことができる便利な関数がたくさん用意されています。

ここではその一部を紹介します。

question()

readline がラップされており、より簡単に使うことができるようになっています。

// 入力式の質問
const name = await question('あなたの名前はなんですか?')
// 第2引数の choices に配列の文字列を渡すことで選択式の質問を作れる
const address = await question('お住まいはどちらですか?', {
  choices: []
})

// => あなたの名前はなんですか?田中太郎
// => お住まいはどちらですか?茨城県

sleep()

一時的に待ち状態を作る sleep が使えます。

await sleep(1000);
$`echo 1秒たったら出力されるよ`;

nothrow()

0 以外の exit code が返ってきたときでも後続の処理が止まらないようにしてくれます。

await $`find ./examples -type f -print0`
  .pipe(nothrow($`xargs -0 grep something`))
  .pipe($`wc -l`)

その他インストールなしで使うことができるパッケージ

その他にも zx には追加のインストールを行わずとも使うことができる便利なパッケージが同梱されています。

fetch()

Node.js でも fetch() を使えるようにできる node-fetch をラップしているので特に何もしなくてもHTTP通信が簡単に使えます。

const result = await fetch('https://jsonplaceholder.typicode.com/todos/1')
if(result.ok) {
  console.log(await result.json());
  // => { userId: 1, id: 1, title: 'delectus aut autem', completed: false }
}

fs

fs-extrafs で使うことができるようになっています。

fs-extra は Node.js の FileSystem module である fs に足りない機能が追加されていたり、非同期処理に対して Promise を返してくれたりします。

await fs.ensureDir('/tmp/hogehoge/');
await $`echo 'success!!`

globby

glob パターンでファイルを指定できる globby が使えます。

await $`svgo ${await glob('*.svg')}`

minimist

実行時の引数をパースして Node.js から扱いやすくしてくれる minimist が使えます。

グローバル変数 argv から参照できるので、セットアップを行う必要がないまま使うことができます。

script.mjs

console.log(argv)
$ ./script.mjs --hoge
// => { _: [ '/Users/turusuke/WebstormProjects/zx/script.mjs' ], hoge: 123 }

chalk

出力される文字の色を変えるときに便利な chalk が使えます。

console.log(chalk.blue('Hello') + chalk.yellow(' World') + chalk.red('!'));

chalk を使って文字列に装飾を加えて出力している画像

Polyfill

通常 ESM modules では使えない機能が zx scriptだと動作させることができます。(zx を使って実行したときのみ)

  • __filename & __dirname
  • require()

Typescript で書く

zx は TypeScript で書くことで型の恩恵を受けることができます。

script.ts

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

await $`ls -la`

ts-node などでコンパイルして実行します。

ts-node script.ts

マークダウンで書く

ファイルを読み込んで実行することができます。

コードブロックでファイルタイプを js として書いてあるコードを実行することができます。

ファイルタイプを bash あるいは sh とすることで bash のコードを実行することもできます。

実行時に mjs ファイルになっているのでトップレベル await も使えます。

# Markdown Script

これは Markdown で書かれたスクリプトです。

## Shell script

```sh
whoami
``` ``

## zx script

```javascript
console.log(chalk`{white.bgGreen Platform }: ${os.platform()} `)
console.log(chalk`{white.bgGreen Hostname }: ${os.hostname()} `)
```

## 他のファイルタイプは無視される

```html
<p>これは実行されない。</p>
```
zx script.md

// turusuke
// Platform : darwin
// Hostname : MacBook-Pro.local

リモートスクリプトを実行する

https から始まるURLで始まる外部のスクリプトを実行することができます。

下記の Github Gist に置いてあるファイルを実行してみます。

https://gist.github.com/turusuke/444de1bc9e80d35dee3cd90d17d04c16

zx https://gist.githubusercontent.com/turusuke/444de1bc9e80d35dee3cd90d17d04c16/raw/bcefec602679831d611549252eb1dcbd8cc24622/script.m

// turusuke
// Platform : darwin
// Hostname : MacBook-Pro.local

普段シェルスクリプトに触れていない人でもメンテナンスがしやすくなっていい感じです。


この記事で紹介したツール

github.com