۲۳ - ۷پارسینگِ فایلهای پیکربندی
برای مثالهای بعدی از فرمت ِ فایلهای پیکربندیِ INI* استفاه میکنیم. یکی از دلایلش اینه که خیلی فرمت ِ رسمیای نیست و میشه سریع و آزادانه باهاش کار کنیم، که برای یادگیری و آزمایش مناسبه. یه دلیلِ دیگه اینه که نسبتاً پیچیدگیِ کمی داره.
INI یه استاندارِ غیررسمی برای فایلهای پیکربندی در بعضی سکوهاست. اسمش براساسِ پسوند فایلش، .ini که مخففِ initialization (مقداردهیِ اولیه) هست گذاشته شده.
یه مثال کوچولو از یه فایلِ پیکربندی ِ INI:
; comment
[section]
host=wikipedia.org
alias=clawاین فایلِ نمونه یه کامنت داره، که تأثیری روی داده ِ پارسشده از فایلِ پیکربندی نداره، ولی میتونه برای تنظیماتی که پیکربندی میکنه توضیحاتی بده. خطِ بعد یه عنوانِ بخش به اسمِ section داره. این بخش خودِش شامل دوتا تنظیماته: یکی به نامِ host با مقدارِ wikiperdia.org، یکی هم به اسمِ alias با مقدارِ claw.
این مثال رو با پراگماها، تعریفِ ماژول، و وارداتها شروع میکنیم:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Data.Ini where
import Control.Applicative
import Data.ByteString (ByteString)
import Data.Char (isAlpha)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Text (Text)
import qualified Data.Text.IO as TIO
import Test.Hspec
import Text.RawString.QQ
-- parsers 0.12.3, trifecta 1.5.2
import Text.Trifectaدیگه باید با OverloadedStrings و QuasiQuotes آشنا باشین.*
م. کاری که پراگما ِ OverloadedStrings انجام میده، در واقع پلیمورفیک کردنِ لفظهای نوشته هست (شبیهِ وضعی که لفظهای عددی بصورتِ پیشفرض دارن) تا بشه تایپهای دیگهای بهشون اختصاص داد (مثلِ ByteString و Text).
آسونترین راهِ نوشتن پارسرها در هسکل اینه که اول با پارسرهای کوچکتر که مسائلِ ریزتَر رو حل میکنن شروع کنین، بعد اونها رو با هم ترکیب کنین تا پارسر ِ نهایی رو بدست بیارین. این دستورالعملِ ایدهآل برای درکِ پارسرهاتون نیست، اما اینکه میشه به سادگیِ توابع اونها رو با هم ترکیب کرد خیلی کارا و خوبه. کارِمون رو با تعریفِ یه ورودیِ تست برای عنوان ِ INI، یه نوعداده، و بعد پارسرِش ادامه میدیم:
headerEx :: ByteString
headerEx = "[blah]"
-- "[blah]" -> Section "blah"
newtype Header =
Header String
deriving (Eq, Ord, Show)
parseBracketPair :: Parser a -> Parser a
parseBracketPair p =
char '[' *> p <* char ']'
-- این اپراتورها باعث میشن کروشهها
-- پارس بشن بعد دور انداخته بشن، اما
-- به عنوان جواب باقی میمونه. p
parseHeader :: Parser Header
parseHeader =
parseBracketPair (Header <$> some letter)اینجا دوتا پارسر رو ترکیب کردیم تا یه Header پارس کنیم. میشه هرکدومشون رو تو REPL آزمایش کنیم. اول تایپِ some letter که به parseBracketPair دادیم رو بررسی میکنیم:
> :t some letter
ome letter :: CharParsing f => f [Char]
> :t Header <$> some letter
eader <$> some letter :: CharParsing f => Header
> let slp = Header <$> some letter :: Parser Headerتایپِ اول برای پارسِر ایه که حروف رو میشناسه و در صورت موفقیت یه مقدارِ String تولید میکنه. تایپِ دوم هم همونه، فقط بجای یه String، یه مقدارِ Header درست میکنه. تایپهای پارسر در هسکل تقریباً همیشه احتمالِ شکست هم در نظر میگیرن؛ جلوتر در این فصل چگونگیش رو توضیح میدیم. تایپِ سوم، بجای تایپِ پلیمورفیک ِ f، تایپِ معین ِ Parser از trifecta رو میده.
تابعِ letter یک کاراکتر مجرد رو پارس میکنه، و some letter یک کاراکتر یا بیشتر رو. باید با سازنده ِ Header بپوشونیمش تا جوابمون (هر حرفی که ممکنه داخلِ کروشهها باشه، همون p از parseBracketPair) به عنوانِ Header ِ فایل (م. به معنای عنوانِ فایل) در پارس ِ نهایی مشخص بشه.
assignmentEx یه ورودیِ نمونه از یک واگذاری در فایلِ پیکربندی ِه، که برای تستکردنِ پارسرِمون ازش استفاده میکنیم. تایپهای مستعار هم فقط برای خوانایی بیشتر تعریف کردیم. چیز خاصی نیست:
assignmentEx :: ByteString
assignemntEx = "woot=1"
type Name = String
type Value = String
type Assignments = Map Name Value
parseAssignment :: Parser (Name, Value)
parseAssignment = do
name <- some letter
_ <- char '='
val <- some (noneOf "\n")
skipEOL -- خیلی مهم!
return (name, val)
-- | آخرِ خط و فاصلههای سفیدِ
-- | .بعد از اون رو رَد کن
skipEOL :: Parser Header
skipEOL = skipMany (oneOf "\n")پارسر ِ parseAssignment رو قدم به قدم توضیح میدیم. برای پارس کردنِ کلید ِ یه واگذاری، یک یا چند حرف رو پارس میکنیم:
name <- some letterبعد "="، که کلید رو از مقدار جدا میکنه پارس میکنیم و دور میندازیم.
_ <- char "="بعد هم کاراکترها رو تا جایی که خطِجدید نیستن پارس میکنیم. این کار رو کردیم تا حروف، اعداد، و فاصلههای سفید پارس بشن:
val <- some (noneOf "\n")"پایانِ خط" رو رَد میکنیم تا دیگه کاراکترهای خطِجدید نبینیم:
skipEOL -- خیلی مهم!با این کار پایانِ واگذاریها مشخص میشن و راحت میشه بیشتر از یک واگذاری رو پارس کرد. یه حالتِ دیگه از همین پارسر که skipEOL نداره رو فرض کنین:
parseAssignment' :: Parser (Name, Value)
parseAssignment' = do
name <- some letter
_ <- char '='
val <- some (noneOf "\n")
return (name, val)اگه امتحانش کنیم:
λ> let spa' = some parseAssignment'
λ> let s = "key=value\nblah=123"
λ> parseString spa' mempty s
Success [("key","value")]میبینین... نتونست واگذاری ِ دوم رو پارس کنه. اما نسخهی اول که skipEOL رو داره میتونه:
λ> let spa = some parseAssignment
λ> parseString spa mempty s
Success [("key","value"),("blah","123")]
λ> let d = "key=value\n\n\ntest=data"
λ> parseString spa mempty d
Success [("key","value"),("test","data")]باید یک-یا-بیشتر کاراکترهای خطِجدید که واگذاری ِ اول رو از دوم جدا میکنن رو رَد کنیم تا پارسرِ واگذاری با موفقیت شروع به پارس ِ حروفِ کلید ِ واگذاری ِ دوم کنه. خوش میگذره، نه؟
parseAssignment رو با توپل کردنِ اسم و مقدارِ واگذاری، و نهایتاً قرار دادن اون توپل توی تایپِ Parser خاتمه میدیم:
return (name, val)برای کامنتهای INI، که باید کلاً رَد بشن، پارسر ِ skipComments رو تعریف میکنیم:
commentEx :: ByteString
commentEx =
"; last modified 1 April\
\ 2001 by John Doe"
commentEx' :: ByteString
commentEx' =
"; blah\n; woot\n \n;hah"
-- | از اول خط شروع کن و
-- | .کامنتها رو رد کن
skipComments :: Parser ()
skipComments =
skipMany (do _ <- char ';' <|> char '#'
skipMany (noneOf "\n")
skipEOL)دوتا مثال برای پارسرِ کامنت تعریف کردیم. دقت کنید که کامنتها ممکنه با ; یا # شروع بشن.
بعد، پارسینگِ بخش لازم داریم. مثل کامنتها، برای این هم داده ِ تست تعریف میکنیم. اینجا جاییه که از اون توسعه ِ QuasiQuotes استفاده میکنیم تا نوشتههای چندخطی رو خوشایندتر بنویسیم:
sectionEx :: ByteString
sectionEx =
"; ignore me\n[states]\nChris=Texas"
sectionEx' :: ByteString
sectionEx' = [r|
; ignore me
[states]
Chris=Texas
|]
sectionEx'' :: ByteString
sectionEx'' = [r|
; comment
[section]
host=wikipedia.org
alias=claw
[whatisit]
red=intoothandclaw
|]بعد میریم سراغِ پارسینگِ بخش:
data Section =
Section Header Assignments
deriving (Eq, Show)
newtype Config =
Config (Map Header Assignments)
deriving (Eq, Show)
skipWhitespace :: Parser ()
skipWhitespace =
skipMany (char ' ' <|> char '\n')
parseSection :: Parser Section
parseSection = do
skipWhitespace
skipComments
h <- parseHeader
skipEOL
assignments <- some parseAssignment
return $
Section h (M.fromList assignments)دوتا نوعداده، یکی برای بخش، یکی هم برای کلِ یه فایلِ پیکربندی ِ INI تعریف کردیم. میبینید که الان parseSection هم فاصلههای سفید، و هم کامنتها رو رد میکنه. و بخش ِ پارس شده رو با عنوانِش (همون h) و یه Map از واگذاریها برمیگردونه:
*Data.Ini> parseByteString
parseSection
mempty
sectionEx
Success (Section (Header "states")
(fromList [("Chris","Texas")]))خب، تا اینجا که همه چیز خوب بوده. حالا یه تابع بنویسیم که بخشها رو توی یه Map میپوشونه. کلید ِ این Mapها عنوان ِ بخشها، و مقادیرشون Mapهای دیگهایاند که اسمِ واگذاریها رو به مقدارشون نگاشت میدن. با استفاده از foldr، یه لیست از بخشها رو توی یک مقدار ِ Map جمعآوری میکنیم:
rollup :: Section
-> Map Header Assignment
-> Map Header Assignment
rollup (Section h a) m =
M.insert h a m
parseIni :: Parser Config
parseIni = do
sections <- some parseSection
let mapOfSections =
foldr rollup M.empty sections
return (Config mapOfSections)بعد از بارگذاری تو REPL، این دوتا رو اجرا کنین و خروجیهاشون رو با هم مقایسه کنین:
parseByteString parseIni mempty sectionEx
parseByteString parseSection mempty sectionExحالا اینها رو میذاریم کنار هم. میخوایم بدونیم پارسِرهامون کاری که باید انجام بدن رو درست انجام میدن یا نه، نمیخوایم یه فایلِ واقعیِ INI رو پارس کنیم. پس با main چندتا تستِ hspec رو اجرا میکنیم. از یه تابع کمکی به اسمِ maybeSuccess هم به عنوانِ بخشی از تستها استفاده میکنیم:
maybeSuccess :: Result a -> Maybe a
maybeSuccess (Success a) = Just a
maybeSuccess _ = Nothing
main :: IO ()
main = hspec $ do
describe "Assignment Parsing" $
it "Can parse a simple assignment" $ do
let m = parseByteString
parseAssignment
mempty assignmentEx
r' = maybeSuccess m
print m
r' `shouldBe` Just ("woot", "1")
describe "Header Parsing" $
it "Can parse a simple header" $ do
let m = parseByteString
parseHeader
mempty
headerEx
r' = maybeSuccess m
print m
r' `shouldBe` Just (Header "blah")
describe "Comment Parsing" $
it "Skips comment before header" $ do
let p = skipComments >> parseHeader
i = "; woot\n[blah]"
m = parseByteString p mempty i
r' = maybeSuccess m
print m
r' `shouldBe` Just (Header "blah")
describe "Section Parsing" $
it "Can parse a simple section" $ do
let m = parseByteString
parseSection
mempty
sectionEx
r' = maybeSuccess m
states =
M.fromList [("Chris", "Texas")]
expected' =
Just (Section (Header "blah")
states)
print m
r' `shouldBe` expected'
describe "INI Parsing" $
it "Can parse multiple sections" $ do
let m = parseByteString
parseIni
mempty
sectionEx''
r' = maybeSuccess m
sectionValues =
M.fromList
[ ("alias", "claw")
, ("host", "wikipedia.org")]
whatisitValues =
M.fromList
[ ("red", "intoothandclaw")]
expected' =
Just (Config
(M.fromList
[ (Header "section"
, sectionValues
, (Header "whatisit"
, whatisitValues)]))
print m
r' `shouldBe` expected'اجرا و آزمایش با این کُدها رو به خودتون میسپاریم.