zxの紹介 〜 さよならシェルスクリプト そして伝説へ|Offers Tech Blog

2022/06/06に公開

はじめに

こんにちは!!

プロダクト開発人材の副業転職プラットフォーム Offers を運営する株式会社 overflow 普通のバックエンドエンジニアの takkun7171 でございます。
最近は Apex もやってますが、筋トレにハマっています。可変式ダンベル買ったんですけど、これはいいモノですね。齢 40 のオッサンだけど、ちゃんと鍛えれば筋肉育つんだから!!w

zxとは

簡単に言うと、JavaScript でシェルスクリプトがお手軽に書けます。
zx は Node の child_process のラッパーで、$で囲んで shell コマンドを簡単に実行できるので、
zxを使いこなせれば、シェルスクリプトとおさらばできます。
https://github.com/google/zx

zx は 2021 年に最もトレンディだったプロジェクトだったらしいです。すごいですね。
https://risingstars.js.org/2021/ja

紹介記事はネット上にちらほらあるのですが、便利さの割に周りで使ってる人もいないので今回 zx について書いてみました。

なお zx は Google の Organization 下で公開されていますが、現在は Anton Medvedev 氏によって保守されており、Google によって公式にサポートされているわけではないようです。

何が良いの?

こちらの方も語っているのですが、
https://blog.8-p.info/ja/2021/09/15/bash/

シェルスクリプトって何かよくわからない落とし穴が多くて、ちょっとしたことで動かないイメージがあります。実際、制御構文に癖がありますし、配列や json の扱いもやりづらいです。こんないつ爆発するかわからない地雷みたいな言語、出来れば書きたくないし、保守もしたくないですよね。

そこで zx の登場なんですけど、文法は JavaScript で皆が知ってる通りですから、遥かに開発も保守の両方が楽になります。

後述しますが、TypeScript でも動いたり、リモートで実行したり、md ファイルでも動いたりします。docker 内で動かせるようにすれば RUN ./script.mjs で動くので便利です。

つまりzxを使いこなせれば、シェルスクリプトとおさらばできます。
大事なことなので 2 回書きました w

とりあえず使ってみよう

以下でインストールできます。

npm i -g zx

拡張子は mjs にすると、await が top-level でいきなり使えるので便利です。
js でもいいけど、その場合は await 使うのに、void async function () {...}()でラップする必要があります。

以下のように script.mjs を作成し

#!/usr/bin/env zx
const result = await $`ls -la`
console.log(result) // stdout, stderr, exitCodeなどが取得できる

これで実行できます

chmod +x script.mjs
zx script.mjs

基本的な使い方

以下サンブルの使い方です。
ちょいちょい公式や他の方の記事のスクリプトを紹介させてもらっています。

  • 配列は展開されるみたいです。コマンドのオプション渡す時も便利です。
let files = ["wttr_request.mjs", "test.mjs"]
let res = await $`ls -l ${files}`
console.log(res)
  • Promise.all で並列実行もできます
await Promise.all([
  $`sleep 1; echo 1`,
  $`sleep 2; echo 2`,
  $`sleep 3; echo 3`,
]);
  • await で非同期処理の制御も簡単で、node-fetch などよく使われるパッケージはすぐ使えます。以下でいい感じの天気予報が出ます。API を叩く時などに便利ですね。
let resp = await fetch('http://wttr.in')
if (resp.ok) {
  console.log(await resp.text())
}
  • chalk、globby、os、sleep、yaml も最初から使えます
console.log(chalk.blue('Hello world!'))

let txts = globby.globbySync('*.txt')
console.log(txts)

await $`cd ${os.homedir()}`

$`sleep 5`

console.log(YAML.parse('foo: bar').foo)
  • リモートで実行できます。ただし https である必要があります。
    save_public_key_by_uuid いきかでいきなりライフゲームが始まります w
zx https://medv.io/game-of-life.js
  • TypeScript でも書けるので、型の恩恵にあずかれます。
    自分は以下の方法で TypeScript が動くことを確認しています。
npm init
npm install --save typescript ts-node
./node_modules/.bin/tsc --init

したあとで package.json に "type": "module"を追加し、
tsconfig.json に"module": "ESNext", "moduleResolution": "node" を追加します。

それから test.ts を以下のように作成し

import { $ } from 'zx'

void async function () {
  await $`ls -la`
}()

以下を実行で TypeScript が動きます

node --loader ts-node/esm test.ts
  • docker 内でシェルスクリプト使うことあるあるだと思うんですけど、
    npx を使用することで zx をインストールしなくても RUN npx zx ./script.mjs で実行できます。
    docker内のシェルスクリプト、デプロイ用のシェルスクリプト、Makefileなどを頑張ればzxで置換できます。 個人的にはこれが一番有用なユースケースではないかと思ってます。更に package.json の scripts に zx の実行コマンドを書くと捗りそうです。
    まだ着手していませんが、いずれ docker 内のシェルスクリプトを zx によって一掃したいと考えています。

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

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

パイプを使って処理をつなげていきたいときもこれなら見通しが良くなります。
【参考】
https://www.webdelog.info/entry/2021/11/06/215036
https://github.com/google/zx/blob/main/docs/pipelines.md

  • 例外処理も簡単で 0 以外の exit を捕捉できます。
try {
  await $`exit 1`
} catch (p) {
  console.log(`p: ${p}`)
  console.log(`Exit code: ${p.exitCode}`)
  console.log(`Error: ${p.stderr}`)
}
  • nothrow をつけると 0 以外の exit を throw しなくなり、処理を続行できます。
try {
  await nothrow($`grep something from-file`)
} catch (p) {
  console.log(`p: ${p}`)
  console.log(`Exit code: ${p.exitCode}`)
  console.log(`Error: ${p.stderr}`)
}
  • 拡張子が md の markdown ファイルの実行ができます。

https://github.com/google/zx/blob/main/docs/markdown.md

js のコードブロックの部分のみ実行して、それ以外のコードブロックの部分は無視するという仕組みです。README 的な md の文章に js つけて、実行用のスクリプトだけど、markdown で説明をふんだんに盛り込む、ということが可能です。なかなかユニークかつ有用ではないでしょうか?

  • readline が最初から入っており、対話形式のコマンドも楽です。
let userName = await question('What is your username? ')
console.log(userName);
  • cd で移動できます
cd('/tmp')
await $`pwd` // outputs /tmp
  • 実験的な機能ですが、指定時間 delay して、お手軽に retry 出来ます。
import { retry } from 'zx/experimental'

let { stdout } = await retry(3, 1000)`npm whoami`
console.log(stdout)
  • quiet で囲むと出力を抑制できます
await quiet($`cat wttr_request.mjs`)

$.verbose を false にすることでも、出力を抑制できます。

$.verbose = false;
const msg = "Hello, world!";
$`echo ${msg}`.pipe(process.stdout);

--quiet をつけて実行しても出力を抑制できます。

zx --quiet script.mjs
  • 環境変数の読み込みも簡単
process.env.FOO = 'bar'
await $`echo $FOO`
  • file 読み込みも簡単です
let content = fs.readFileSync('./zxについて.txt', { encoding: 'utf-8' })
console.log(content)
  • escape 周りはちょっと注意事項があるらしく、こちらの方の記事が詳しいです

https://t28.dev/write-script-using-zx/#quotes-zx-のお作法-

まとめ

今回は zx についてまとめてみました。
これを使って、開発も保守もしづらいシェルスクリプトからおさらばしたいですね!!
ではまた!!

関連記事

https://zenn.dev/offers/articles/20220425-universal-attitude
https://zenn.dev/offers/articles/20220523-component-design-best-practice
https://zenn.dev/offers/articles/20220415-leader-and-manager-roles-in-overflow

Offers Tech Blog

Discussion