I've written a rudimentary parser for INI files:
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Map as M
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
type Ini = M.Map T.Text Section
data Section =
Section
{ name :: T.Text
, properties :: M.Map T.Text T.Text
}
deriving (Show)
main :: IO ()
main = parseIni iniFilePath >>= \ini -> putStrLn $ "Parsed INI: " ++ show ini
where
iniFilePath = "/home/me/test.ini"
parseIni :: FilePath -> IO Ini
parseIni iniFilePath = parseToIni . T.pack <$> readFile iniFilePath
parseToIni :: T.Text -> Ini
parseToIni stringToParse =
-- We return the parsed Ini, not the helper values
firstOfTriple $
foldr
(\line (ini, currentSectionMaybe, lineIndex) ->
-- We're at a new section start or the end of the file → add the previous section
-- to the parsed Ini value and create a new section
if isSectionHeader line || lineIndex >= length lines - 1
then let updatedIni = addSection ini currentSectionMaybe
in (updatedIni, Just $ Section (getSectionName line) M.empty, 1 + lineIndex)
else (ini, updateSection currentSectionMaybe line, 1 + lineIndex))
(M.empty, Nothing, 0) $
-- Since foldr is right associative we would process the lines starting with the last one, that's
-- why we reverse the list of lines
reverse lines
where
lines :: [T.Text]
lines = T.splitOn "\n" stringToParse
firstOfTriple :: (a, b, c) -> a
firstOfTriple (x, _, _) = x
parseProperty :: T.Text -> Maybe (T.Text, T.Text)
parseProperty line =
case T.splitOn "=" line of
[name, value] -> Just (T.strip name, T.strip value)
_ -> Nothing
updateSection :: Maybe Section -> T.Text -> Maybe Section
updateSection sectionMaybe line = fmap updateSection' sectionMaybe
where
updateSection' :: Section -> Section
updateSection' section =
-- Add the property to the section if the property can be parsed.
-- Otherwise, leave the section as it were
maybe
section
(\(propName, value) -> Section (name section) (M.insert propName value (properties section)))
(parseProperty line)
getSectionName :: T.Text -> T.Text
getSectionName line = fromMaybe line headerWithoutBracketsMaybe
where
headerWithoutBracketsMaybe = T.stripPrefix "[" line >>= T.stripSuffix "]"
isSectionHeader :: T.Text -> Bool
isSectionHeader line = T.isPrefixOf "[" strippedLine && T.isSuffixOf "]" strippedLine
where
strippedLine = T.strip line
addSection :: Ini -> Maybe Section -> Ini
addSection ini sectionMaybe = maybe ini (\section -> M.insert (name section) section ini) sectionMaybe
I'd love to get feedback on how to simplify the code and/or make it more readable.
Things I'm aware of and ok with at the moment:
- The parser doesn't support comments
addSection
could be eta-reduced- I don't use a parsing library like Parsec
- I don't use lenses