zx の使い方 — JavaScriptでシェルスクリプトをもっと快適に書く
一言でいうと
zx は、Google が開発した「JavaScript/TypeScript でシェルスクリプトを書く」ためのツールです。child_process のラッパーとして、テンプレートリテラルでシェルコマンドを直感的に実行でき、bash スクリプトの代替として本格的に使えます。
どんな時に使う?
- 複雑な bash スクリプトを書きたくない時 — 条件分岐やエラーハンドリングを JavaScript の構文で書けるため、可読性・保守性が大幅に向上します。
- CI/CD パイプラインやデプロイスクリプトの構築 — ファイル操作、Git 操作、Docker コマンドなどを組み合わせた自動化スクリプトを型安全に書けます。
- 開発ツールやタスクランナーの自作 — プロジェクト固有のビルド手順やデータ移行スクリプトを、チームメンバーが読みやすい JavaScript で記述できます。
インストール
# npm
npm install zx
# yarn
yarn add zx
# pnpm
pnpm add zx
# グローバルインストール(スクリプト単体で使う場合に便利)
npm install -g zx
Note: Node.js 18 以上が必要です(v8 系)。
基本的な使い方
ファイルを作成して実行する
script.mjs(拡張子は .mjs または .ts):
#!/usr/bin/env zx
// シェルコマンドをテンプレートリテラルで実行
const branch = await $`git branch --show-current`;
console.log(`現在のブランチ: ${branch.stdout.trim()}`);
// 複数コマンドの連続実行
await $`echo "Hello from zx!"`;
await $`ls -la`;
実行方法:
# zx コマンドで実行
npx zx script.mjs
# 実行権限を付与して直接実行(shebang あり)
chmod +x script.mjs
./script.mjs
TypeScript で書く場合
script.ts:
#!/usr/bin/env zx
import { $, question, chalk, fs, path } from 'zx';
const name = await question('プロジェクト名を入力してください: ');
await $`mkdir -p ${name}`;
console.log(chalk.green(`✔ ディレクトリ "${name}" を作成しました`));
npx zx script.ts
よく使う API
1. $ — コマンド実行(最も基本)
import { $ } from 'zx';
// 基本実行
const result = await $`echo "Hello, World!"`;
console.log(result.stdout); // "Hello, World!\n"
console.log(result.exitCode); // 0
// 変数の埋め込み(自動でエスケープされる)
const filename = 'my file with spaces.txt';
await $`touch ${filename}`;
// 複数の引数を配列で渡す
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
await $`ls -la ${files}`;
2. $.pipe / パイプ処理
import { $ } from 'zx';
// パイプでコマンドを繋ぐ
const result = await $`cat package.json`.pipe($`grep "name"`);
console.log(result.stdout);
// 複数段のパイプ
const count = await $`ps aux`
.pipe($`grep node`)
.pipe($`wc -l`);
console.log(`Node プロセス数: ${count.stdout.trim()}`);
3. question — 対話的な入力
import { question } from 'zx';
const answer = await question('デプロイ先を選択 (staging/production): ');
if (answer === 'production') {
const confirm = await question('本当に本番にデプロイしますか? (y/N): ');
if (confirm.toLowerCase() !== 'y') {
console.log('キャンセルしました');
process.exit(0);
}
}
await $`deploy --env ${answer}`;
4. cd — ディレクトリ移動
import { $, cd } from 'zx';
cd('/tmp');
await $`pwd`; // /tmp
cd('/home/user/project');
await $`ls -la`;
// 元のディレクトリに戻る必要はない(プロセス終了時に自動で戻る)
5. within — スコープ付きコンテキスト
import { $, cd, within } from 'zx';
// within 内の cd や設定変更は外に影響しない
await within(async () => {
cd('/tmp');
$.verbose = false;
await $`pwd`; // /tmp(ログ出力なし)
});
// ここでは元のディレクトリのまま
await $`pwd`; // 元のディレクトリ
6. 組み込みユーティリティ
zx は便利なパッケージを再エクスポートしています。追加インストール不要で使えます。
import { chalk, fs, path, glob, YAML } from 'zx';
// chalk — カラー出力
console.log(chalk.red.bold('エラー!'));
console.log(chalk.blue('情報メッセージ'));
// fs (fs-extra) — ファイル操作
await fs.ensureDir('./dist');
await fs.copy('./src/config.json', './dist/config.json');
const pkg = await fs.readJson('./package.json');
// glob — ファイル検索
const tsFiles = await glob('src/**/*.ts');
console.log(`TypeScript ファイル数: ${tsFiles.length}`);
// path — パス操作
const fullPath = path.resolve('./src', 'index.ts');
エラーハンドリング
import { $ } from 'zx';
// デフォルトでは非ゼロの終了コードで例外がスローされる
try {
await $`exit 1`;
} catch (error) {
console.error(`コマンド失敗: ${error.message}`);
console.error(`終了コード: ${error.exitCode}`);
console.error(`stderr: ${error.stderr}`);
}
// nothrow() でエラーを無視する
const result = await $`grep "not-found" somefile.txt`.nothrow();
if (result.exitCode !== 0) {
console.log('パターンが見つかりませんでした');
}
// quiet() でコマンド出力を抑制
await $`npm install`.quiet();
便利な設定オプション
import { $ } from 'zx';
// コマンドのログ出力を抑制
$.verbose = false;
// デフォルトのシェルを変更
$.shell = '/bin/bash';
// タイムアウト設定(ミリ秒)
$.timeout = 30_000; // 30秒
// 環境変数の設定
$.env = { ...process.env, NODE_ENV: 'production' };
// プレフィックスを付ける(set -e 相当など)
$.prefix = 'set -e; set -o pipefail;';
類似パッケージとの比較
| 特徴 | zx | shelljs | execa |
|---|---|---|---|
| テンプレートリテラル構文 | ✅ | ❌ | ✅(v9+) |
| TypeScript サポート | ✅ | △(型定義あり) | ✅ |
| 組み込みユーティリティ(chalk, fs等) | ✅ 豊富 | ❌ | ❌ |
| 対話的入力 (question) | ✅ | ❌ | ❌ |
| パイプ | ✅ | ✅ | ✅ |
| shebang でスクリプト実行 | ✅ | ❌ | ❌ |
| 軽量さ | △(依存多め) | ✅ | ✅ |
| 開発元 | 個人 | sindresorhus | |
| 主な用途 | スクリプト全般 | シェルコマンド代替 | プロセス実行 |
- execa — プロセス実行に特化。ライブラリとしてアプリに組み込む場合に最適。
- shelljs — Unix コマンドを JavaScript 関数として提供。async/await 非対応の部分あり。
- zx — スクリプトファイルとして独立実行する用途に最適。「bash の代替」として最も完成度が高い。
注意点・Tips
1. 変数の自動エスケープを理解する
// ✅ zx が自動でシェルエスケープしてくれる
const userInput = 'hello; rm -rf /';
await $`echo ${userInput}`; // 安全。"hello; rm -rf /" がそのまま文字列として出力される
// ⚠️ 意図的にエスケープを無効にしたい場合
const flags = '--verbose --dry-run';
await $({ raw: true })`echo ${flags}`; // raw モードで展開
2. $.verbose はデバッグ時に活用する
// 開発中は true にしてコマンドの実行内容を確認
$.verbose = true;
// 本番スクリプトでは false にして出力をクリーンに
$.verbose = false;
3. 並列実行で高速化
// Promise.all で並列実行
const [branch, status, log] = await Promise.all([
$`git branch --show-current`,
$`git status --porcelain`,
$`git log --oneline -5`,
]);
4. .mjs 拡張子に注意
package.json に "type": "module" がないプロジェクトでは、.mjs 拡張子を使うか、.ts で書いて zx に実行させてください。zx v8 は ESM 前提です。
5. process.argv でスクリプト引数を受け取る
#!/usr/bin/env zx
// npx zx script.mjs --env production --dry-run
const args = process.argv.slice(3); // zx 経由の場合、先頭3つをスキップ
console.log(args);
// minimist で解析する場合
import { minimist } from 'zx';
const opts = minimist(process.argv.slice(3));
console.log(opts.env); // "production"
console.log(opts['dry-run']); // true
まとめ
zx は、bash スクリプトの「書きにくさ」「読みにくさ」「エラーハンドリングの難しさ」を JavaScript/TypeScript の力で解決するツールです。テンプレートリテラルによる直感的なコマンド実行、自動エスケープによるセキュリティ、chalk や fs-extra などの組み込みユーティリティにより、開発・運用スクリプトの生産性が大幅に向上します。
シェルスクリプトに少しでも不満を感じているなら、次のスクリプトから zx を試してみてください。