Haskell iniファイル用のパーサーを作成する

この記事では、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ブログに投稿していただきありがとうございます。



All Articles