読者です 読者をやめる 読者になる 読者になる

yunomuのブログ

酒とゲームと上から目線

Haskellでコマンドラインパーサを使う2

前回とは違うライブラリを使ってみます。
前回: Haskellでコマンドラインパーサを使う - yunomuのブログ

前回の記事ではcmdargsを使ったんですが、あれはあれで面倒くさいというか、あらかじめ定義したrecord型の通りにしかデータを取ることができないのがたまに面倒だったりとか。
第二引数があった時は第三引数が必須とか、逆に要らないとか、そういうのが面倒くさかったりします。
具体的にはgitみたいなのを作るのが面倒くさい。

ということで他になんかないものかと思って使ってみたのがparseargsというライブラリです。

ArgとかArgsとか似たような型が色々あってちょとややこしいですけど、要するにparseArgsIOに[Arg a]の配列を渡してあげればとりあえず使えそう。で、Arg aというのがコマンドライン引数の型とかオプション文字列とかdescriptionとかを持ったいわゆる「設定」で、"a"はハッシュのキーみたいなもののようだ。
なのでさしあたりaはIntということにするとこうなる。

module Main where
import System.Console.ParseArgs

options :: [Arg Int]
options =
    [Arg 1 Nothing Nothing (argDataRequired "abc" ArgtypeInt) "abc value"]

main :: IO ()
main = do
    a <- parseArgsIO ArgsComplete options
    putStrLn $ "progName: " ++ argsProgName a
    print (getArg a 1 :: Maybe Int)

これを実行するとこうなる。

% ./args
args: missing required argument <abc>
usage: args <abc>
  <abc>  abc value

DataArg(Argの4つ目の引数)を"argDataRequired"で作って渡したので、この引数はRequiredになっている。引数の名前が<abc>になってるのも、argDataRequiredに渡した文字列から取られているみたいですっていうか、マニュアルにそう書いてます。
で、なんか適当な引数を渡してみる。

% ./args text
progName: args
args: argument text to abc is not an int
usage: args <abc>
  <abc>  abc value

これは"text"って文字列を渡しているんですが、やっぱりargDataRequiredに渡してるArgtypeがArgtypeIntになっているので、Intとしてパースできる値を渡してあげないとエラーになる。

% ./args 1234
progName: args
Just 1234

Intとしてパースできる文字列を入れてあげるとこのようにうまくいきます。

ここでちょっと面白いのは、"text"って文字列を渡した時の動きで。これよく見ると、putStrLnで出力した文字列は先に出てるんですよね。
つまりこの「"text"はintじゃないからだめだよ」っていう感じのエラーメッセージは、parseArgsIOを実行した時ではなくて、そこで取ってきた値を使おうとした時、つまりgetArgした時に出力されているということなんです。まあこれは単にparseArgsIOの中でerror関数とか使って適当に戻り値を設定しているせいなんだと思いますけど。いや調べてませんけど。
まあこうしておいた方が、「この値が不正な時にどうしよう〜」っていう微妙な需要に対応できる可能性があって良いのかもしれません。どうやってやるのかわかりませんけど。

それはそうと、ArgtypeIntについて、パースできる型はInt, Integer, Float, Double, Stringとあるので、まあ十分でしょう。

Argの一つ目の引数については、最初あたりで書いたようにハッシュのキーみたいになっていて

    print (getArg a 1 :: Maybe Int)

のところの"1"で参照しています。もうちょっとわかりやすい値にした方がいいのかもしれません。

Argの2つ目と3つ目の引数はそれぞれショートオプションとロングオプションの指定になっていて、

    Arg 2 (Just 'f') (Just "flag") Nothing "flag"

みたいにすると、"-f"とか"--flag"を読み取ることができる。

この例の場合、4つ目の引数の、さっきargDataRequiredとかになってた部分がNothingになっていますけど、ここで書いたようにNothingにすると、gccでいうところの"-c"みたいに単体で何らかの意味のあるオプションになります。Just DataArgにすると"-o name"みたいなパラメータがついてるオプションになる。
そしてオプションが存在するかどうかはgotArgで調べられるので、

main = do
    (略)
    print $ gotArg a 2

とすると、"-f"がついていればTrueだし、ついてなければFalseになります。普通にgotArgは引数の存在確認として使えます。

argDataRequiredの代わりにargDataOptionalを使えば必須じゃない引数になりますし、argDataDefaultedを使えば引数が無かった時のデフォルト値を設定することもできる。
argsUsageでヘルプの表示もできる。
あとparseArgsIOに渡してるArgsCompleteは、だいたい何も考えない時はArgsCompleteでいいです。

というのをひと通りやってみたのがGitHubに上げてるこちらになります。
exercises/parseargs/Main.hs at master · yunomu/exercises · GitHub

前回紹介したcmdargsとの違いは、細かい分岐がやりやすいところかなぁ。いやrecordを読むかgetArgを読むか程度の違いしか無い気もしますけど。Requiredな引数を作るのがparseargsの方が簡単なような、うーん。
ヘルプを充実させたいならやっぱりcmdargsの方が強力だった気もしますが、私はどっちかというと今回のparseargsの方が好きかもなぁ。
という感じでした。