🖥️

タイプセーフなNode.js CLIアプリケーションを簡単に作成する

2024/05/08に公開

導入

Node.jsで単純なCLIアプリケーションを作るのはそれほど難しくありませんが、引数やオプション、サブコマンドなどが増えてくると構築や保守が大変になってきます。

特に問題なのが入力値の型安全性です。
フラグオプションと引数オプションの違いや、サブコマンドごとに異なるオプションなど考慮すべき事項は多くあります。

さらにCLIと同じ機能を持つAPIを用意しようと思った場合には、追加のボイラーテンプレートが必要になります。

そこで今回は、このような複雑な機能を持ったCLIアプリケーションをタイプセーフで宣言的に簡単に作れるライブラリ@jill64/ts-cliを紹介します。

https://github.com/jill64/ts-cli#readme

なお、JavaScriptでも利用することはできますが、型安全性を最大限に生かすために今回のチュートリアルではTypeScriptを使用します。

使い方

まずは以下のコマンドで依存関係をインストールします。

npm i @jill64/ts-cli

最小構成

App@jill64/ts-cliからimportして使用します。

app.ts
import { App } from '@jill64/ts-cli'
import process from 'node:process'

const app = new App({}, () => {
  console.log('Hello, World!')
})

app.run(process.argv)

new Appの第一引数には後程、引数やオプションなどの情報を入力します。
まずはこの状態で実行してみましょう。

tsxを使用するとTypeScriptファイルをNodeで直接実行できます。

npx tsx app.ts

以下の結果が出力されます。

Hello, World!

単純に第二引数で渡した関数が実行されています。

引数

では次にコマンドライン引数を追加してみます。
以下のようにargプロパティを追加するようにapp.tsを書き換えます。

app.ts
import { App } from '@jill64/ts-cli'
import process from 'node:process'

const app = new App(
  {
    args: [
        [
            'foo', // 引数名
            'First Argument Description' // 引数の説明
        ]
    ] as const
  },
  ({ args: { foo } }) => {
    console.log(`Hello, ${foo}!`)
  }
)

app.run(process.argv)

第二引数でプロパティargsが利用可能になります。
この時、VSCodeであれば型補完が効いてfooだけが、argsのプロパティとして選択可能になっています。

args-1

まずは試しに引数なしで実行してみます。

npx tsx app.ts

以下のエラーが出ます。

Error: Missing required argument: foo

必須の引数fooを渡さなかったのでエラーが出ました。

では引数を指定して実行してみます。

npx tsx app.ts bar

引数の値を反映させることに成功しました。

Hello, bar!

さらに引数の数を増やします。
配列の順番が引数の順番と一致します。

app.ts
import { App } from '@jill64/ts-cli'
import process from 'node:process'

const app = new App(
  {
    args: [
      ['arg1', 'First Argument Description'],
      ['arg2', 'Second Argument Description'],
      ['arg3', 'Third Argument Description']
    ] as const
  },
  ({ args: { arg1, arg2, arg3 } }) => {
    console.log(`${arg1} - ${arg2} - ${arg3}`)
  }
)

app.run(process.argv)

引数を指定して実行します。

npx tsx app.ts hoge fuga piyo
hoge - fuga - piyo

このように引数の数が増えてもプロパティ名でタイプセーフに引数にアクセスできることがこのライブラリの強みです。

オプショナル引数

optionalプロパティを使用して、含めるかどうか任意の引数を定義することもできます。

app.ts
import { App } from '@jill64/ts-cli'
import process from 'node:process'

const app = new App(
  {
    optional: [
      ['optional_arg',  'Optional argument'],
    ] as const
  },
  ({ optional }) => {
    console.log(`${optional?.optional_arg ?? 'Fallback value'}!`)
  }
)

app.run(process.argv)

まずは引数ありで実行します。

npx tsx app.ts arg_str

引数の値が使用されます。

arg_str!

次に引数なしで実行します。

npx tsx app.ts

引数なしの場合はこのようにundefinedが返され、この場合フォールバック値が使用されます。

Fallback value!

これはargsプロパティと組み合わせることも可能です。

オプション

optionsプロパティを使用して、以下のいずれかの型のオプションを定義することができます。

  • boolean
  • boolean[]
  • string
  • string[]

optionsプロパティはロング名をキーとするオブジェクトで定義されます。
以下に例を示します。

app.ts
import { App } from '@jill64/ts-cli'
import process from 'node:process'

const app = new App(
  {
    options: {
      help: {
        type: 'boolean', // オプションの型
        alias: 'h', // ショート名
        description: 'Show help message' // オプションの説明
      },
      out: {
        type: 'string',
        alias: 'o',
        description: 'Output directory',
      },
      values: {
        type: 'string[]',
        alias: 'v',
        description: 'Values to be printed'
      }
    }
  },
  ({ options }) => {
    if (options?.help) {
      console.log('Help message')
      return
    }

    console.log(`Output Dir: ${options?.out}`)

    console.log(options?.values?.join(','))
  }
)

app.run(process.argv)

まずはオプションなしの場合

npx tsx app.ts

未指定のオプションはundefinedが返ります。

Output Dir: undefined
undefined

--helpフラグを指定してみます。

npx tsx app.ts --help

Help messageが出力されます。

Help message

複数のオプションを同時に指定することが可能です。
GNUガイドラインに従ったショート名でのオプション指定も可能です。

npx tsx app.ts -o dist --values 1 --values 2 --values=3
Output Dir: dist
1,2,3

restパラメーター

例えば子プロセスを起動して任意のコマンドを実行させたい場合、任意個の引数を受け取りたいことがあります。
この場合、restパラメーターを使うことで、これを実現できます。

app.ts
import { App } from '@jill64/ts-cli'
import process from 'node:process'

const app = new App(
  {
    rest: {
      placeholder: 'command', // restパラメーターの名前
      description: 'Command to run' // restパラメーターの説明
    }
  },
  ({ rest }) => {
    console.log('RUN: ',rest?.join(' '))
  }
)

app.run(process.argv)

npx tsx app.ts sub_command --flag
RUN: sub_command --flag

なお、optionsrestを併用する場合は、optionsのパラメーターはrestの前に配置する必要があります。

import { App } from '@jill64/ts-cli'
import process from 'node:process'

const app = new App(
  {
    options: {
      quiet: {
        alias: 'q',
        description: 'No output',
        type: 'boolean'
      }
    },
    rest: {
      placeholder: 'command',
      description: 'Command to run'
    }
  },
  ({ rest, options }) => {
    if (options?.quiet) {
      return
    }

    console.log('RUN: ', rest?.join(' '))
  }
)

app.run(process.argv)

npx tsx app.ts -q sub_command --flag
(出力なし)

npx tsx app.ts sub_command -q --flag
RUN:  sub_command -q --flag

これもargsoptionalと組み合わせることができます。

まとめ

いかがだったでしょうか。
この記事がNode.jsでCLIツールを作成しようとしている方の助けになれば幸いです。

また@jill64/ts-cliについてバグや不明点がありましたらぜひ以下からIssueを開いてください。

https://github.com/jill64/ts-cli/issues

Discussion