この記事では、Haskellでパーサーのiniファイルを作成する方法を説明します。
前回の記事で構築
した文脈自由文法を基礎
として取り上げます。 パーサーを構築するには、
Parsecライブラリを使用します。これにより、
パーサーコンビネーターを使用して既製の
プリミティブパーサーを組み合わせて、独自のパーサーを構築できます。
重要:この記事は、読者がHaskellの基本に精通していることを前提としています。 そうでない場合は、初心者向けの記事を最初に読むことをお勧めします(Habréを含む)。
文法
最初に、前の記事で作成したiniファイルの文法を思い出しましょう。
inidata = spaces, {section} .
section = "[", ident, "]", stringSpaces, "\n", {entry} .
entry = ident, stringSpaces, "=", stringSpaces, value, "\n", spaces .
ident = identChar, {identChar} .
identChar = letter | digit | "_" | "." | "," | ":" | "(" | ")" | "{" | "}" | "-" | "#" | "@" | "&" | "*" | "|" .
value = {not "\n"} .
stringSpaces = {" " | "\t"} .
spaces = {" " | "\t" | "\n" | "\r"} .
彼女の説明がすぐに必要です。
ハスケルとパーセク
Parsecをインストールすることから始めます(公式Webサイトで入手するか、OSの既製のパッケージを探してください)。 異なるシステムのインストールプロセスは異なる場合があるため、ここでは説明しません。
Haskellでパーサーを作成するプロセスを詳細に説明しようとします。 必要なモジュールを接続することから始めましょう。 標準のシステム(パラメーターの受信用)、Data.Char(isSpace関数用)、およびData.List(find関数用)に加えて、Parsecモジュール-Text.ParserCombinators.Parsecを接続する必要があります。
1 module Main where
2
3 import System.Environment
4 import Data.Char
5 import Data.List
6 import Text.ParserCombinators.Parsec
データタイプを定義します。レコードはキーと値のペア、セクションはレコードのキーリスト、すべてのiniファイルデータはセクションのリストです。
8 type Entry = (String, String)
9 type Section = (String, [Entry])
10 type IniData = [Section]
ここで、文法をBackus-Naur表記からHaskellに転送します。 inidataから始めましょう。
12 inidata = spaces >> many section >>= return
ここで何が書かれているのかを説明します:inidataはスペース(これはプリミティブなParsecライブラリパーサー)で構成され、その後に(モナド演算子>>で示されます)値が返される(>> = return)多くのセクションが続きます。
値を返すとはどういう意味ですか? パーサーのタスクは、文法とデータの対応をチェックするだけでなく、データを何らかの構造形式に変換することでもあります。 私たちの場合、これはIniDataデータ型です。 many関数は、非終端Aパーサーの{A}のパーサーを構築するパーサーコンビネーターです。
次に、非終端セクションをHaskellに翻訳します。 セクションはinidataよりもはるかに複雑であるため、do-notationで記述します。
14 section = do
15 char '['
16 name <- ident
17 char ']'
18 stringSpaces
19 char ' \n '
20 spaces
21 el <- many entry
22 return (name, el)
このコードは、Backus-Naur表記法からの非終端セクションのほぼ文字通りの翻訳です。 char関数は、単一の文字を解析するプリミティブパーサーを作成します。 16行目、21行目、および22行目に注意する価値があります。16行目では、ident非終端記号(セクション名)の値を保存し、21行目ではセクション見出しに続くレコードのリストを保存します。 22行目では、読み取ったセクション名とレコードのリストを返します(これはセクションタイプに対応しています)。
レコードに移動します。
24 entry = do
25 k <- ident
26 stringSpaces
27 char '='
28 stringSpaces
29 v <- value
30 spaces
31 return (k, v)
セクション用のパーサーの作成方法を理解していれば、問題はないはずです。 要するに、25行目と29行目では、パラメーター名とその値を保存し、それらで構成されるペアを返します(Entryタイプに対応)。
識別子の非終端記号を記述します。 Parsecには、identCharとidentの非終端記号を1つに結合できるmany1コンビネータがあるという事実を利用します(そのような指定がないため、Backus-Naur表記ではこれを行うことができませんでした)。
32 ident = many1 (letter <|> digit <|> oneOf "_.,:(){}-#@&*|" ) >>= return . trim
many1コンビネータは、識別子が少なくとも1文字で構成されることを意味します。 演算子<|>は、文字「|」と一致します バッカスナウア表記法。 文字と数字は、それぞれ文字と数字の
プリミティブパーサーです。 文字列のoneOf関数は同等です(char '_' <|> char '。' <|> .....)。 また、値が返されると、受信した文字列が切り捨てられることに注意してください(trim関数を使用)。
値の非終端に対しても同じことを行いますが、oneOfの逆のnoneOfパーサーを使用します。
34 value = many (noneOf " \n " ) >>= return . trim
最後の非終端文字であるstringSpacesが残ります(非終端文字はすでにParsecにあります)。
36 stringSpaces = many (char ' ' <|> char ' \t ' )
それはすべて文法です。 いくつかの便利な機能と、もちろんメイン自体を定義することは残っています。
行の先頭と末尾の余分なスペースを削除するには、トリム関数が必要です。
38 trim = f . f
39 where f = reverse . dropWhile isSpace
split関数は、デリミタ区切り文字を使用してテキストを行に分割します。区切り文字自体は行末に残ります。
41 split delim = foldr f [[]]
42 where
43 f x rest @ (r : rs)
44 | x == delim = [delim] : rest
45 | otherwise = (x : r) : rs
removeComments関数は、コメントと空の行を削除します。テキストを行に分割し、「;」で始まる行を削除します または「\ n」で、それらを再び接着します。
47 removeComments = foldr ( ++ ) [] . filter comment . split ' \n '
48 where comment [] = False
49 comment (x : _) = (x /= ';' ) && (x /= ' \n ' )
findValue関数は、セクションの名前とパラメーター名によってパラメーター値のIniDataを検索します(計算はMaybeモナドで行われます)。 最初に名前でセクションを見つけ、次にセクションのレコードの中から目的のパラメーターを見つけます。 ある時点で何も見つからない場合、関数は単にNothingを返します。
51 findValue ini s p = do
52 el <- find ( \ x -> fst x == s) ini
53 v <- find ( \ x -> fst x == p) (snd el)
54 return $ snd $ v
最後のステップ-メイン関数に進みます。
56 main = do
57 args <- getArgs
58 prog <- getProgName
59 if (length args) /= 3
60 then putStrLn $ "Usage: " ++ prog ++ " <file.ini> <section> <parameter>"
61 else do
62 file <- readFile $ head args
63 [s,p] <- return $ tail args
64 lns <- return ( removeComments file )
65 case (parse inidata "some text" lns) of
66 Left err -> putStr "Parse error: " >> print err
67 Right x -> case (findValue x s p) of
68 Just x -> putStrLn x
69 Nothing -> putStrLn "Can't find requested parameter"
70 return ()
すべてが古き良きCの57〜58行目と同じです。パラメーターとプログラム名を取得します。 さらに、3つのパラメーターがない場合は、使用法を表示します。 パラメーターがすべて問題ない場合は、ファイルを読み取り(62)、コメントを削除します(64)。
次に、パーサーを開始する必要があります。 これを行うには、解析(65)関数があります。この関数には、メインの非端末、テキストの名前(エラーの表示に使用)、およびテキスト自体を渡す必要があります。 解析関数は、エラーの説明(左、65)または受信データ(右、66)を返します。 すべてが解析されると、受信したデータでセクションの名前とパラメーターの名前(67)でレコードを検索します。 検索は、見つかった値(Just、68)を返してから表示するか、何も返さない(Nothing、69)か、エラーメッセージを表示します。
これで、コードは完全に作成されました。 コンパイルして、テスト例で実行します。
$ ghc --make ini.hs -o ini_hs
[1 of 1] Compiling Main ( ini.hs, ini.o )
Linking ini_hs ...
$ ./ini_hs /usr/lib/firefox-3.0.5/application.ini App ID
{ec8030f7-c20a-464f-9b0e-13a3a9e97384}
$ ./ini_hs /usr/lib/firefox-3.0.5/application.ini App IDD
Can't find requested parameter
この記事が、独自のパーサーの作成に役立つことを願っています=)
興味深いメモ:この記事のパーサーを、記事
「 C ++
でiniファイル用のパーサーを作成する」の C ++のパーサーと比較でき
ます 。
PS。 この投稿をHaskellブログに投稿していただきありがとうございます。