LoginSignup
9
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-31

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

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

optparse-applicativeのえらい点

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

目次

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

基本

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

echo2 [-n] [STRING]

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

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

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

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を作成しました。

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 ()型を持つ関数に渡してあげれば完成です。

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

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

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

$ 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: ハイフンの付かない引数
    • 文字列に特殊化したstrArgumentがある
    • 文字列以外をパースするときは、Read型クラスのインスタンスであればautoが使える
      • argument autoという形
  • flag: -nみたいなやつ
    • Boolにパースするときはswitch = flag False Trueが便利
  • option: -f FILE_PATHみたいなやつ
    • 文字列に特殊化したstrOptionがある
  • command: サブコマンド
    • 各サブコマンドは独立したParserInfoを持つ

こんなときどうする?

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

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

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

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

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

 文字列の引数つきオプションをパースするには、strOptionを使います。
 また、オプショナルな引数を使う時はControl.Applicative.optionalを使います。
 オプションの設定が長くなってきたら、foldWriterモナドを使いましょう。

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 "")

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

{-# 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 {..}

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

$ 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が使えます。
 さらに、複数回受け取れる引数にはmanysomeが使えます。

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

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

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

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

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

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...")

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

$ 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には複数の文字列を渡すことができます。

$ echo2 "Hello," "World!"
Hello, World!

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

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

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

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

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

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

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

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

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を使って繋げれば完成です。
 見ての通り小さいパーサーはそれぞれ独立しているので、モジュール分割も容易です。

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"

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

$ 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のヘルプも見てみましょうか。

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

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

補完を使いたい

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

$ 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

 補完はargumentoptionで使えます。

completeWith

 最も簡単なのはcompleteWithで、補完候補をリストで与えます。

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

action

 次はbashの補完を利用できるactionです。

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

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

mkCompleter

 最も柔軟なのは、completer . mkCompleterです。
 次のようにして、TABを押した時点での文字列を受け取って候補のリストを返すことができます。
 mkCompleterString -> IO [String]という型を持つので、IO操作を行うこともできます。

something :: IO [String]
something = undefined

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

おわりに

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

9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2