👀

Node.jsによるCLI開発のススメ ~watchモードを添えて~

こんにちは、エンジニアの籏野です。

近年のフォルシアのアプリ開発は Node.js を中心としたエコシステムの恩恵を多く受けながら行われています。
モジュールとしてアプリで利用するだけでなく、便利な CLI などもたくさん存在しており、ちょっとした困りごとが簡単に解決できる場面も多いです。
今回は開発を進める中で CLI を自作しようかなと思う場面があり、基本的な CLI の作り方について調べました。
よく見る「watch モード」も搭載してみましたので紹介します。

利用するモジュール

今回は以下の 3 つのモジュールを利用しました。
※ TypeScript を利用していますので、その他必要なモジュールは適宜インストールしてください。

  • commander
    • CLI を簡単に作れます
  • chokidar
    • watch モードの実装に利用します。
  • valibot
    • コマンドラインで指定されたオプションのバリデーションと型付けに利用します。
    • zodなどお好みのものでいいと思いますが、今回筆者が試しに使ってみたくて利用します。

commander で CLI を作る

開発をした CLI を使うときにエントリーポイントとなるスクリプトを作成します。
commanderを使うことでお手軽に実装することができました。

以下のように -t(--text)で指定した文字列をそのまま出力してみます。

cli.ts
import { Command } from "commander";

const program = new Command();
program
  .name("cli-test")
  .description("自作のCLIです")
  .version(`v1.0.0`)
  .option("-t, --text <text>", "テキストを指定する")
  .action(async (options) => {
    console.log(options.text);
  });
program.parse();

CLI に渡すオプションは、.options()で指定した引数名でパースされ action に渡されていることがわかります。
このスクリプトをtsxで実行することで以下のような結果が得られました。

$ npx tsx ./cli.ts -t Hello
Hello

バージョン表示やヘルプの表示にも対応しています。

$ npx tsx ./cli.ts --version
v1.0.0
$ npx tsx ./cli.ts --help
Usage: cli-test [options]

自作のCLIです

Options:
  -V, --version      output the version number
  -t, --text <text>  テキストを指定する
  -h, --help         display help for command

引数に型を付ける

このままだと action メソッド内の options 変数の型が anyになってしまい、普段型に守られた開発をしている身としてはかなり不安になってきます。
そこでvalibotのようなバリデーションライブラリを利用することで、引数に対して型を付けることにしました。

cli.ts
import { Command } from "commander";

import { object, string, Output, parse, enum_ } from "valibot";

enum UserEnum {
  world = "world",
}

const optionSchema = object({
  text: string(),
  user: enum_(UserEnum),
});

const program = new Command();
program
  .name("cli-test")
  .description("自作のCLIです")
  .version(`v1.0.0`)
  .option("-t, --text <text>", "テキストを指定する")
  .option("-u, --user <user>", "ユーザー名")
  .action(async (options) => {
    const parsedOptions = parse(optionSchema, options); // { text: string, user: UserEnum } になる
    console.log(`${parsedOptions.text}, ${parsedOptions.user}!`);
  });
program.parse();

上記により、意図せぬ値が入ってきたときにはエラーにもなってくれます。

$ npx tsx ./cli.ts -t Hello -u world
Hello, world!
$ npx tsx ./cli.ts -t Hello -u worlddddd
ValiError: Invalid type

watch モードの実装

chokidarを利用すると、watch モードも手軽に実装できます。

cli.ts
import { readFileSync } from "fs";
import chokidar from "chokidar";
import { Command } from "commander";

import { object, string, Output, parse, boolean, optional } from "valibot";

const optionSchema = object({
  path: string(),
  watch: optional(boolean()),
});

type Options = Output<typeof optionSchema>;

const program = new Command();
program
  .name("cli-test")
  .description("自作のCLIです")
  .version(`v1.0.0`)
  .option("-p, --path <path>", "読み込むファイル")
  .option("-w, --watch", "watch mode")
  .action(async (options) => {
    const parsedOptions = parse(optionSchema, options);
    main(parsedOptions);
    if (parsedOptions.watch) {
      // 指定されたパス以下のファイルを監視するためのwatcherを作成
      const watcher = chokidar.watch(parsedOptions.path);
      watcher.on("ready", () => {
        console.log("...waiting...");
        // watcherの準備完了後、ファイルの変更を監視する
        watcher.on("all", (event, path) => {
          console.log(event, path);
          main(parsedOptions);
        });
      });
    }
  });
program.parse();

function main(options: Options) {
  const text = readFileSync(options.path, { encoding: "utf-8" });
  console.log(text);
}

test.txt というファイルを用意して、CLI に読み込ませてみます。

$ npx tsx ./cli.ts -p ./test.txt
Hello

今度は watch モードを利用してみます。

$ npx tsx ./cli.ts -p ./test.txt --watch
Hello
...waiting...
# ここでtest.txtを更新する
change test.txt # console.log(event, path)の出力
Hello, world! # console.log(text)の出力

まとめ

今回は CLI を作るための基本的な部分を紹介しました。
筆者は上記のような構成をベースに必要なツールを作っています。
開発時に少し不便だなと感じてもそのままにしてしまうことも多々あるかと思いますが、一度ツールを作ってしまうことでその後の開発効率が爆上がりすることも多いのではないでしょうか。

今回紹介した方法も生かして、皆様により快適な開発ライフが訪れますように!

この記事を書いた人

籏野 拓
2018 年新卒入社

今年もいちごのタルトをつくりました。

FORCIA Tech Blog

Discussion