۲۳ - ۷پارسینگِ فایلهای پیکربندی
برای مثالهای بعدی از فرمت ِ فایلهای پیکربندیِ 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'
اجرا و آزمایش با این کُدها رو به خودتون میسپاریم.