Haskellでコマンドラインアプリケーション(以下CLI)を作る時の基本的な情報とTipsをまとめてみました。 Haskellは雰囲気で読める、しかしCLIはあまり作ったことが無い、って人が想定読者です。
この記事はHaskell Advent Calendar 2014の16日目のエントリです。
とりあえず何のサンプルコードも無く話を進めるのも雰囲気が伝わらないかなと思って、Gitリポジトリにあるファイルの中身を標準出力に出力するプログラムをls-more
という名前で作ってみました。
module Main where import Control.Monad import Data.Monoid import qualified Data.Version import Options.Applicative import Paths_ls_more (version) import System.IO (Handle, hGetLine, hIsEOF) import System.Process (runInteractiveProcess) data CommandLineOption = CommandLineOption { showVersion :: Bool -- バージョンを出力 , show3Lines :: Bool -- 3行だけ出力 } commandLineOption :: Parser CommandLineOption commandLineOption = CommandLineOption <$> switch ( long "verion" <> short 'v' <> help "Show version" ) <*> switch ( long "show-3-lines" <> short '3' <> help "Show 3 lines only" ) main :: IO () main = do -- コマンドラインオプションパーサーを実行してオプションを取得 opts <- execParser (info commandLineOption mempty) -- バージョンを出すか、実行するかで分岐 if showVersion opts -- バージョンを出す then putStrLn $ Data.Version.showVersion version else do -- `runInteractiveProcess`で実行した外部プロセスの標準出力を受け取る (_,out,_,_) <- runInteractiveProcess "git" ["ls-files"] Nothing Nothing -- 本処理へ go opts out where go :: CommandLineOption -> Handle -> IO () go opts handle = do -- ファイル末尾か判定 eof <- hIsEOF handle if eof then return () else do -- 一行読む path <- hGetLine handle --- ファイルの中身を読む content <- readFile path -- ファイルパスを出力 putStrLn path if show3Lines opts then -- 3行だけ出力 forM_ (take 3 (lines content)) putStrLn else -- すべて出力 putStrLn content -- 次へ go opts handle
いかがでしょうか。冒頭にこのサンプルコードを置く意味がどの程度あったのかという問いを放置しつつ、箇条書きでCLIを作る際に必要な情報をまとめていこうと思います。
ファイルの読み書き
Prelude
にreadFile
、writeFile
など基本的な関数があります。ByteString
、Text
向けにも同じインターフェイスの関数が用意されています。
- http://hackage.haskell.org/package/base/docs/Prelude.html
- http://hackage.haskell.org/package/bytestring/docs/Data-ByteString.html
- http://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Lazy.html
- http://hackage.haskell.org/package/text/docs/Data-Text-IO.html
- http://hackage.haskell.org/package/text/docs/Data-Text-Lazy-IO.html
ディレクトリ操作
System.Directory
を使います。
ファイルパス
System.FilePath
を使います。
ドキュメントはSystem.FilePath.Posix
とSystem.FilePath.Windows
に分かれていますがインターフェイスは同じです。
- http://hackage.haskell.org/package/filepath/docs/System-FilePath-Posix.html
- http://hackage.haskell.org/package/filepath/docs/System-FilePath-Windows.html
外部プロセスの呼び出し
System.Process
を使います。readProcess
、readProcessWithExitCode
を使うことが多いような気がします。
結構前の話ですがsystem
が1.2.0.0で非推奨になってしまいました。よく使う関数だったので衝撃です。今後はcallCommand
を使えば良いみたいです。
文字列操作
真面目にHaskellのプログラムを書いていると、ユニコードの文字を出力する必要がある、遅い、などの事情で素のString
ではなくByteString
とText
を使うことになると思います。「ファイルの読み書き」にも書きましたが、この2つのライブラリははPreludeと同じインターフェイスの関数を多く装備していて、それを使うとByteString
/Text
の文字列もリスト操作感覚でいじることができます。
終わり方
exitSucess
/ exitFailure
が System.Exit
にあります。
標準入出力
getChar
、getLine
、getContents
など基本的なものはPrelude
にあります。さっきのサンプルプログラムにあるようにHandle
を指定するなどの場合はSystem.IO
にあるh
で始まるものを使います。
これもByteString
/Text
に同様の関数があります。
- http://hackage.haskell.org/package/base/docs/Prelude.html
- http://hackage.haskell.org/package/bytestring/docs/Data-ByteString.html
- http://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Lazy.html
- http://hackage.haskell.org/package/text/docs/Data-Text-IO.html
- http://hackage.haskell.org/package/text/docs/Data-Text-Lazy-IO.html
今回は省略を省きますが、conduit
、pipes
、io-streams
等のストリーム処理ライブラリを使うのも良いと思います。
- http://hackage.haskell.org/package/conduit
- http://hackage.haskell.org/package/pipes
- http://hackage.haskell.org/package/io-streams
コマンドラインオプションは?
沢山ライブラリがあります。optparse-applicative
がおすすめです。APIの変更が若干激しい、ドキュメントが読みづらい等欠点はありますが使いやすいライブラリです。
- https://www.haskell.org/haskellwiki/Command_line_option_parsers
- https://github.com/pcapriotti/optparse-applicative
同梱したファイルを使いたい
.cabal
ファイルのdata-files
プロパティで同梱するファイルを指定できます。
このようなパッケージ固有の情報はPaths_pkgname
というモジュールから読めます。同梱ファイルのパスはgetDataFileName
で取得できます。
バージョンを出したい
サンプルコードにある通り、
import Paths_pkgname import Data.Verison (showVersion)
して、 showVersion Paths_pkgname.version
で取得できます。
設計
個人的には下記のような構造に落ち着きました。各工程をテストできるのが良いです。 実際はもうちょっとグチャグチャになります。
-- コマンドラインオプションを生成 parseArgs :: [String] -> CommandLineOption -- コマンドラインオプションからオプションを生成。 -- 環境変数などはここで読んで、オプションを完成させる buildOption :: CommandLineOption -> IO Option -- オプションから処理を実行。 run :: Option -> IO () -- 各部品をつなげる。 main = (parseArgs <$> getArgs) >>= buildOption >>= run