Zx — JavaScriptでより良いシェルスクリプトを書く
概要
zxは、JavaScript/TypeScriptでシェルスクリプトを書くためのGoogleのツールです。child_processをラップし、引数を自動エスケープし、適切なデフォルト設定を提供します。これにより、スクリプト内でJavaScriptエコシステムの力を活用できます。
#!/usr/bin/env zx
await $`cat package.json | grep name`
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
const name = 'foo & bar'
await $`mkdir /tmp/${name}` // 引用符は不要 — 自動エスケープされます
Bashは単純なタスクには最適ですが、スクリプトが複雑になると、本格的なプログラミング言語が役立ちます。zxはchild_processの便利なラッパーを追加し、引数をエスケープし、適切なデフォルト設定を提供します。考え方:bash + JavaScript が1つのスクリプトに。
トリガー
JavaScriptでのシェルコマンド実行、bashスクリプトからzxへの変換、リモートスクリプトの実行、Markdownスクリプト、TypeScriptシェルスクリプトについてユーザーが質問した場合もトリガーされます。
クイックスタート
npm install zx
スクリプトは.mjsファイルとして記述します(トップレベルawaitをサポート)。#!/usr/bin/env zxシバンを追加するか、CLI経由で実行します:
zx ./script.mjs # 直接実行
npx zx ./script.mjs # npx経由
node --import zx/globals # Node.jsローダーとして
すべての関数($, cd, fetchなど)は、zxスクリプト内でインポートなしでグローバルに利用可能です。明示的なインポート(VS Codeのオートコンプリート向上のため):
import 'zx/globals'
コアコンセプト
$`コマンド` — シェルコマンドの実行
タグ付きテンプレートリテラルがzxの中核です。${...}内のすべてが自動エスケープされ、引用符で囲まれます。
// 非同期(標準) — ProcessPromiseを返す
const output = await $`ls -la`
// 同期バリアント — ProcessOutputを直接返す
const dir = $.sync`pwd`
// 配列はフラット化されます
const flags = ['--oneline', '--decorate', '--color']
await $`git log ${flags}`
// ゼロ以外の終了コードはProcessOutputをスローします
try {
await $`exit 1`
} catch (p) {
console.log(`終了コード: ${p.exitCode}, エラー: ${p.stderr}`)
}
$({...}) によるプリセット設定
プリセットオプションを指定してカスタム$インスタンスを作成 — チェーン可能で合成可能:
const $$ = $({ verbose: false, env: { NODE_ENV: 'production' } })
const pwd = $$.sync`pwd`
// プリセットはチェーン可能
const $1 = $({ nothrow: true })
const $2 = $1({ sync: true }) // nothrow + sync の両方が適用
ProcessPromise と ProcessOutput
$`cmd` ProcessPromise (Promiseを拡張)
├── .pipe() ストリームパイピング
├── .kill() プロセス終了
├── .text() 文字列として出力
├── .json() パース済みJSONとして出力
├── .lines() 行で分割された出力
├── .nothrow() このコマンドのエラーを抑制
├── .quiet() このコマンドの出力を抑制
├── .timeout() 期間後に自動強制終了
├── .stdio() I/O設定
├── .exitCode Promise<終了コード>
├── .stdout 読み取り可能ストリーム
├── .stderr 読み取り可能ストリーム
├── .stdin 書き込み可能ストリーム
├── .pid / .cmd プロセスメタデータ
└── await → ProcessOutput
├── .stdout string
├── .stderr string
├── .exitCode number
├── .signal string|null
├── .text() / .json() / .lines() / .buffer() / .blob()
└── .ok boolean (nothrow時)
決定木
zxスクリプトを書く際は、この決定木を使用します:
| 目標 | アプローチ |
|---|---|
| コマンドを実行 | await $`cmd` |
| 同期的に実行 | $.sync`cmd` |
| 出力をパイプ | .pipe($`next`) / .pipe('file.txt') |
| エラーを適切に処理 | $({nothrow: true}) / .nothrow() |
| タイムアウト設定 | $({timeout: '30s'}) / .timeout('30s') |
| JSON出力をパース | (await $`cmd`).json() |
| リアルタイムストリーミング | for await (const line of $`cmd`) |
| 失敗時にリトライ | retry(5, () => $`cmd`) |
| ユーザープロンプト | question('名前: ') |
| 進捗インジケーター | await spinner('処理中...', () => $`cmd`) |
| ディレクトリ変更 | cd('/パス') または within(() => { $.cwd = '/tmp' }) |
| 一時ファイル/ディレクトリ | tmpfile() / tmpdir() |
| CLI引数をパース | argv.flag または minimist(process.argv.slice(2)) |
| .envファイルを読み込む | dotenv.config('.env') |
効果的なzxスクリプトの書き方
並列実行
const results = await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
nothrowを使ったエラーハンドリング
$.nothrow = true
const repos = ['zx', 'webpod']
const clones = repos.map(n => $`git clone https://github.com/google/${n}`)
const results = await Promise.all(clones)
const errors = results.filter(o => !o.ok).map(o => o.stderr.trim())
console.log('エラー:', errors.join('\n'))
ストリームパイピング
// bashパイプのようにコマンドをチェーン
const greeting = await $`printf "hello"`
.pipe($`awk '{printf $1", world!"}'`)
.pipe($`tr '[a-z]' '[A-Z]'`)
// ファイルにパイプ
await $`echo "Hello!"`.pipe('/tmp/output.txt')
// 端末へのリアルタイム出力
await $`echo 1; sleep 1; echo 2; sleep 1; echo 3`.pipe(process.stdout)
ストリームの分割とマージ
// 1つのソースを複数のコンシューマーに分割
const p = $`some-command`
const [o1, o2] = await Promise.all([
p.pipe`log`,
p.pipe`extract`,
])
// 複数のソースをマージ
const $h = $({ halt: true })
const p1 = $`echo foo`
const p2 = $h`echo a && sleep 0.1 && echo b`
const p3 = $h`echo c && sleep 0.1 && echo d`
const cat = $h`cat`
p1.pipe(cat); p2.pipe(cat); p3.pipe(cat)
await cat.run()
出力フォーマッター
const p = $`echo '{"foo":"bar"}\nline2'`
await p.json() // { foo: 'bar' }
await p.lines() // ['{"foo":"bar"}', 'line2']
await p.text() // '{"foo":"bar"}\nline2\n'
非同期イテレーション
for await (const line of $`git log --oneline --max-count=5`) {
console.log(line)
}
シェル設定
zxはデフォルトでbashを使用します。必要に応じてシェルを切り替えます:
import { useBash, usePowerShell, usePwsh } from 'zx'
usePowerShell() // PowerShell.exe
usePwsh() // PowerShell v7+
useBash() // bashに戻す
// 手動オーバーライド
$.shell = '/bin/zsh'
Windowsでは、bashサポートのためにWSLまたはGit Bashの使用を検討するか、usePowerShell()でPowerShellに切り替えます。
組み込みヘルパー一覧
| ヘルパー | 目的 | 例 |
|---|---|---|
cd() | ディレクトリ変更 | cd('/tmp') |
fetch() | HTTPリクエスト、.pipe()をサポート | fetch('https://api.example.com') |
question() | インタラクティブなユーザー入力 | question('名前: ') |
sleep() | 実行遅延 | await sleep(1000) |
echo() | 標準出力に表示 | echo`ステータス: ${p}` |
stdin() | 標準入力の読み込み | JSON.parse(await stdin()) |
within() | 分離された非同期設定コンテキスト | within(() => { $.cwd = '/tmp' }) |
retry() | 遅延/バックオフによるリトライ | retry(5, () => $curl url) |
spinner() | CLI進捗インジケーター | await spinner(() => $long-cmd) |
glob() | ファイルグロブマッチング (globby) | glob('**/*.js') |
which() | 実行可能ファイルのパス検索 | await which('node') |
ps | クロスプラットフォームプロセスリスト | ps.lookup({ command: 'node' }) |
tmpdir() | 一時ディレクトリ | tmpdir('sub') |
tmpfile() | 一時ファイル | tmpfile('f.txt', 'content') |
argv | パース済みCLI引数 | argv.verbose |
dotenv | .envファイル読み込み | dotenv.config('.env') |
公開されているnpmパッケージ: chalk (色), fs (fs-extra), os, path, YAML (yaml), minimist。
リソース
- references/api.md — 完全なAPIリファレンス:
$のオプション、cd()、fetch()、question()、sleep()、echo()、stdin()、within()、retry()、spinner()、glob()、which()、ps、kill()、tmpdir()、tmpfile()、minimist、argv、chalk、fs、os、path、YAML、dotenv、quote()、useBash()、usePowerShell()、usePwsh() - references/configuration.md — すべての
$.options:shell、prefix、postfix、quote、verbose、quiet、env、cwd、timeout、nothrow、detached、preferLocal、spawn、kill、log、input、signal、stdio、halt、delimiter、defaults - references/cli.md — CLIの使用法:フラグ、環境変数、Markdownスクリプト、リモートスクリプト、標準入力実行、REPLモード
- references/process.md — ProcessPromise/ProcessOutputのライフサイクル、パイピング、強制終了、中断、出力フォーマッター、ストリーム処理