optparse-applicativeでコマンドライン引数をパースする

# Haskellでコマンドラインをパースする

　この記事では、optparse-applicativeというライブラリを用いてHaskellでコマンドラインのパースを行う方法を紹介します。
　これは、柔軟なオプションの作成が可能でありながら、モジュールの分割が容易かつ型安全、小さいパーサーを組み合わせて大きなパーサーを作っていくのがコンセプトのHaskellらしいライブラリです。

## optparse-applicativeのえらい点

* ヘルプの自動生成
* 補完の自動生成
* サブコマンドの作成が容易

## 目次

1. 基本
1. コマンドライン引数とパーサーの対応付け
1. こんなときどうする？
1. おわりに

## 基本

　optparse-applicativeの基本は、コマンドラインをパースして、コマンドラインを表現する代数的データ型に格納する、というものです。
　まずはどのようなコマンドラインを受け付けるか設計して、それを表現する型を作成します。ここでは`echo2`という革新的なコマンドを作りましょう。

```bash
echo2 [-n] [STRING]
```

　このコマンドラインオプションに対応する型を考えます。

```Haskell
data Argument = Argument
    { noTrailingNewline :: Bool
    , text :: String
    } deriving (Read, Show)
```

　そうしたらパーサーを書きます。
　optparse-applicativeの名の通り、アプリカティブスタイルで`Argument`を組み立てていきます。またここで各引数についての追加の情報を`Monoid`で指定します。
　`switch`がフラグによる`Bool`の指定を、`strArgument`が文字列の引数を表します。また、フラグに使う文字、ヘルプに表示するオプションの説明、metavar, value (デフォルト値) を追加の情報として指定しました。

```Haskell
import Options.Applicative

argumentParser :: Parser Argument
argumentParser = Argument
    <$> switch (short 'n' <> help "Do not output the trailing newline")
    <*> strArgument (metavar "STRING" <> value "")
```

　次はこれを`ParserInfo`型に変換する必要があります。これには`info`関数を使います。`info`とヘルプを生成する`helper`を合成し、パーサーとその説明を渡すと`ParserInfo`を渡してくれる関数`withInfo`を作成しました。

```Haskell
withInfo :: Parser a -> String -> ParserInfo a
withInfo p = info (p <**> helper) . progDesc

argumentParserInfo :: ParserInfo Argument
argumentParserInfo = argumentParser `withInfo` "display a line of text"
```

　後は`execParser`を使って実際にコマンドライン引数をパースし、それを`Argument -> IO ()`型を持つ関数に渡してあげれば完成です。

```Haskell
main :: IO ()
main = execParser argumentParserInfo >>= runMain

runMain :: Argument -> IO ()
runMain Argument {..} = do
    putStr text
    unless noTrailingNewline $
        putChar '\n'
```

　これがoptparse-applicativeの基本です。
　ヘルプを見てみましょう。この`--help`オプションは、`withInfo`関数の中で使った`helper`が生成したものです。

```bash
$ echo2 --help
Usage: echo2 [-n] [STRING]
  display a line of text

Available options:
  -h,--help                Show this help text
  -n                       Do not output the trailing newline
```

　なかなか美しいヘルプテキストに見えます。

## コマンドライン引数とパーサーの対応付け

　optparse-applicativeは、コマンドライン引数を下の4つに分類して、それぞれに対してパーサーを用意しています。
　自分が欲しいコマンドライン引数がどれに対応するかを考えてパーサーを書く必要がありますね。

* [argument](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:argument): ハイフンの付かない引数
    * 文字列に特殊化した[strArgument](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:strArgument)がある
    * 文字列以外をパースするときは、Read型クラスのインスタンスであれば`auto`が使える
        * `argument auto`という形
* [flag](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:flag): `-n`みたいなやつ
    * Boolにパースするときは`switch = flag False True`が便利
* [option](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:option): `-f FILE_PATH`みたいなやつ
    * 文字列に特殊化した[strOption](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:strOption)がある
* [command](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:command): サブコマンド
    * 各サブコマンドは独立した`ParserInfo`を持つ

## こんなときどうする？

### オプション(引数をつきフラグ)が欲しい

　革新的な`echo2`を、なんとファイルの内容表示することもできるようにしたいと思います。
　次のような感じですね。

```bash
echo2 [-n] [-f FILE_PATH] [STRING]
```

　これに合わせて、`Argument`型を修正します。

```Haskell
data Argument = Argument
    { noTrailingNewline :: Bool
    , filePath :: Maybe FilePath
    , text :: String
    } deriving (Read, Show)
```

　文字列の引数つきオプションをパースするには、[strOption](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:strOption)を使います。
　また、オプショナルな引数を使う時は`Control.Applicative.optional`を使います。
　オプションの設定が長くなってきたら、`fold`や`Writer`モナドを使いましょう。

```Haskell
import Control.Monad.Writer.Strict
import Options.Applicative

argumentParser :: Parser Argument
argumentParser = Argument
    <$> switch (short 'n' <> help "Do not output the trailing newline")
    <*> optional (strOption . execWriter $ do
            tell $ short 'f'
            tell $ metavar "FILE_PATH"
            tell $ help "Display a content in a file"
        )
    <*> strArgument (metavar "STRING" <> value "")
```

　特に`ApplicativeDo`、`RecordWildCards`、Writerモナドの組み合わせは抜群に使い心地がいいです。

```Haskell
{-# LANGUAGE ApplicativeDo   #-}
{-# LANGUAGE RecordWildCards #-}

import Control.Monad.Writer.Strict
import Options.Applicative

argumentParser :: Parser Argument
argumentParser = do
    noTrailingNewline <- switch (short 'n' <> help "Do not output the trailing newline")
    filePath <- optional . strOption . execWriter $ do
        tell $ short 'f'
        tell $ metavar "FILE_PATH"
        tell $ help "Display a content in a file"
    text <- strArgument (metavar "STRING" <> value "")
    pure Argument {..}
```

　ヘルプを表示は次のようになりました。

```bash
$ echo2 --help
Usage: echo2 [-n] [-f FILE_PATH] [STRING]
  display a line of text

Available options:
  -h,--help                Show this help text
  -f                       Display a content in a file
  -n                       Do not output the trailing newline
```

### オプショナルな引数や、複数回設定できる引数を実現したい

　先ほどちらっと出てきましたが、オプショナルな引数には`Control.Applicative`モジュールの`optional`が使えます。
　さらに、複数回受け取れる引数には`many`や`some`が使えます。

　`-f FILE_PATH`がオプショナルな引数、`STRING...`が0個以上の文字列としましょう。

```bash
echo2 [-n] [-f FILE_PATH] [STRING...]
```

　オプショナルな`-f`は`Maybe FilePath`, 複数回受け取れるtextは`[String]`にします。

```Haskell
data Argument = Argument
    { noTrailingNewline :: Bool
    , filePath :: Maybe FilePath
    , text :: [String]
    } deriving (Read, Show)
```

　パースする部分では、`optional`と`many`を使います。`many`は0個以上の場合に、`some`は1つ以上の場合に使います。
　`many`や`some`を使う場合、`value`を併用しないよう気を付けてください。無限にデフォルト値を取ってきてしまいます。

```Haskell
import Control.Monad.Writer.Strict

argumentParser :: Parser Argument
argumentParser = Argument
    <$> switch (short 'n' <> help "Do not output the trailing newline")
    <*> optional (strOption . execWriter $ do
            tell $ short 'f'
            tell $ metavar "FILE_PATH"
            tell $ help "Display a content in a file"
        )
    <*> many (strArgument $ metavar "STRING...")
```

　ヘルプを表示すると、次のような感じです。

```bash
$ echo2 --help
Usage: echo2 [-n] [-f FILE_PATH] [STRING...]
  display a line of text

Available options:
  -h,--help                Show this help text
  -n                       Do not output the trailing newline
  -f FILE_PATH             Display a content in a file,
```

　また、実際に`echo2`には複数の文字列を渡すことができます。

```Haskell
$ echo2 "Hello," "World!"
Hello, World!
```

### サブコマンドを実現したい

　サブコマンドを実現するには、`command`パーサーにサブコマンドの`ParseInfo`を渡します。
　`toolbox`というコマンドを新たに作り、そのサブコマンドとして`echo2`と`cat2`を作ってみましょう。

　`toolbox`コマンドは、サブコマンドとして`echo2`あるいは`cat2`を持つので、パース結果は下のようになります。

```Haskell
data ToolBox
    = Echo2 Echo2Argument
    | Cat2 Cat2Argument
    deriving (Read, Show)
```

　`echo2`は今までのものを流用します。`cat2`は引数にファイルパスを受け取るものとしましょう。

```Haskell
data Echo2Argument = Echo2Argument
    { noTrailingNewline :: Bool
    , text :: [String]
    } deriving (Read, Show)

newtype Cat2Argument = Cat2Argument
    { filePath :: [FilePath]
    } deriving (Read, Show)
```

　`echo2`, `cat2`それぞれのパーサーを書きます。

```Haskell
echo2ArgumentParser :: Parser Echo2Argument
echo2ArgumentParser = Echo2Argument
    <$> switch (short 'n' <> help "Do not output the trailing newline")
    <*> many (strArgument $ metavar "STRING...")

echo2ArgumentParserInfo :: ParserInfo Echo2Argument
echo2ArgumentParserInfo = echo2ArgumentParser `withInfo` "display a line of text"

cat2ArgumentParser :: Parser Cat2Argument
cat2ArgumentParser = Cat2Argument
    <$> many (strArgument $ metavar "FILE_PATH...")

cat2ArgumentParserInfo :: ParserInfo Cat2Argument
cat2ArgumentParserInfo = cat2ArgumentParser `withInfo` "concatenate files and print on the standard output"
```

　あとはそれを`subcommand`を使って繋げれば完成です。
　見ての通り小さいパーサーはそれぞれ独立しているので、モジュール分割も容易です。

```Haskell
toolBoxParser :: Parser ToolBox
toolBoxParser = subparser $ echo2 <> cat2
     where
     echo2 = command "echo2" $ Echo2 <$> echo2ArgumentParserInfo
     cat2 = command "cat2" $ Cat2 <$> cat2ArgumentParserInfo

toolBoxParserInfo :: ParserInfo ToolBox
toolBoxParserInfo = toolBoxParser `withInfo` "a minimum box with basic tools"
```

　ヘルプを見てみましょう。

```bash
$ toolbox --help
Usage: toolbox COMMAND
  a minimum box with basic tools

Available options:
  -h,--help                Show this help text

Available commands:
  echo2                    display a line of text
  cat2                     concatenate files and print on the standard output
```

　これはえらいですね。`cat2`のヘルプも見てみましょうか。

```Haskell
toolbox cat2 [FILE_PATH...]
  concatenate files and print on the standard output

Available options:
  -h,--help                Show this help text
```

### 補完を使いたい

　optparse-applicativeは補完も指定することができます。
　その前に、おまじないをかけておきましょう。`yourprogram`は適宜置き換えてください。

```bash
$ source <(yourprogram --bash-completion-script $(which yourprogram))
```

　bashの他にはzshとfishの補完がサポートされているようです。

> * --zsh-completion-script: which is analogous for zsh;
> * --fish-completion-script: which is analogous for fish shell

　補完は`argument`や`option`で使えます。

#### completeWith

　最も簡単なのは[completeWith](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:completeWith)で、補完候補をリストで与えます。

```Haskell
someParser :: Parser String
someParser = strArgument (completeWith ["foo", "bar", "buz"])
```

#### action

　次はbashの補完を利用できる[action](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:action)です。

　`complete`コマンドの`-A`オプションが使えます。
　http://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#Programmable-Completion-Builtins

```Haskell
someParser :: Parser String
someParser = strArgument (action "file")
```

#### mkCompleter

　最も柔軟なのは、[completer](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:completer) . [mkCompleter](https://hackage.haskell.org/package/optparse-applicative-0.15.1.0/docs/Options-Applicative.html#v:mkCompleter)です。
　次のようにして、TABを押した時点での文字列を受け取って候補のリストを返すことができます。
　`mkCompleter`は`String -> IO [String]`という型を持つので、`IO`操作を行うこともできます。

```Haskell
something :: IO [String]
something = undefined

someParser :: Parser String
someParser = strArgument (execWriter $ do
    tell . completer . mkCompleter $ \arg ->
        filter (arg `isPrefixOf`) <$> something
    )
```

## おわりに

　optparse-applicativeというHaskellのコマンドライン引数パーサーを紹介し、その魅力と使いかたを紹介しました。
　誰かしらの何らかの助けになれば幸いです。