Chatopts のための node.js のコマンドオプションパーサーを選定する

## 背景

例えばSlackからChatoptsで何かを実行するときに、最初は正規表現マッチで単純な情報だけを抽出してという形で簡単にできるのですが、使っているうちにあれもやりたいこれもやりたいとなって、正規表現がどんどん複雑化していくともうメンテできない、ぐぬぬってなってしまいます。

要はCLIでコマンドを実行するくらい、複雑なことになりえるので、じゃあそれ用のコマンドラインのオプションパーサーを使いましょうという発想になります。

ところが、CLIのためのコマンドオプションパーサーは、Chatoptsでよく利用される常駐型のbotへのメッセージに対するパースでのユースケースが考慮されて作られているわけではないので、様々な問題があります。

例えば、間違ったオプションがついていたときに、自動でhelpを表示し、exitするようなインターフェースしかもたないライブラリがあります。これは、CLIでは親切な挙動かもしれないのですが、常駐型のbotでは余計なお世話になります。

## Chatoptsのためのbotからの要求

そこで、今回調査する項目ですが、

* 例題の要件を満たすコマンドラインパーサーが実現できること
* コマンドラインパーサーインスタンスがシングルトンではなく、いくらでも生成できること
* 間違ったオプションのパース時にexitしないこと
* helpがstdoutにではなく、文字列として取得できること

になります。

## 今回調べるライブラリ

ぐぐったり、npmjsで調べたりをしてnodeの一般的なコマンドラインパーサーを調べてみました。

* yargs
* commander
* minimist
* nopt
* meow
* nomnom
* coa


http://www.npmtrends.com/yargs-vs-commander-vs-minimist-vs-meow-vs-nomnom-vs-coa
みると、上位3つが少し他を引き離してますね。
とりあえず `yargs`,`commander`,`minimist`に関して調査してみます。

## 例題

`@bot create-todo <title> [remarks] --due <due> [--assign <tomorrow>]`

※ `[]`はオプションという意味です

というような感じのTODO作成コマンドをそれぞれのライブラリで上記の要求を満たしつつ実装できるかを調査します。

## yargs
 
### シングルトンではないパーサーでのパース

`require("yargs")` だけだと、シングルトンでパーサーインスタンスが生成されるので、
`require("yargs/yargs")()` します。
インスタンス作成時に、`require("yargs/yargs")(process.argv.slice(2))` して、最後に`argv`でパース結果を取得するやり方もあるのですが、
`.parse(process.argv.slice(2))`するほうが今回のユースケースにあってます。
`parse`の方は、ソースを読む限りは並列的にparseが走っても競合しないように見えます。

```js
const parseCallback = (exitError, parsed, output) => {
  if (exitError) {
    throw new Error(output)
  }
}

const parser =
  require('yargs/yargs')()
    .usage("Usage: <title> [remarks] [Options]")
    .option("due", {
      demandOption: true
    })
    .option("assign")
    .check(args => {
      if (args._.length < 1 || args._.length > 2) {
        return false
      } else {
        args.title  = args._[0]
        args.remark = args._[1]
        return true
      }
    })
    .version(false)

let args
try {
  args = parser
           .parse(["this is a title", "this is a remark", "--due", "tomorrow"], parseCallback)
} catch(err) {
  // parse error 時の対応を行う
}
console.log(args)

console.log(argv)
// { _: [ 'this is a title', 'this is a remark' ],
//   help: false,
//   due: 'tomorrow',
//   '$0': 'yargs.js',
//   title: 'this is a title',
//   remark: 'this is a remark',
//   assign: undefined }
```

arguments に関するAPIが見つからなかったので、`check`を使って無理やりチェックおよび、`argv`のpropertyに代入しています。
ただし、このcheckに対して、Human Readableなdescriptionをつけることができません。checkが失敗すると、error 時の output にコードが無慈悲に出力されます。。

```js
// ...snip...
           .parse(["--due", "tomorrow"], paseCallback)
} catch(err) => {
  console.error(err.message)
}

// Usage: <title> [remarks] [Options]
// 
// オプション:
//   --help  ヘルプを表示                                                    [真偽]
//   --due                                                                   [必須]
//
// 引数のチェックに失敗しました: args => {
//       if (args._.length < 1 || args._.length > 2) {
//         return false
//       } else {
//         args.title  = args._[0]
//         args.remark = args._[1]
//         return true
//       }
//     }
```

まぁHuman Reaableなfunctionを用意しておいて、 `.check( _ => titleShouldExistAndRemarksIsOptional(_))` とかってするくらいしかできないでしょうね。。

### 間違ったオプションのパース時にexitしないこと

`parseCallback`を`parse`で指定することにより、勝手にexitしなくなります。
`parseCallback`内で`exitError`があるときに、例外を投げることで、パースエラーをexitせずにハンドリングすることができます。

### help文字列を取得する

`showHelp`というメソッドは、名前の通りコンソールに出力しますので、有用ではありません。また、helpを表示する前に、parseを行おうとしますので、前回のパース結果によっては不必要な表示がされてしまうことがあります。

そこで、`--help` を明示的に `parse` するというワークアラウンドで解決できます。。

```js
const helpCallback = (exitError, parsed, output) => {
  throw new Error(output)
}

// ...snip...
           .parse(["--help"], helpCallback)
} catch(err) => {
  console.error(err.message)
}

// Usage: <title> [remarks] [Options]
// 
// オプション:
//   --help  ヘルプを表示                                                    [真偽]
//   --due                                                                   [必須]
```

`--help` をつけた場合はエラーではないので、`exitError`はないです。先程の`parseCallback`と混ぜてもいいと思います。

ただし、このhelpはwindowSizeが最大80までとして表示されてしまいます。
したがって、Slackに長いhelpが投稿されると、いろいろ折り返されてつらくなります。。
そういった場合は、parserに対して `.wrap(null)` とか設定してやると折り返しされなくなります

### yargs のまとめ

* 上記3つの要求は一応publicなAPIで対応できた
 * だが、例外処理機構を使わないといけないところがあるので若干きれいではない（特にhelp)
* argumentsに関するAPIが弱く、パーサーに関する宣言的な記述以外にユーザに示す情報を別で保持しないといけない

## commander

### シングルトンではないパーサーでのパース

`require("commander")`としてしまうと、シングルトンのパーサーが返ってくるので、`Command`クラスを取得して毎回newしてあげるとよいです。

```js
const Command = require("commander").Command
const program = new Command()

program
  .arguments('<title> [remarks]')
  .action((title, remarks, args) => {
    args.title   = title
    args.remarks = remarks
  })
  .option("--due <due>")
  .option("--assign [assign]")

const args = program
              .parse(["", "", "this is a title", "this is a remark", "--due", "tomorrow"])

if (args.args.length > 2 + 1 || typeof args.title === "undefined" || typeof args.due === "undefined") {
  // See the reason of +1: https://github.com/tj/commander.js/issues/386#issuecomment-94589519
  // parse error のハンドリングをする
}
console.log(args)
// Command {
//   commands: [],
//   options:
//    [ Option {
//        flags: '--due <due>',
//        required: -7,
//        optional: 0,
//        bool: true,
//        long: '--due',
//        description: '' },
//      Option {
//        flags: '--assign [assign]',
//        required: 0,
//        optional: -10,
//        bool: true,
//        long: '--assign',
//        description: '' } ],
//   _execs: {},
//   _allowUnknownOption: false,
//   _args:
//    [ { required: true, name: 'title', variadic: false },
//      { required: false, name: 'remarks', variadic: false } ],
//   _name: '',
//   _events:
//    { 'command:*': [Function: listener],
//      'option:due': [Function],
//      'option:assign': [Function] },
//   _eventsCount: 3,
//   rawArgs:
//    [ '',
//      '',
//      'this is a title',
//      'this is a remark',
//      '--due',
//      'tomorrow' ],
//   due: 'tomorrow',
//   args: [ 'this is a title', 'this is a remark', [Circular] ],
//   title: 'this is a title',
//   remarks: 'this is a remark' }
```

arguments に関しては、`yargs`と同様にparse後に自前での処理が必要になります。
また、`parse`の内部で引数を`slice(2)`するので、CLIからの入力を直接入れる場合は便利なのですが、こういう用途では、ダミーのデータをいらないといけないので見通しが悪くなります。

### 間違ったオプションのパース時にexitしないこと

上の例にあるように、実は `commander` は `<title>` というような形で必須パラメータは定義できるものの、バリデーションはしてくれませんので自前でする必要があります。

しかし一方で知らないオプションが渡ってきたときはエラーを吐きつつ無慈悲にexitしてくれます。そこで上の例のように `.allowUnknownOption(true)`をつけておくとexitはしなくなります。その場合、自前で知らないオプションがあったらエラーにするか、知らないオプションがあっても気にしないというような実装になってしまします。

個人的にはtypoが多いため明示的にフィードバックしてくれるとありがたいので、知らないオプションをわざわざエラーにする方向が好みです。
その場合、`commander`を利用すると、自前で実装することが多くなってしまい大変です。。

### help文字列を取得する

`commander`においてhelp文字列の取得は`helpInformation`というメソッドから取得できます。これは簡単ですね

```js
console.log(program.helpInformation())
// Usage:  [options] <title> [remarks]
//
//
// Options:
//
//   --due <due>
//   --assign [assign]
//   -h, --help         output usage information

```

### commanderのまとめ

* 上記3つの要求は素直に対応できた
 * ただし arguments にたいするaction周りにバグがある
* argumentsにかぎらずvalidationを自前で実装しないといけないので大変すぎる

`commander`は、arguments周りのバグ？があるのと、validationをやってくれないので自前での実装が多くなってしまうという特徴があります。でも一応機能は満たせています。

## minimist

`minimist`はそもそもものすごいシンプルなparserなので、parseしてから自前でなんとかやる感じになります。なので、結果的にまぁやりたいことはなんでもできるんだが、結局helpの文字列をつくるところまで含めて全部やる必要があるので、これ使うくらいだったらはじめから全部実装してもいいのでは感ありますw

## まとめ

どれもあわないけど、`yargs`が一番手数少なくやりたいことができそうだった