optparse-applicativeをふわっと使う

  • 21
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

今Haskellでいちばん熱いコマンドラインオプションパーザといえば、optparse-applicativeですよね(要出典)!

何事もつかってみるのが一番ということで、とりあえずふわっと使ってみましょう!

次のモジュールを使用しているのでimportしておきましょう。

import Data.Monoid
import Data.Char
import Text.Read
import Options.Applicative

実装

データ型

最初に想定されるコマンドライン引数を表現するデータ型を定義します。

data Mode = Foo | Bar | Baz deriving (Show, Read, Enum, Bounded)

data Options = Options
    { verbose        :: Bool
    , limit          :: Int
    , mode           :: Mode
    , outputFileName :: String
    , inputFiles     :: [FilePath]
    } deriving Show

このくらいにしておきましょう。入力だけ位置引数で取る感じのコマンドを想定しています。
ヘルプに表示される順番と一致するので、見栄えのためinputFilesを最後にしました。

Parser

これらのオプションをどのように受け取るかを定義するParserを書いていきましょう。

最初はverboseオプションを取るParserです。

verboseP :: Parser Bool
verboseP = switch $ short 'v' <> long "verbose" <> help "verbose mode"

こんな感じですね。
switchはオプションが与えられるとTrue、与えられないとFalseとなるParserです。
この場合、-vまたは--verboseが与えられるとTrueになります。また、

switch == flag False True

なので、Boolではなく自分で定義した型を使用したい時はこちらを使いましょう。

ModMonoidになっているので(<>)で繋いでいきましょう。もちろん後述のようにmconcatも使用できます。

さくさく行きましょう。説明の都合上、先にlimitPを実装しましょう。

limitP :: Parser Int
limitP = option auto $ mconcat 
    [ short 'l', long "limit"
    , help "limit"
    , metavar "INT"
    , value 0
    , showDefault
    ]

optionの第一引数には生コマンドライン引数から内部の型(この場合はInt)に変換する関数です。auto :: Read a => ReadM aReadインスタンスに従って変換します。

第二引数にはその他のオプションを指定します。
valueがデフォルト値を指定する関数で、showDefaultがヘルプにデフォルト値を表示するための関数です。
デフォルト値を指定しないと、そのオプションは必須となります。
metavarはヘルプ中でのメタ変数を指定する関数です。

この場合、ヘルプの表示は-l|--limit INTとなります。

outputFileNameP :: Parser String
outputFileNameP = strOption $ mconcat
    [ short 'o', long "output"
    , help "output file"
    , metavar "FILE"
    , value "output.txt"
    , showDefaultWith id
    ]

showDefaultだとshow関数を用いて表示するので、ダブルクオーテーションを表示させないためにshowDefaultWithを使用しています

inputFileP :: Parser FilePath
inputFileP = strArgument $ mconcat
    [ help "input files"
    , metavar "FILE"
    , action "file"
    ]

inputFilesP :: Parser [FilePath]
inputFilesP = some inputFileP

argumentは位置引数を一つ取る関数です。第一引数にはoptionと同様にReadM aの関数を取りますが、今回は文字列そのままなのでstrを指定しています。

ParserAlternativeなのでそれにsomeをつけて1つ以上の引数を取るように出来ます。もちろん0個以上ならmanyです。

completerではどの様に補完を行なうかを指定します。String -> IO [String]をwrapしたCompleter型を与えるのですが、いちいちhaskellで書くのは大変なので、bashにおまかせするactionが提供されています。

actionはbashの組み込みコマンドcompgenを使用して補完候補を生成する関数で、この場合ではcompgen -A file -- 入力の結果が補完に使用されます。

modeReader :: ReadM Mode
modeReader = str >>= \s -> case s of
    []   -> readerError "no input"
    c:cs -> case readMaybe $ toUpper c : map toLower cs of
        Nothing -> readerError $ "unknown mode:" ++ c:cs
        Just m  -> return m

modeP :: Parser Mode
modeP = option modeReader $ mconcat
    [ short 'm', long "mode"
    , help "mode"
    , value Foo
    , showDefault
    , metavar "MODE"
    , completer $ listCompleter $ concatMap 
        ((\s -> [s, map toLower s]) . show) [minBound .. maxBound :: Mode]
    ]

引数を全て小文字でも指定出来るようにするため、modeReaderを定義しています。ReadMMonadなのでこの例の様にReadインスタンスとは別の処理をする事も簡単に出来ます。

さて、これらを纏めましょう。

optionsP :: Parser Options
optionsP = (<*>) helper $ 
    Options <$> verboseP <*> limitP <*> modeP <*> outputFileNameP <*> inputFilesP

helperはヘルプを表示するための-h--helpオプションを追加する関数です。

ParserInfo

つづいてinfo関数を使用して、ParserInfoを構築しましょう。ここはもうイディオムみたいな感じなのでさらっと流しましょう。

myParserInfo :: ParserInfo Options
myParserInfo = info optionsP $ mconcat 
    [ fullDesc
    , progDesc "test program."
    , header "header"
    , footer "footer"
    , progDesc "progDesc"
    ]
  • fullDesc/briefDesc ヘルプに表示する情報量っぽいけど現状使われてないっぽい?

  • header(Doc), footer(Doc), progDesc(Doc) それぞれの位置に表示する説明文を指定します。

  • failureCode 引数のパーズに失敗したときに返す終了コードを指定します。

  • noIntersperse これを付けておくと、位置引数の後にオプションを付けるとパーズに失敗するようになる……的なニュアンスだと思うのですが上手く動かなかったので未確認……

main

あとはこれをexecParserに与えるだけです!

main :: IO ()
main = execParser myParserInfo >>= print

また、customExecParserを使用すればオプションパーザ全体の設定を行う事が出来ます。

例えば、パーズに失敗した時にヘルプを表示するようにするには、

main = customExecParser (prefs showHelpOnError) myParserInfo >>= print

の様に指定します。

使ってみる

さあ、これをコンパイルして、PATHの通ったところにコピーして使ってみましょう!
コードをまとめてgistにおいておきます。optparse.hs

$ ghc -O2 -Wall optparse.hs
$ cp optparse ~/bin

続いて、bash補完のためのコマンドを打ちます。手元のMac(OS X 10.9.4)にもとから入っているbash(3.2.51(1)-release)では上手く動かなかったので、homebrew等で新しいものを入れておきましょう。

$ bash --version
GNU bash, バージョン 4.3.18(1)-release (x86_64-apple-darwin13.3.0)
Copyright (C) 2013 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

# 補完
$ source <(optparse --bash-completion-script `which optparse`)

これで補完が利くのでいろいろ試してみましょう!

$ optparse <TAB>
--help                -o
--limit               -v
--mode                .DS_Store
--output              cabal.sandbox.config
--verbose             optparse
-h                    optparse.hi
-l                    optparse.hs
-m                    optparse.o

$ optparse --<TAB>
--help     --limit    --mode     --output   --verbose

$ optparse --mode <TAB>
Bar  Baz  Foo  bar  baz  foo

また、実行結果も確かめておきましょう。

# argumentが一つ以上存在しないため、簡易のUsageを表示して終了
$ optparse
Usage: optparse [-v|--verbose] [-l|--limit INT] [-m|--mode MODE]
                [-o|--output FILE] FILE
  progDesc

# help表示
$ optparse --help
header

Usage: optparse [-v|--verbose] [-l|--limit INT] [-m|--mode MODE]
                [-o|--output FILE] FILE
  progDesc

Available options:
  -h,--help                Show this help text
  -v,--verbose             verbose mode
  -l,--limit INT           limit (default: 0)
  -m,--mode MODE           mode (default: Foo)
  -o,--output FILE         output file (default: output.txt)
  FILE                     input files

footer

# 成功例
$ optparse optparse.hs
Options {verbose = False, outputFileName = "output.txt", inputFiles = ["optparse.hs"], limit = 0, mode = Foo}

$ optparse -l 3 -m baz -v -o qux optparse.hs optparse
Options {verbose = True, outputFileName = "qux", inputFiles = ["optparse.hs","optparse"], limit = 3, mode = Baz}

# 失敗例
$ optparse -l a
option -l: cannot parse value `a'

Usage: optparse [-v|--verbose] [-l|--limit INT] [-m|--mode MODE]
                [-o|--output FILE] FILE
  progDesc

$ optparse -m qux
option -m: unknown mode: qux

Usage: optparse [-v|--verbose] [-l|--limit INT] [-m|--mode MODE]
                [-o|--output FILE] FILE
  progDesc

完璧ですね!

おわりに

さらっとoptparse-applicativeのだいたいの機能を見てまわりました。
ほかにもサブコマンド(gitコマンド等の様なもの)を使用するための機能などもありますので必要に応じてgithubのREADME.mdやhaddockを見れば良いと思います。

おまけ

オプションの処理に失敗した時にヘルプを表示したい

Options.Applicative.Extraを使うとパーザ全体の設定を変更できます。

myParserPrefs :: ParserPrefs
myParserPrefs = defaultPrefs
    { prefShowHelpOnError = True
    }

main :: IO ()
main = customExecParser myParserPrefs myParserInfo >>= print
$ optparse
Missing: FILE

header

Usage: optparse [-v|--verbose] [-l|--limit INT] [-m|--mode MODE] [-o|--output FILE]
           FILE
  progDesc

Available options:
  -h,--help                Show this help text
  -v,--verbose             verbose mode
  -l,--limit INT           limit (default: 0)
  -m,--mode MODE           mode (default: Foo)
  -o,--output FILE         output file (default: output.txt)
  FILE                     input files

footer

参考