背景
例えば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が走っても競合しないように見えます。
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 にコードが無慈悲に出力されます。。
// ...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
するというワークアラウンドで解決できます。。
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してあげるとよいです。
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
というメソッドから取得できます。これは簡単ですね
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
が一番手数少なくやりたいことができそうだった