Pull to refresh

Создаём парсер для ini-файлов на Haskell

Reading time8 min
Views8.8K
В данной статье я расскажу как написать свой парсер ini-файлов на Haskell. За основу возьму контекстно-свободную грамматику, построенную в моей предыдущей статье. Для построения парсера будет использоваться библиотека Parsec, которая позволяет строить свои собственные парсеры комбинируя готовые примитивные парсеры при помощи парсерных комбинаторов.

Важно: в данной статье предполагается, что читатель знаком с основами Haskell. Если это не так, то я советую сначала прочитать пару статей для новичков (их можно найти в том числе и на Хабре).

Грамматика


Для начала вспомним какую грамматику для 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"} .

Её описание нам скоро понадобится.

Haskell и Parsec


Начните с установки Parsec (можно взять на официальном сайте или поискать готовые пакеты для вашей OS). Процесс установки для разных систем может быть различным, поэтому я не буду его здесь описывать.

Я постараюсь подробно описать процесс создания парсера на Haskell. Начнём с подключения необходимых модулей. Кроме стандартных System (для получения параметров), 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]

Теперь будем переносить грамматику из нотации Бэкуса-Наура в Haskell. Начнём с inidata.
12 inidata = spaces >> many section >>= return

Объясню, что тут написано: inidata состоит из пробелов (это примитивный парсер библиотеки Parsec), за которыми следует (обозначается монадическим оператором >>) много секций, значения которых мы возвращаем (>>= return).
Что значит возвращаем значения? Задача парсера не только проверить соответствие грамматики и данных, но и ещё преобразовать данные в какой-то структурный вид. В нашем случае — это тип данных IniData. Функция many — это комбинатор парсеров, который по некоторому парсеру для нетерминала A строит парсер для {A}.

Теперь переведём в Haskell нетерминал section. section намного сложнее inidata и поэтому я запишу его в do-нотации.
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)

Этот код — почти дословный перевод нетерминала section из нотации Бэкуса-Наура. Функция char создаёт примитивный парсер, который парсит один символ. Стоит обратить внимание на строки 16, 21 и 22. В строке 16 мы сохраняем значение нетерминала ident (имени секции), а в строке 21 сохраняем список записей, которые идут за заголовком секции. В строке 22 мы возвращаем прочитанные название секции и список записей (это соответствует типу Section).

Переходим к записям.
24 entry = do 
25          k <- ident
26          stringSpaces
27          char '=' 
28          stringSpaces
29          v <- value
30          spaces
31          return (k, v)

Если вы поняли, как мы построили парсер для section, то тут никаких проблем быть не должно. В кратце: в строчках 25 и 29 сохраняем имя параметра и его значение, и возвращаем пару составленную из них (соответствует типу Entry).

Запишем нетерминал для идентификатора. Мы воспользуемся тем, что в Parsec есть комбинатор many1, который позволит склеить нетерминалы identChar и ident в один (мы не могли сделать это в нотации Бэкуса-Наура, т.к. там нет подобного обозначения).
32 ident = many1 (letter <|> digit <|> oneOf "_.,:(){}-#@&*|">>= return . trim

Комбинатор many1 означает, что идентификатор состоит хотя бы из одного символа. Оператор <|> соответствует знаку "|" в нотации Бэкуса-Наура. letter и digit — примитивные парсеры для букв и цифр соответственно. Функция oneOf для строки эквивалентна (char '_' <|> char '.' <|> .....). Отметим также, что при возвращении значения полученная строки обрезается (при помощи функции trim).

Аналогично поступаем с нетерминалом для значения, но используя парсер noneOf, который обратен к oneOf.

34 value = many (noneOf "\n">>= return . trim


Остался последний нетерминал — stringSpaces (нетерминал spaces уже есть в Parsec).
36 stringSpaces = many (char ' ' <|> char '\t')

С грамматикой всё. Осталось определить несколько полезных функций и, конечно, сам main.

Функция trim нужна для удаления лишних пробелов в начале и в конце строки.
38 trim = f . f
39    where f = reverse . dropWhile isSpace

Функция split разделяет текст на строки используя разделитель delim, причём сам разделитель остаётся в конце строки.
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


Переходим к последнему шагу — функции main.

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, то выводим usage. Если с параметрами всё ок, то читаем файл (62) и удаляем комментарии (64).
Теперь нужно запустить парсер. Для этого есть функция parse (65), которой нужно передать главный нетерминал, имя текста (используется для вывода ошибок) и сам текст. Функция parse возвращает либо описание ошибки (Left, 65), либо полученные данные (Right, 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


Надеюсь, что данная статья поможет вам написать свой собственный парсер =)

Интересное замечание: вы можете сравнить парсер из этой статьи с парсером на С++ из статьи «Создаём парсер для ini-файлов на С++».

PS. Спасибо, что помогли перенести этот пост в блог Haskell.
Tags:
Hubs:
Total votes 39: ↑30 and ↓9+21
Comments15

Articles