パーサへの誘い
Magnusさんのご厚意により、邦訳してもよいことになりましたので掲載させて頂きます。
もとの記事はこちらのAdventures in parsingです。
私はParsecが使いこなせるようになりたいと常日頃から思っていました。何度か試してみたのですが、そのいずれもどこかで躓いて失敗し、そういった期間が非常に長かったので、何度試したかは忘れてしまいました。私の~/devo/test/haskellのなかに書きちらされたコードたちが私の失敗の数々を物語っています。
さて、私の一番最初の試みとして、/proc/< pid >/mapsの中身をパースすることを考えました。
mapsのマニュアルを見てみましょう。
どうやら、
address perms offset dev inode pathname 08048000-08056000 r-xp 00000000 03:0c 64593 /usr/sbin/gpm
という構造になっているようです。
まず、いくつかのデータ型を用意し、つなげていきます。最初にアドレスの範囲を表します。
data Address = Address { start :: Integer, end :: Integer } deriving (Show)
パーミッションにsまたはpが入っているものはAccessと呼ぶことにします。
data Access = Shared | Private deriving (Show)
rwxのようなパーミッションは簡単にブール型として表しましょう。
data Perms = Perms { read :: Bool, write :: Bool, executable :: Bool, access :: Access } deriving (Show)
デバイスは素直に。
data Device = Device { major :: Integer, minor :: Integer } deriving (Show)
最後に、これらをメモリ空間を表すひとつのデータ型に固めます。
data MemRegion = MemRegion { address :: Address, perms :: Perms, offset :: Integer, device :: Device, inode :: Integer, pathname :: String } deriving (Show)
すべてのデータ型はShowのインスタンスとしている(つまり最低でもshowは使える)ので、それらの出力は簡単になっています。
さて、Parsecを使っていきます。トップダウンかボトムアップの方法で開発ができますが、私は後者を選ぶことにしました。しかし、mapsファイルの中の一行がシンプルな形式なので、最終的にどういった関数になるのかが容易に想像できますね。私がボトムアップですると決めたのは、データ型のおかげで、どのように一行を切り離すかが簡単にわかるからです。まず、アドレスの範囲をパースしてみます。
parseAddress = let hexStr2Int = Prelude.read . ("0x" ++) in do start <- many1 hexDigit char '-' end <- many1 hexDigit return $ Address (hexStr2Int start) (hexStr2Int end)
アドレス自体が16進数表記で表されており、少なくとも1文字以上の長さがあるので、many1 hexDigitを使っています。count 8 hexDigitのように、アドレスの長さが8文字であるかをチェックするのが安全であるとは思いますが(少なくとも32bitマシンでは)、私は試していません。
16進数からIntegerに変える方法は2つあります。一つは上でしたようにPrelude.readが0xから始まる文字列を16進数として読みこんでくれることを利用する方法です。もう一つは fst . (!! 0) . readHexを使ってする方法です。
マニュアルページによると、アドレスはダッシュ'-'で区切られているようなので、char '-'で処理しています。
この関数をテストするのは簡単です。ghciを使ってロードし、parseを使用します。
*Main> parse parseAddress "" "0-1" Right (Address { start = 0, end = 1 } ) *Main> parse parseAddress "hhh" "01234567-89abcdef" Right (Address { start = 19088743, end = 2309737967 } )
うまくいっていますね。
次に、パーミッションをパースします。これは素直にかけるのでコメントは必要ないかもしれません。
parsePerms = let cA a = case a of 'p' -> Private 's' -> Shared in do r <- anyChar w <- anyChar x <- anyChar a <- anyChar return $ Perms (r == 'r') (w == 'w') (x == 'x') (cA a)
デバイス情報も上記のアドレスのものと同じ書き方で実装できますが、今回はダッシュではなくコロン':'で分けられています。
parseDevice = let hexStr2Int = Prelude.read . ("0x" ++) in do maj <- many1 digit char ':' min <- many1 digit return $ Device (hexStr2Int maj) (hexStr2Int min)
次に、MemRegionインスタンスを作り出すためにこれらをつなげましょう。
parseRegion = let hexStr2Int = Prelude.read . ("0x" ++) parsePath = (many1 $ char ' ') >> (many1 $ anyChar) in do addr <- parseAddress char ' ' perm <- parsePerms char ' ' offset <- many1 hexDigit char ' ' dev <- parseDevice char ' ' inode <- many1 digit char ' ' path <- parsePath <|> string "" return $ MemRegion addr perm (hexStr2Int offset) dev (Prelude.read inode) path
問題は、パス名のない行なんかもあることです。こんな感じで。
address perms offset dev inode pathname 09058000-0805b000 rwxp 00000000 00:00 0
inodeのあとにスペースがあるようなので、parseRegionの中のchar ' 'は残しています。もしパスをパースしようとして失敗したら、空の文字列をパースすることで解決しました。それがparsePath <|> string ""の意味です。パスネームはどうやらスペースの数によって整えられているようですが、面倒なのでスペースをすべて消費しています。パスネームとしてどの文字が使用できるのか把握していないので、文字があったらとにかく消費していくことにしています。
ここで、つくったこの関数で遊ぶために、pidによる指定で特定のプロセスのmapsファイルをパースし、その結果をMemRegionインスタンスのリストで返す関数を考えました。
getMemRegions pid = let fp = "/proc" </> show pid </> "maps" doParseLine' = parse parseRegion "parseRegion" doParseLine l = case (doParseLine' l) of Left _ -> "Failed to parse line" Right x -> x in do mapContent <- liftM lines $ readFile fp return $ map doParseLine mapContent
上のコードでは、行をIOモナドからうけとり、Parserモナドにそれを渡し、再びIOモナドに返しています。試してみましょう。
*Main> getMemRegions 1
これで実行できますが、ものすごい数で出力されたりするのでtakeを使って制限したりするとよいでしょう。ですので、最後の行は、
return $ map doParseLine (take 4 mapContent)
となります。
さて、これで最初のコマンドライン引数をpidとして受け取るプログラムを書いてみましょう。
main = do pid <- liftM (Prelude.read . (!! 0)) getArgs regs <- getMemRegions pid mapM_ (putStrLn . show) regs
これでパーサは完成です。
ちなみに、以下のモジュールをインクルードする必要があります。
import Control.Monad import System import System.FilePath import Text.ParserCombinators.Parsec