NodeでCLIツール作成時にCommander.jsを使うと便利だった話


NodeでCLIツールを作る際に [Commander.js](https://github.com/tj/commander.js) を使うとよく使うオプションコマンドの指定など容易に設定できて重宝したのでメモ。

# 参考

+ [Node.jsを使ってCommand line ツールことはじめ](http://mid0111.hatenablog.com/entry/2014/06/08/153613)
+ [tj/commander.js](https://github.com/tj/commander.js)
+ [Commander.js](https://tj.github.io/commander.js/)

# できること

+ `--version`の指定
+ オプションの指定。`-v`と`--verbose`などのように一文字のものと複数文字列の指定も可能
+ オプションに従い、自動で`--help`の文言を作成
+ オプションの指定時に引数も指定できる。`--id 1`など。`-l 1,2,3`のようにリストなどの引数も取得できる
+ オプション引数の任意指定、必須指定
+ オプション引数のデフォルト値の設定
+ 正規表現によるオプション引数の確認

他にも色々できそう(gitみたいにサブコマンドを作るなど）できそうですが、今回はそこは未確認です。時間があれば別途。

# 準備

```bash
$npm install commander
```

# オプションの設定

`.option()`メソッドを使うことでCLIのオプションの定義ができます。
定義したオプションの内容は自動でhelpの際に表示ができます

```index.js
#!/usr/bin/env node

/**
 * Module dependencies.
 */

var program = require('commander');

program
  .version('0.0.1')
  .option('-p, --peppers', 'Add peppers')
  .option('-P, --pineapple', 'Add pineapple')
  .option('-b, --bbq-sauce', 'Add bbq sauce')
  .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
  .parse(process.argv);

console.log('you ordered a pizza with:');
if (program.peppers) console.log('  - peppers');
if (program.pineapple) console.log('  - pineapple');
if (program.bbqSauce) console.log('  - bbq');
console.log('  - %s cheese', program.cheese);
```

使うときはこんな感じ。

```bash
$node index.js --version
0.0.1

$node index.js --help

  Usage: index [options]

  Options:

    -h, --help           output usage information
    -V, --version        output the version number
    -p, --peppers        Add peppers
    -P, --pineapple      Add pineapple
    -b, --bbq-sauce      Add bbq sauce
    -c, --cheese [type]  Add the specified type of cheese [marble]

# --cheseについてはデフォルト値が設定されている。optionメソッドの第3引数に指定する
$node index.js 
you ordered a pizza with:
  - marble cheese

# -pオプションと--pepersオプションは同じ意味。 
$node index.js --peppers
you ordered a pizza with:
  - peppers
  - marble cheese
  
$node index.js -p
you ordered a pizza with:
  - peppers
  - marble cheese
```

# オプションの引数の指定

以下、公式のサンプルコード。

```index.js
#!/usr/bin/env node

/**
 * Module dependencies.
 */

var program = require('commander');

function range(val) {
  return val.split('..').map(Number);
}

function list(val) {
  return val.split(',');
}

function collect(val, memo) {
  memo.push(val);
  return memo;
}

function increaseVerbosity(v, total) {
  return total + 1;
}

program
  .version('0.0.1')
  .usage('[options] <file ...>')
  .option('-i, --integer <n>', 'An integer argument', parseInt)
  .option('-f, --float <n>', 'A float argument', parseFloat)
  .option('-r, --range <a>..<b>', 'A range', range)
  .option('-l, --list <items>', 'A list', list)
  .option('-o, --optional [value]', 'An optional value')
  .option('-c, --collect [value]', 'A repeatable value', collect, [])
  .option('-v, --verbose', 'A value that can be increased', increaseVerbosity, 0)
  .parse(process.argv);

console.log(' int: %j', program.integer);
console.log(' float: %j', program.float);
console.log(' optional: %j', program.optional);
program.range = program.range || [];
console.log(' range: %j..%j', program.range[0], program.range[1]);
console.log(' list: %j', program.list);
console.log(' collect: %j', program.collect);
console.log(' verbosity: %j', program.verbose);
console.log(' args: %j', program.args);
```

オプション引数の必須指定、任意指定の設定ができる。

```bash
# optionメソッドの第２引数で<n>というように<>と指定するとオプションの引数が必須となる
$node index.js -i

  error: option `-i, --integer <n>' argument missing
```

```bash
# [value]というように[]とするとオプションの引数は任意
$node index.js -o
 int: undefined
 float: undefined
 optional: true
 range: undefined..undefined
 list: undefined
 collect: []
 verbosity: undefined
 args: []
 
$node index.js -o optional
 int: undefined
 float: undefined
 optional: "optional"
 range: undefined..undefined
 list: undefined
 collect: []
 verbosity: undefined
 args: []
```

オプション引数で様々な型の値を取得できる

```bash
# optionメソッドの第3引数にfunctionを設定した場合、その関数が実行される.intの場合にはparseInt
$node index.js -i 1
 int: 1
 float: undefined
 optional: undefined
 range: undefined..undefined
 list: undefined
 collect: []
 verbosity: undefined
 args: []

# parseIntによってintのみ許容
$node index.js -i hoge
 int: null
 float: undefined
 optional: undefined
 range: undefined..undefined
 list: undefined
 collect: []
 verbosity: undefined
 args: []
 
# カンマ区切りのリスト.これは自分でlist関数を作ってそれを利用
$node index.js -l 1,2,3
 int: undefined
 float: undefined
 optional: undefined
 range: undefined..undefined
 list: ["1","2","3"]
 collect: []
 verbosity: undefined
 args: []
 
# collectには何も指定しなくても[]が設定されている。第3引数に関数を指定している場合には、第4引数がデフォルト値となる
$node index.js
 int: undefined
 float: undefined
 optional: undefined
 range: undefined..undefined
 list: undefined
 collect: []
 verbosity: undefined
 args: []
```

# オプション引数の正規表現確認

```index.js
program
  .version('0.0.1')
  .option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')
  .option('-d --drink [drink]', 'Drink', /^(coke|pepsi|izze)$/i)
  .option('-i --id [id]', 'SecurityGroupId', /^sg-[0-9a-z]*$/i)
  .parse(process.argv);

console.log(' size: %j', program.size);
console.log(' drink: %j', program.drink);
console.log(' id: %j', program.id);
```

確認。

```bash
# 正規表現で指定されているもののみ指定可能
$node index.js -s large
 size: "large"
 drink: undefined
 
# 正規表現に合致しないのでデフォルト値が設定される
$node index.js -s lar
 size: "medium"
 drink: undefined
 
# 内部的にRegExp.execを結果の配列の[0]を返却しているのでなんでもできる
$node index.js --id sg-abc
 size: "medium"
 drink: undefined
 id: "sg-abc"
 
# 合致しないとtrueが返る.undefinedかtrueならNGとするなどできる
$node index.js --id s-aaaa
 size: "medium"
 drink: undefined
 id: true
```