Haskellでコマンドラインアプリケーションを作る時の基本的な情報とTips

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を作る際に必要な情報をまとめていこうと思います。

ファイルの読み書き

PreludereadFilewriteFileなど基本的な関数があります。ByteStringText向けにも同じインターフェイスの関数が用意されています。

ディレクトリ操作

System.Directoryを使います。

ファイルパス

System.FilePath を使います。

ドキュメントはSystem.FilePath.PosixSystem.FilePath.Windowsに分かれていますがインターフェイスは同じです。

外部プロセスの呼び出し

System.Process を使います。readProcessreadProcessWithExitCodeを使うことが多いような気がします。

結構前の話ですがsystemが1.2.0.0で非推奨になってしまいました。よく使う関数だったので衝撃です。今後はcallCommandを使えば良いみたいです。

文字列操作

真面目にHaskellのプログラムを書いていると、ユニコードの文字を出力する必要がある、遅い、などの事情で素のStringではなくByteStringTextを使うことになると思います。「ファイルの読み書き」にも書きましたが、この2つのライブラリははPreludeと同じインターフェイスの関数を多く装備していて、それを使うとByteString/Textの文字列もリスト操作感覚でいじることができます。

終わり方

exitSucess / exitFailureSystem.Exit にあります。

標準入出力

getChargetLinegetContentsなど基本的なものはPreludeにあります。さっきのサンプルプログラムにあるようにHandleを指定するなどの場合はSystem.IOにあるhで始まるものを使います。 これもByteString/Textに同様の関数があります。

今回は省略を省きますが、conduitpipesio-streams等のストリーム処理ライブラリを使うのも良いと思います。

コマンドラインオプションは?

沢山ライブラリがあります。optparse-applicativeがおすすめです。APIの変更が若干激しい、ドキュメントが読みづらい等欠点はありますが使いやすいライブラリです。

同梱したファイルを使いたい

.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

おわりに

HaskellCLI作るの楽しいです。相性良い気がします。是非お試しを!

追記

バージョン情報が無かった。各ライブラリは2014/12/16時点で最新のものを使っています。

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.8.3
$ cabal --version
cabal-install version 1.20.0.3
using version 1.20.0.2 of the Cabal library