Edited at
HaskellDay 13

optparse-applicativeとoptparse-simple入門

More than 1 year has passed since last update.

どうも。最近、Node.jsのCUIツール作りながら、「辛え、Haskellで書きたい、Haskell、Haskell書かしてくれ・・・・・・!」って感じの薬物中毒に陥ってる、弱いHaskellerです。まあ、Haskellに戻ったら戻ったで、辛いって言ってる気がするし、そういう性分なんでしょうね。

ということで、今日はCUIツールの基本、オプションパーサーについて書きたいと思います。


Haskellのオプションパーサ事情

Haskellのオプションパーサーは結構いろいろあります。だいたい、ここに載ってますが、主なものをちょっと紹介しておきましょう。



  • GetOpt: baseにあるやつ。弱い


  • options: GetOptよりちょっと強いけど弱い


  • optparse-declarative: tanakhさんの作った型レベルのやつ。強いけど、Optionをまとめるのが辛いのと、カスタムパーサ書きにくいのとで諦めた


  • cmdargs: それなりに使いやすいが、癖が強い


  • optparse-applicative: 大体みんな使ってるやつ

まあ、大体こんな感じですね。

多分現状は、Hackage見る限りcmdargsとoptparse-applicativeの二強っぽいです。

僕はoptparse-applicativeが一番使いやすいと思うので、その簡単な紹介をしたいと思います。


簡単な使い方

正直大体は、philoponさんの記事読めば分かります。でもそれじゃ元も子もないんで、いちよ紹介しときます。

optparse-applicativeの基本要素は二つです。ParserとParserInfoです。あとコマンドもあるんですが、これは後ほど触れるので今はとりあえずParser/ParserInfoの二つが基本だと思っておいてください。


  • Parser: オプションパーサの本体。コマンドのオプションをアプリカティブで構築していけるやつです

  • ParserInfo: Parserにヘルプの表示方法などのカスタム情報を付随させたデータです

という感じですね。optparse-applicativeを使う流れとしては、


  1. Parserを作る

  2. 作ったParserから幾つか表示方法を設定してParserInfoを作る

  3. ParserInfoに引数を食わせて、結果を得る

となります。まあ、正直触ってみた方が早いと思うので触ってみましょう。

module Main where

import Options.Applicative

data Sample = Sample
{ hello :: String
, quiet :: Bool
} deriving (Eq, Show)

sample :: Parser Sample
sample = Sample
<$> helloOption
<*> quietOption
where
helloOption :: Parser String
helloOption = strOption
$ long "hello"
<> metavar "TARGET"
<> help "Target for the greeting"

quietOption :: Parser Bool
quietOption = switch
$ long "quiet"
<> short 'q'
<> help "Whether to be quiet"

parserInfo :: ParserInfo Sample
parserInfo = info (helper <*> sample)
$ fullDesc
<> progDesc "Print a greeting for TARGET"
<> header "hello - a sample for optparse-applicative"

greet :: Sample -> IO ()
greet (Sample h b)
| b = putStrLn $ "Hello, " <> h
| otherwise = return ()

main :: IO ()
main = do
options <- execParser parserInfo
greet options

はい、公式のをちょっと パクって 分かりやすく改変したコードになります。分りやすいように、めっちゃ綺麗に書いてますが、Parserは普通のアプリカティブの使い心地なのでもっと雑に使っても問題無いです。では、実行してみましょう。

$ stack runhaskell Main.hs -- --help

hello - a sample for optparse-applicative

Usage: Main.hs --hello TARGET [-q|--quiet]
Print a greeting for TARGET

Available options:
-h,--help Show this help text
--hello TARGET Target for the greeting
-q,--quiet Whether to be quiet
$ stack runhaskell Main.hs -- --hello Haskell
Hello, Haskell
$ stack runhaskell Main.hs -- --hello Haskell -q
$ stack runhaskell Main.hs
Missing: --hello TARGET

Usage: Main.hs --hello TARGET [-q|--quiet]
Print a greeting for TARGET

はい、こんな感じです。

それぞれオプションはもちろんFunctorで関数からカスタムさせることもできますし、Applicativeでカスタムすることも可能です。デフォルトで他に幾つかのパーサーも用意されていて、Options.Applicative.Builderとかに色々転がっています1


optparse-applicativeの辛いとこ

そもそもコンフィグ周りって面倒くさいんですよね。環境変数読み込んだり、設定ファイル読み込んだりだの、グローバルに呼び出したいだの。まあ、この辺なんとかしたいなあって思ってて、密かにライブラリ作ってるんですけど遅々として進んで無い感じですね。

で、まずHaskell事情としてのオプションパースの辛いとこなんですけど、環境変数とかと一緒じゃ無いんで、オプションで埋まらなかったら環境変数で読み込むとかいった場合に、読み込む用にMonoidにしたデータと、実行用にMonoidから全てのパラメータ確定したデータと用意しなきゃいけなくて、それがほんとにめんどい。ほんと、同じようなデータを2個作るのって苦行ですね。TemplateHaskellで頑張ろうとも思うんですけど、実際には1アプリ一回しか使う機会無いですし、TemplateHaskellすぐAPI変わっちゃうし、なんか書く気力が起きない、的な・・・。

あと、Parser自体も結局、各オプションをそのままデータ構造に落とし込むコード書くことが多いんで、明らかにこれTemplateHaskellでオプションパーサーからデータ型作れるやろとか、思うようなコードになっちゃって辛いんですよね。その辺のインターフェースが欲しい。

それから、多分ユーザー全員思ってることだと思うんですが、サブコマンド作るためのインターフェースがほんと扱いにくい。なんかサブコマンド作るための機構自体は用意されてるんですが、完全にユーザーフレンドリーじゃなくて、辛いです。あと、一々fullDescとか書かなきゃいけないのもあれですね。普通fullDescじゃ。

すいません、半分愚痴です。ところで、最後の部分に関しては、毎度ながらFPCompleteというか、@chrisdoneさんが出してくれていまして、optparse-simpleというパッケージが存在します。


optparse-simpleパッケージ

optparse-simpleはoptparse-applicativeをもっと気軽に使えるように幾つかインターフェースを付け加えています2。特にサブコマンド周りが改善されています。ちょっと使ってみましょう。

module Main where

import Options.Applicative.Simple

sample :: IO ()
sample = putStrLn "Yeah! Yeah!"

hello :: String -> IO ()
hello s = putStrLn $ "Hello, " <> s

main :: IO ()
main = do
(msg, runCmd) <- cmdParser
putStrLn msg
runCmd
where
cmdParser = simpleOptions
"version"
"header"
"description"
(strOption $ long "enter-message")
$ do
sampleCommand
helloCommand

sampleCommand = addCommand "sample"
"This is a sample command"
(const sample)
(pure ())

helloCommand = addCommand "hello"
"Show hello message"
hello
(strOption $ long "your-name")

実行してみます。

$ stack runhaskell Main.hs -- --help

sample

Usage: Main.hs [--version] [--help] --enter-message MESSAGE COMMAND
This is a sample application with optparse-simple

Available options:
--version Show version
--help Show this help text

Available commands:
sample This is a sample command
hello Show hello message
$ stack runhaskell Main.hs -- --version
0.0.0
$ stack runhaskell Main.hs -- --enter-message Entered hello --your-name "Nyan"
Entered
Hello, Nyan
$ stack runhaskell Main.hs -- --enter-message Hello sample
Hello
Yeah! Yeah!

サブコマンドを付けることができました。

まあ、正直それでも使いにくい。

いちよ、本家も使いにくさは認知していて、時折改善が入ってるんですが、まあ使いにくいですね。正直僕も使いにくくはあるけど代替案は思いつかない状態ですね。コンフィグ周りはめんどい・・・・・・。優しい世界に行きたい・・・・・・・。


最後に

optparse-applicativeパッケージとoptparse-simpleパッケージの紹介でした。Advent Calendarだし許されるっしょって感じでめっちゃ雑にしか紹介して無いですね。また機会があればもっと踏み込んだ説明しようと思います。なんか分からない点あれば、コメントでもTwitterでも聞いてもらえれば答えます。

optparse-simple自体は開発が停滞していて、というかまあ繋ぎみたいな感じなので、開発を存続させる気力も無いみたいなものを感じますが、軽くサブコマンド付きのCUIツール作る分には良いパッケージだと思います。

本家のインターフェースがもっと改善されるのが望ましいので、僕もコントリビュートしていきたいですね。時間が欲しい・・・・・・。





  1. 正直ここにあるのだけじゃ物足りない場合が多いです。そういう場合、ここに一杯あります;) 



  2. ほとんどstackのソースから引っ張ってきたやろって思ったのは内緒な