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