{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Ini
(
readIniFile
,parseIni
,lookupValue
,readValue
,parseValue
,sections
,keys
,printIni
,writeIniFile
,KeySeparator(..)
,WriteIniSettings(..)
,defaultWriteIniSettings
,printIniWith
,writeIniFileWith
,Ini(..)
,iniParser
,sectionParser
,keyValueParser
)
where
import Control.Applicative
import Control.Monad
import Data.Attoparsec.Combinator
import Data.Attoparsec.Text
import Data.Char
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as M
import Data.Semigroup
import Data.Monoid (Monoid)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Prelude hiding (takeWhile)
newtype Ini = Ini { unIni :: HashMap Text (HashMap Text Text) }
deriving (Show, Semigroup, Monoid)
readIniFile :: FilePath -> IO (Either String Ini)
readIniFile = fmap parseIni . T.readFile
parseIni :: Text -> Either String Ini
parseIni = parseOnly iniParser
lookupValue :: Text -> Text -> Ini -> Either String Text
lookupValue name key (Ini ini) =
case M.lookup name ini of
Nothing -> Left ("Couldn't find section: " ++ T.unpack name)
Just section ->
case M.lookup key section of
Nothing -> Left ("Couldn't find key: " ++ T.unpack key)
Just value -> return value
sections :: Ini -> [Text]
sections (Ini ini) = M.keys ini
keys :: Text -> Ini -> Either String [Text]
keys name (Ini ini) =
case M.lookup name ini of
Nothing -> Left ("Couldn't find section: " ++ T.unpack name)
Just section -> Right (M.keys section)
readValue :: Text -> Text -> (Text -> Either String (a, Text))
-> Ini
-> Either String a
readValue section key f ini =
lookupValue section key ini >>= f >>= return . fst
parseValue :: Text -> Text -> Parser a
-> Ini
-> Either String a
parseValue section key f ini =
lookupValue section key ini >>= parseOnly (f <* (skipSpace >> endOfInput))
writeIniFile :: FilePath -> Ini -> IO ()
writeIniFile = writeIniFileWith defaultWriteIniSettings
printIni :: Ini -> Text
printIni = printIniWith defaultWriteIniSettings
data KeySeparator
= ColonKeySeparator
| EqualsKeySeparator
deriving (Eq, Show)
data WriteIniSettings = WriteIniSettings
{ writeIniKeySeparator :: KeySeparator
} deriving (Show)
defaultWriteIniSettings :: WriteIniSettings
defaultWriteIniSettings = WriteIniSettings
{ writeIniKeySeparator = ColonKeySeparator
}
writeIniFileWith :: WriteIniSettings -> FilePath -> Ini -> IO ()
writeIniFileWith wis fp = T.writeFile fp . printIniWith wis
printIniWith :: WriteIniSettings -> Ini -> Text
printIniWith wis (Ini ini) =
T.concat (map buildSection (M.toList ini))
where buildSection (name,pairs) =
"[" <> name <> "]\n" <>
T.concat (map buildPair (M.toList pairs))
buildPair (name,value) =
name <> separator <> value <> "\n"
separator = case writeIniKeySeparator wis of
ColonKeySeparator -> ": "
EqualsKeySeparator -> "="
iniParser :: Parser Ini
iniParser = fmap (Ini . M.fromList) (many sectionParser)
sectionParser :: Parser (Text,HashMap Text Text)
sectionParser =
do skipEndOfLine
skipComments
skipEndOfLine
_ <- char '['
name <- takeWhile (\c -> c /=']' && c /= '[')
_ <- char ']'
skipEndOfLine
values <- many keyValueParser
return (T.strip name, M.fromList values)
keyValueParser :: Parser (Text,Text)
keyValueParser =
do skipEndOfLine
skipComments
skipEndOfLine
key <- takeWhile1 (\c -> not (isDelim c || c == '[' || c == ']'))
delim <- satisfy isDelim
value <- fmap (clean delim) (takeWhile (not . isEndOfLine))
skipEndOfLine
return (T.strip key, T.strip value)
where clean ':' = T.drop 1
clean _ = id
isDelim :: Char -> Bool
isDelim x = x == '=' || x == ':'
skipEndOfLine :: Parser ()
skipEndOfLine = skipWhile (\c -> isEndOfLine c)
skipComments :: Parser ()
skipComments =
skipMany (do _ <- satisfy (\c -> c == ';' || c == '#')
skipWhile (not . isEndOfLine)
skipEndOfLine)