CLI のテンプレートプロジェクト by node and TypeScript

node で CLI(Command Line Interface) を開発する機会が数回あって、せっかくなのでテンプレートプロジェクトとしてまとめてみた。

## テンプレートプロジェクト

* [amay077/node-cli-sample: Sample npm project for development your CLI](https://github.com/amay077/node-cli-sample)

### 必要なモノ

* nodejs: v11.13.0+
* typescript: v3.7.3+

### 試し方

1. 上記のリポジトリを Clone する
2. リポジトリのディレクトリに cd して ``npm ci`` する
3. ``npm run build`` する
4. ``npm link`` する
5. ``source ~/.bash_profile`` を行うかまたはターミナルを再起動する

これでどのディレクトリでも ``my-great`` コマンドが使用できるようになる。

#### 例

```terminal
$ my-great hello -f Echizen -s Ooka -a 42

Hello Echizen Ooka.
You're 42 years old.
```

```terminal
$ my-great something wrong param

Command Line Interface for My great service

  Sample for CLI.

Commands

  my-great hello -f <first_name> -s <second_name>   Say Hello.
  my-great version                                  Show version.
```

### アンインストール

```
npm uninstall -g @amay/my-great-cli
```

## 要点

### コマンドライン引数の解析と使用方法の表示

* [command-line-args - npm](https://www.npmjs.com/package/command-line-args)
* [command-line-usage - npm](https://www.npmjs.com/package/command-line-usage)

[yargs](https://www.npmjs.com/package/yargs) とかいろいろあったけど、自分的に使いやすかったのでこれを選択。

## プログラム構成

```
my-great hello -f <first_name> -s <second_name>   Say Hello.
my-great version                                  Show version.
```

のように、第一引数を「コマンド」とし、第2引数以降をそのコマンド専用の引数群としたかったので、``index.ts`` で第一引数のみを parse して取得し、コマンド毎に ``command-xxxx.ts`` へ委譲している。

command-line-args では ``commandLineArgs(this.paramDef, { partial: true })`` と ``partial:true`` を設定すると、引数定義(``paramDef``) に存在しない引数があっても無視する。


### cli コマンド名

cliコマンド名 ``my-great`` は ``package.json`` の ``bin:`` で指定している。

**package.json**

```json
{
  "name": "@amay/my-great-cli",
＜省略＞
  "bin": {
    "my-great": "build/index.js"
  },
＜省略＞
```

ビルドされた ``./build/index.js`` を指すように設定している。
ちなみに npm run 経由で node を実行する場合は、引数の前に ``--`` を付ける（例: ``node ./build/index.js -- version``）。


### 必須引数のチェック

command-line-args では [引数の必須チェックを自力で行わなければならない](https://github.com/75lb/command-line-args/wiki/Validation) ようなので、定義体の ``paramDef`` に ``require: boolean`` を追加し、パースした実際の引数である ``XxxxConfig`` に ``require = true`` な項目が含まれているかをチェックするようにした。

```typescript
// Valid require params
const requiresNotSetted = this.paramDef
  .filter(x => x.require)
  .filter(x => cfg[x.name] == null)
  .map(x => `--${x.name}`);

if (requiresNotSetted.length > 0) {
  console.log(`Param: ${requiresNotSetted.join(' ')} is required.`);
  console.log(`------------------------------------`);
  this.usage[1].optionList = this.paramDef;
  const usg = commandLineUsage(this.usage)
  console.log(usg);  
  return -1;
}
```

### kebab-case VS camelCase VS  snake_case

コマンドの引数は kebab-case がデファクトスタンダードの模様。
command-line-args では ``commandLineArgs(this.paramDef, { camelCase: true })`` とすると、``--first-name`` に渡された引数を、``firstName`` 変数に格納してくれる。

が、前述の必須引数のチェックが(定義体と実体の変数名が異なるため)正しく機能しなくなるので妥協案として snake_case の ``--first_name`` を採用している。


### コマンドを追加するには

1. ``index.ts`` の ``CommandType`` に ``xxxx`` を増やす
2. ``command-xxx.ts``(``CommandXxxx`` クラス) を作る
3. ``index.ts`` の ``commandMap`` に追加する
4. ``mainUsage`` になんか書く

## 参考

* [Node.jsでのCLIの作り方と便利なライブラリまとめ - Qiita](https://qiita.com/toshi-toma/items/ea76b8894e7771d47e10)
* [npm runでコマンドライン引数を渡す方法 - Qiita](https://qiita.com/qrusadorz/items/db042f65be95f34d6271)