Getopt::Kingpinを使って、さくっとコマンドラインスクリプトを作成する

この記事は、[Perl 5 Advent Calendar 2016 - Qiita](http://qiita.com/advent-calendar/2016/perl5)の3日目の記事です。

昨日はnqounetさんの[Perl5で文字列操作](http://www.nishimiyahara.net/2016/12/01/231419)でした。
Perlの文字列操作はかなり強力なのを再認識できますね。

今日は、引数解析の拙作モジュール`Getopt::Kingpin`に関する記事です。

# Goal

`Getopt::Kingpin` を使ったコマンドラインスクリプトのサンプルを提示する。

# 背景

前の記事([perlスクリプトの引数処理モジュール`Getopt::Kingpin`の紹介](http://qiita.com/sago35/items/20d7d102048c1de9b441))で、モジュールを紹介しました。
今回は、実際の使用例／実装例を示したい。
ドキュメント／サンプルソースを充実させたいので、そのついでとしてという意味も。

# お題 ≒ 今回作るもの

`wc`コマンドを作成してみます。
https://infohost.nmt.edu/tcc/cgi/manweb.cgi?p=wc によると、ヘルプは以下のような感じ。
これをざっくりコピーする。

```
NAME
    wc - print the number of newlines, words, and bytes in files

SYNOPSIS
    wc [OPTION]... [FILE]...

DESCRIPTION
    Print newline, word, and byte counts for each FILE, and a total line if more than one FILE is specified.
    With no FILE, or when FILE is -, read standard input.

    -c, --bytes
        print the byte counts
    -m, --chars
        print the character counts
    -l, --lines
        print the newline counts
    -L, --max-line-length
        print the length of the longest line
    -w, --words
        print the word counts
    --help
        display this help and exit
    --version
        output version information and exit
```

# 作ってみる

## 基本

`Getopt::Kingpin`は、なるべくオリジナルであるgolang版に近い動き／記述ができるように作ってあります。
SYNOPSISに従い`Getopt::Kingpin->new()`して、`flag()`および`arg()`を追加していくのが基本となります。
それぞれ以下のようなイメージです。

 * new('script name', 'script description')
  * parserを生成します
 * flag('flag name', 'flag description')
  * `--words`や`-w`等の`-`から始まるフラグ(≒オプション)を設定する
 * arg('arg name', 'arg description')
  * 引数を表す(`flag`を除くもの(典型的には`-`から始まらないもの))

ここまで設定できたら`parse()`を実行し引数を処理します。

## もう少し詳細

例えば`$kingpin->flag('lines', 'print the newline counts')->short('l')->bool`であれば以下のイメージです。

 * flagの名前／指定の仕方は`--lines`
 * 詳細は`print the newline counts`
 * flagのshort_nameは`-l`
 * 型はbool (0 or 非0)

## 引数の解析

引数の解析部まで、以下に示します。

```perl
use Getopt::Kingpin;

my $kingpin = Getopt::Kingpin->new($0, 'print the number of newlines, words, and bytes in files');
my $flag = {
    lines           => $kingpin->flag('lines', 'print the newline counts')->short('l')->bool,
    words           => $kingpin->flag('words', 'print the word counts')->short('w')->bool,
    chars           => $kingpin->flag('chars', 'print the character counts')->short('m')->bool,
    bytes           => $kingpin->flag('bytes', 'print the byte counts')->short('c')->bool,
    max_line_length => $kingpin->flag('max-line-length', 'print the length of the longest line')->short('L')->bool,
};
my $files = $kingpin->arg('file', 'target file[s]')->required->existing_file_list;
$kingpin->version('0.02');
$kingpin->parse;
```

ここまで書いた時点でヘルプの実装はできていて、以下のように表示できます。

```
$ perl wc.pl --help
usage: wc.pl [<flags>] <file>

print the number of newlines, words, and bytes in files

Flags:
      --help             Show context-sensitive help.
  -l, --lines            print the newline counts
  -w, --words            print the word counts
  -m, --chars            print the character counts
  -c, --bytes            print the byte counts
  -L, --max-line-length  print the length of the longest line
      --version          Show application version.

Args:
  <file>  target file[s]
```

## 参考) golang版

うーん、そっくり(当たり前)。
実装もヘルプも、ほとんど同じになるように作っています。

```go
package main

import (
	"gopkg.in/alecthomas/kingpin.v2"
	"os"
)

var (
	app             = kingpin.New("wc", "print the number of newlines, words, and bytes in files")
	lines           = app.Flag("lines", "print the newline counts").Short('l').Bool()
	words           = app.Flag("words", "print the word counts").Short('w').Bool()
	chars           = app.Flag("chars", "print the character counts").Short('m').Bool()
	bytes           = app.Flag("bytes", "print the byte counts").Short('c').Bool()
	max_line_length = app.Flag("max-line-length", "print the length of the longest line").Short('L').Bool()
	files           = app.Arg("file", "target file[s]").Required().ExistingFiles()
)

func main() {
	app.Version("0.02")
	app.Parse(os.Args[1:])
}
```

```
usage: wc [<flags>] <file>...

print the number of newlines, words, and bytes in files

Flags:
      --help             Show context-sensitive help (also try --help-long and
                         --help-man).
  -l, --lines            print the newline counts
  -w, --words            print the word counts
  -m, --chars            print the character counts
  -c, --bytes            print the byte counts
  -L, --max-line-length  print the length of the longest line
      --version          Show application version.

Args:
  <file>  target file[s]
```

# スクリプト完成版

以下に完成版を置いてますので、適宜参照してください。

https://gist.github.com/sago35/76e26a4e3603dbc1438bbe2471e571df

## 注意点

以下のように`*_list`という名前の型を使うと、引数`file`を複数(0個以上)指定することができます。
また、`$files`は、`Getopt::Kingin::Argオブジェクト`のarray refとなります。

```perl
my $files = $kingpin->arg('file', 'target file[s]')->required->existing_file_list;
```

なので、使うときにはデリファレンスが必要で、以下のようにします。

```perl
my @files = @{$files->value};
```

このようにすると、`@files` は `Path::Tinyオブジェクトの配列`となります。
後は煮るなり焼くなりどうぞ。

＃この部分の作りは、もっといいやり方に直したいとは思っているが、いい方法が浮かばない


# まとめ

自分が作ったものを使っているので当たり前ですが、自分にとっては使いやすいです。
golang版kingpinに馴染んでいる人ならある程度違和感なく使えるかと思います。
ということで、是非使ってみてください。

 * https://metacpan.org/pod/Getopt::Kingpin