シェルで複雑な処理を書きたい場合はスクリプトファイルを作成することになります。
私の場合、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 氏がオープンソースで公開しているプロジェクトです。
まだ公開されてから半年くらいです。
主な特徴
以下のような特徴があります。
- トップレベルで
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-extra を fs
で使うことができるようになっています。
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('!'));
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
〆
普段シェルスクリプトに触れていない人でもメンテナンスがしやすくなっていい感じです。