最近では、何かプログラムを書くときは、Haskell を使うようにしています。Haskell でスクリプトを書くと困ることの一つに、コマンドライン・オプションの処理があります。
IOモナド地獄
System モジュールで定義されている getArgs は IO [String] を返します。そこで、型が IO () である main などから以下のように使うことになります。
-- "-c" オプションを調べる import System main = do argv <- getArgs let cflag = "-c" `elem` argv -- ここに何か書く
この方法には2つ問題があります。
- main で getArgs を使うと、コマンドライン・オプションの処理結果(cflag など)を下位の関数にずっと渡していかないといけない
- コマンドライン・オプションの処理結果が必要な関数で getArgs を使うと、本来純粋であるべきその関数が IO に侵される
やはり、コマンドライン・オプションの処理結果は、
- グローバルにアクセスでき
- IO に侵されてない純粋なデータである
べきだと思います。
禁断の unsafePerformIO
長い間、この問題に悩んでいたのですが、"Tackling the Awkward Squad" を読んで、ようやく解決しました。
unsafePerformIO を使うんですね。
unsafePerformIO は、型が IO a -> a であることから分るように、IO モナドの中身を取り出します。
関数名が示唆する通り危険なので、注意して利用しなければいけません。上記の文献では、以下のようなパターンに使うとよいと書かれています。
- 設定ファルのように実行中一回しか触らないファイルの読み書き
- たとえば関数 configFileContents の中
- グローバル*変数*の割り当て
noOfOpenFiles :: IORef Int noOfOpenFiles = unsafePerformIO (newIORef 0)
- デバッグ用のトレースメッセージの表示
trace :: String -> a -> a trace s x = unsafePerformIO (putStrLn s >> return x)
コマンドライン・オプションは、最初のパターンに当てはまりますから使ってよいでしょう。
すなわち、
unsafePerformIO getArgs
で、[String] が得られる訳です。
サンプル
Haskell には、標準で System.Console.GetOpt というモジュールがありますので、これを使った例を示します。
以下は、C コンパイラの一般的なコマンドライン・オプションである "-c" と "-o FILE" をチェックする例です。
ParseArgs を import すると、解析済みのコマンドライン・オプションが options に、ファイル引数が files に定義されます。
必要な場所でそれぞれ
- optCompile options
- 型は Bool
- optOutput options
- 型は Maybe FilePath
とチェックして下さい。
不適切なコマンドライン・オプションを指定すると、usage も表示します。v^^)
module ParseArgs (progName, files, options, Options(..)) where import System import System.IO.Unsafe import System.Console.GetOpt progName :: String progName = unsafePerformIO getProgName options :: Options files :: [String] (options,files) = parseArgs argspec (unsafePerformIO getArgs) parseArgs :: [OptDescr (Options -> Options)] -> [String] -> (Options, [String]) parseArgs spec argv = case getOpt Permute spec argv of (o,n,[] ) -> (foldl (flip id) defaultOptions o, n) (_,_,errs) -> error (concat errs ++ usageInfo usage argspec) ---------------------------------------------------------------- -- Edit from here ---------------------------------------------------------------- usage :: String usage = "Usage: " ++ progName ++ " [options] [file]" data Options = Options { optCompile :: Bool , optOutput :: Maybe FilePath } deriving Show defaultOptions :: Options defaultOptions = Options { optCompile = False , optOutput = Nothing } argspec :: [OptDescr (Options -> Options)] argspec = [ Option ['c'] ["compile"] (NoArg (\opts -> opts { optCompile = True })) "compile but not link" , Option ['o'] ["output"] (ReqArg (\d opts -> opts { optOutput = Just d }) "FILE") "output file" ]