packages feed

ini 0.3.6 → 0.5.1

raw patch · 5 files changed

Files

+ CHANGELOG.md view
@@ -0,0 +1,38 @@+## 0.5.1++_2025-12-20, Jan Hrček_++- Fixed a parser bug where parsing would fail in presence of comments at the end of file+[#12](https://github.com/jhrcek/ini/pull/12)++## 0.5.0++_2023-02-08, Chris Martin_++The behavior of `(<>)` for the `Ini` type has changed+[#2](https://github.com/andreasabel/ini/issues/2)++- `<>` previously discarded all `iniGlobals`. Now it concatenates+  the globals from the two `Ini` values.++- When two `Ini` values contained `iniSections` with the same name,+  `<>` previously returned the section from the left value and+  discarded the section of the same name from the right value.+  Now it concatenates the sections of the same name.++Tested with GHC 7.0 - ghc-9.6.0.20230128.++## 0.4.2++_2022-07-26, Andreas Abel_++- Fail parsing if the input is not completely consumed [#30](https://github.com/chrisdone/ini/pull/30)+- Print global values as well [#28](https://github.com/chrisdone/ini/pull/28)++Tested with GHC 7.0 - 9.4.1 RC1.++## 0.4.1++_2019-01-02, Chris Done_++- Allow global section [#6](https://github.com/chrisdone/ini/issues/6)
+ README.md view
@@ -0,0 +1,63 @@+[![Hackage](https://img.shields.io/hackage/v/ini.svg?color=informational)](https://hackage.haskell.org/package/ini)+[![ini on Stackage Nightly](https://stackage.org/package/ini/badge/nightly)](https://stackage.org/nightly/package/ini)+[![Stackage LTS version](https://www.stackage.org/package/ini/badge/lts?label=Stackage)](https://www.stackage.org/package/ini)+[![Haskell CI](https://github.com/jhrcek/ini/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/jhrcek/ini/actions/workflows/haskell-ci.yml)++ini+===++Quick and easy configuration files in the INI format for Haskell.++Format rules and recommendations:++* `foo: bar` or `foo=bar` are allowed.+* The `:` syntax is space-sensitive.+* Keys are case-sensitive.+* Lower-case is recommended.+* Values can be empty.+* Keys cannot contain `:`, `=`, `[`, or `]`.+* Comments must start at the beginning of the line with `;` or `#`.++An example configuration file:++``` ini+# Some comment.+[SERVER]+port=6667+hostname=localhost+[AUTH]+user=hello+pass=world+# Salt can be an empty string.+salt=+```++Parsing example:++``` haskell+> parseIni "[SERVER]\nport: 6667\nhostname: localhost"+Right (Ini {unIni = fromList [("SERVER",fromList [("hostname","localhost")+                                                 ,("port","6667")])]})+```++Extracting values:++``` haskell+> parseIni "[SERVER]\nport: 6667\nhostname: localhost" >>=+  lookupValue "SERVER" "hostname"+Right "localhost"+```++Parsing:++``` haskell+> parseIni "[SERVER]\nport: 6667\nhostname: localhost" >>=+  readValue "SERVER" "port" decimal+Right 6667+```++Import `Data.Text.Read` to use `decimal`.++## Related packages++[`ini-qq`](https://hackage.haskell.org/package/ini-qq) provides a quasiquoter for INI.
ini.cabal view
@@ -1,28 +1,69 @@+cabal-version:       >= 1.10 name:                ini-version:             0.3.6-synopsis:            Quick and easy configuration files in the INI format.+version:             0.5.1+synopsis:            Configuration files in the INI format. description:         Quick and easy configuration files in the INI format. license:             BSD3 license-file:        LICENSE author:              Chris Done-maintainer:          chrisdone@gmail.com-homepage:            http://github.com/chrisdone/ini-bug-reports:         http://github.com/chrisdone/ini/issues+maintainer:          Jan Hrček+homepage:            https://github.com/jhrcek/ini+bug-reports:         https://github.com/jhrcek/ini/issues copyright:           2013 Chris Done category:            Data, Configuration build-type:          Simple-cabal-version:       >=1.8 +tested-with:+  GHC == 9.12.2+   || == 9.10.3+   || == 9.8.4+   || == 9.6.7+   || == 9.4.8+   || == 9.2.8+   || == 9.0.2+   || == 8.10.7+   || == 8.8.4+   || == 8.6.5+   || == 8.4.4+   || == 8.2.2+   || == 8.0.2++extra-source-files:+  CHANGELOG.md+  README.md+ library   hs-source-dirs:    src/-  ghc-options:       -Wall -O2-  extensions:        OverloadedStrings   exposed-modules:   Data.Ini   build-depends:     base >= 4 && <5,                      attoparsec,                      text,                      unordered-containers+  if !impl(ghc >= 8)+    build-depends: semigroups >= 0.10 && < 0.21 +  default-language:   Haskell98+  default-extensions: OverloadedStrings+                      TypeOperators++  ghc-options:        -Wall+  if impl(ghc >= 8)+    ghc-options:      -Wcompat++test-suite ini-test+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is: Main.hs+  ghc-options:     -Wall+  build-depends:     base >= 4 && <5+                   , QuickCheck+                   , ini+                   , hspec+                   , text+                   , unordered-containers++  default-language:  Haskell98+ source-repository head   type:     git-  location: http://github.com/chrisdone/ini.git+  location: https://github.com/jhrcek/ini.git
src/Data/Ini.hs view
@@ -1,5 +1,4 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE OverloadedStrings #-}  -- | Clean configuration files in the INI format. --@@ -42,7 +41,9 @@    readIniFile   ,parseIni   ,lookupValue+  ,lookupArray   ,readValue+  ,readArray   ,parseValue   ,sections   ,keys@@ -57,6 +58,7 @@   ,writeIniFileWith    -- * Types   ,Ini(..)+  ,unIni    -- * Parsers   ,iniParser   ,sectionParser@@ -71,17 +73,38 @@ import           Data.Char import           Data.HashMap.Strict        (HashMap) import qualified Data.HashMap.Strict        as M+import Data.Maybe 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)  -- | An INI configuration.-newtype Ini = Ini { unIni :: HashMap Text (HashMap Text Text) }-  deriving (Show, Semigroup, Monoid)+data Ini =+  Ini+    { iniSections :: HashMap Text [(Text, Text)]+    , iniGlobals  :: [(Text, Text)]+    }+  deriving (Show, Eq) +-- | '<>' concatenates the lists of entries within each section (since @ini-0.5.0@)+instance Semigroup Ini where+  x <> y =+    Ini+      { iniGlobals = iniGlobals x ++ iniGlobals y+      , iniSections = M.unionWith (++) (iniSections x) (iniSections y)+      }++instance Monoid Ini where+  mempty = Ini {iniGlobals = mempty, iniSections = mempty}+  mappend = (<>)+++{-# DEPRECATED #-}+unIni :: Ini -> HashMap Text (HashMap Text Text)+unIni = fmap M.fromList . iniSections+ -- | Parse an INI file. readIniFile :: FilePath -> IO (Either String Ini) readIniFile = fmap parseIni . T.readFile@@ -90,36 +113,89 @@ parseIni :: Text -> Either String Ini parseIni = parseOnly iniParser --- | Lookup values in the config.-lookupValue :: Text -> Text -> Ini -> Either String Text-lookupValue name key (Ini ini) =-  case M.lookup name ini of+-- | Lookup one value in the config.+--+-- Example:+--+-- >>> parseIni "[SERVER]\nport: 6667\nhostname: localhost" >>= lookupValue "SERVER" "hostname"+-- Right "localhost"+lookupValue :: Text -- ^ Section name+            -> Text -- ^ Key+            -> Ini -> Either String Text+lookupValue name key (Ini {iniSections=secs}) =+  case M.lookup name secs of     Nothing -> Left ("Couldn't find section: " ++ T.unpack name)     Just section ->-      case M.lookup key section of+      case lookup key section of         Nothing -> Left ("Couldn't find key: " ++ T.unpack key)         Just value -> return value +-- | Lookup one value in the config.+--+-- Example:+--+-- >>> parseIni "[SERVER]\nport: 6667\nhostname: localhost" >>= lookupValue "SERVER" "hostname"+-- Right "localhost"+lookupArray :: Text -- ^ Section name+            -> Text -- ^ Key+            -> Ini -> Either String [Text]+lookupArray name key (Ini {iniSections = secs}) =+  case M.lookup name secs of+    Nothing -> Left ("Couldn't find section: " ++ T.unpack name)+    Just section ->+      case mapMaybe+             (\(k, v) ->+                if k == key+                  then Just v+                  else Nothing)+             section of+        [] -> Left ("Couldn't find key: " ++ T.unpack key)+        values -> return values+ -- | Get the sections in the config.+--+-- Example:+--+-- >>> sections <$> parseIni "[SERVER]\nport: 6667\nhostname: localhost"+-- Right ["SERVER"] sections :: Ini -> [Text]-sections (Ini ini) = M.keys ini+sections = M.keys . iniSections  -- | Get the keys in a section.-keys :: Text -> Ini -> Either String [Text]-keys name (Ini ini) =-  case M.lookup name ini of+--+-- Example:+--+-- >>> parseIni "[SERVER]\nport: 6667\nhostname: localhost" >>= keys "SERVER"+-- Right ["hostname","port"]+keys :: Text -- ^ Section name+     -> Ini -> Either String [Text]+keys name i =+  case M.lookup name (iniSections i) of     Nothing -> Left ("Couldn't find section: " ++ T.unpack name)-    Just section -> Right (M.keys section)+    Just section -> Right (map fst section)  -- | Read a value using a reader from "Data.Text.Read".-readValue :: Text -> Text -> (Text -> Either String (a, Text))+readValue :: Text -- ^ Section name+          -> Text -- ^ Key+          -> (Text -> Either String (a, Text))           -> Ini           -> Either String a readValue section key f ini =   lookupValue section key ini >>= f >>= return . fst +-- | Read an array of values using a reader from "Data.Text.Read".+readArray :: Text -- ^ Section name+          -> Text -- ^ Key+          -> (Text -> Either String (a, Text))+          -> Ini+          -> Either String [a]+readArray section key f ini =+  fmap (map fst) (lookupArray section key ini >>= mapM f)+ -- | Parse a value using a reader from "Data.Attoparsec.Text".-parseValue :: Text -> Text -> Parser a+parseValue :: Text -- ^ Section name+           -> Text -- ^ Key+           -> Parser a            -> Ini            -> Either String a parseValue section key f ini =@@ -137,7 +213,7 @@ data KeySeparator   = ColonKeySeparator   | EqualsKeySeparator-  deriving (Eq, Show)+  deriving (Bounded, Enum, Eq, Show)  -- | Settings determining how an INI file is written. data WriteIniSettings = WriteIniSettings@@ -156,11 +232,12 @@  -- | Print an INI config. printIniWith :: WriteIniSettings -> Ini -> Text-printIniWith wis (Ini ini) =-  T.concat (map buildSection (M.toList ini))+printIniWith wis i =+  T.concat $ map buildPair (iniGlobals i) +++             map buildSection (M.toList (iniSections i))   where buildSection (name,pairs) =           "[" <> name <> "]\n" <>-          T.concat (map buildPair (M.toList pairs))+          T.concat (map buildPair pairs)         buildPair (name,value) =           name <> separator <> value <> "\n"         separator = case writeIniKeySeparator wis of@@ -169,10 +246,15 @@  -- | Parser for an INI. iniParser :: Parser Ini-iniParser = fmap (Ini . M.fromList) (many sectionParser)+iniParser =+  (\kv secs -> Ini {iniSections = M.fromList secs, iniGlobals = kv}) <$>+  many keyValueParser <*>+  many sectionParser <*+  skipComments <*+  (endOfInput <|> (fail . T.unpack =<< takeWhile (not . isControl)))  -- | A section. Format: @[foo]@. Conventionally, @[FOO]@.-sectionParser :: Parser (Text,HashMap Text Text)+sectionParser :: Parser (Text,[(Text, Text)]) sectionParser =   do skipEndOfLine      skipComments@@ -182,7 +264,7 @@      _ <- char ']'      skipEndOfLine      values <- many keyValueParser-     return (T.strip name, M.fromList values)+     return (T.strip name, values)  -- | A key-value pair. Either @foo: bar@ or @foo=bar@. keyValueParser :: Parser (Text,Text)@@ -204,7 +286,7 @@  -- | Skip end of line and whitespace beyond. skipEndOfLine :: Parser ()-skipEndOfLine = skipWhile (\c -> isEndOfLine c)+skipEndOfLine = skipWhile isSpace  -- | Skip comments starting at the beginning of the line. skipComments :: Parser ()
+ test/Main.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Simple test suite.++module Main where++import           Data.Char (isControl, isSpace)+import qualified Data.HashMap.Strict as HM+import           Data.Ini (Ini (..), KeySeparator (..), WriteIniSettings (..), parseIni, printIniWith)+import           Data.Text (Text)+import qualified Data.Text as T+import           Test.Hspec (describe, hspec, it, shouldBe)+import           Test.QuickCheck++main :: IO ()+main =+    hspec $ do+        describe "parseIni" $ do+            it "parses multi-section file with comments" $+                parseIni+                    "# Some comment.\n\+                    \[SERVER]\n\+                    \port=6667\n\+                    \hostname=localhost\n\+                    \[AUTH]\n\+                    \user=hello\n\+                    \pass=world\n\+                    \# Salt can be an empty string.\n\+                    \salt="+                    `shouldBe` Right+                        ( Ini+                            { iniSections =+                                HM.fromList+                                    [+                                        ( "AUTH"+                                        ,+                                            [ ("user", "hello")+                                            , ("pass", "world")+                                            , ("salt", "")+                                            ]+                                        )+                                    ,+                                        ( "SERVER"+                                        , [("port", "6667"), ("hostname", "localhost")]+                                        )+                                    ]+                            , iniGlobals = []+                            }+                        )++            it "parses file with globals" $+                parseIni+                    "# Some comment.\n\+                    \port=6667\n\+                    \hostname=localhost\n\+                    \[AUTH]\n\+                    \user=hello\n\+                    \pass=world\n\+                    \# Salt can be an empty string.\n\+                    \salt="+                    `shouldBe` Right+                        ( Ini+                            { iniSections =+                                HM.fromList+                                    [+                                        ( "AUTH"+                                        ,+                                            [ ("user", "hello")+                                            , ("pass", "world")+                                            , ("salt", "")+                                            ]+                                        )+                                    ]+                            , iniGlobals =+                                [("port", "6667"), ("hostname", "localhost")]+                            }+                        )++            it "fails to parse file with invalid keys" $+                parseIni+                    "Name=Foo\n\+                    \Name[en_GB]=Fubar"+                    `shouldBe` Left "Failed reading: Name[en_GB]=Fubar"++            it "parses file ending with comments" $+                parseIni+                    "[default]\n\+                    \a = 1\n\+                    \\n\+                    \#[staging-PI]\n\+                    \#a = 2\n"+                    `shouldBe` Right+                        ( Ini+                            { iniSections = HM.fromList [("default", [("a", "1")])]+                            , iniGlobals = []+                            }+                        )++            it "parses file with globals only" $+                parseIni+                    "global1 = hello\n\+                    \global2 = 123\n\+                    \# An end of file comment here"+                    `shouldBe` Right+                        ( Ini+                            { iniSections = HM.empty+                            , iniGlobals = [("global1", "hello"), ("global2", "123")]+                            }+                        )++            it "parses empty file" $+                parseIni "" `shouldBe` Right mempty++            it "roundtrips with printIniWith" $+                property $ \ini settings ->+                    let printed = printIniWith settings ini+                        parsed = parseIni printed+                     in counterexample (T.unpack printed) $+                            parsed === Right ini++genKey :: Gen Text+genKey = do+    firstChar <- arbitrary `suchThat` isValidFirstKeyChar+    restChars <- listOf $ arbitrary `suchThat` isValidKeyChar+    pure $ T.pack (firstChar : restChars)+  where+    isValidKeyChar c = not (c == '=' || c == ':' || c == '[' || c == ']' || isControl c || isSpace c)+    isValidFirstKeyChar c = isValidKeyChar c && c /= ';' && c /= '#'++genSectionName :: Gen Text+genSectionName = do+    chars <- listOf1 $ arbitrary `suchThat` isValidSectionChar+    let name = T.strip $ T.pack chars+    if T.null name+        then genSectionName -- retry if stripping results in empty+        else pure name+  where+    isValidSectionChar c = c /= '[' && c /= ']' && not (isControl c)++genValue :: Gen Text+genValue = do+    chars <- listOf $ arbitrary `suchThat` (not . isControl)+    pure $ T.strip $ T.pack chars++genKeyValue :: Gen (Text, Text)+genKeyValue = (,) <$> genKey <*> genValue++instance Arbitrary KeySeparator where+    arbitrary = arbitraryBoundedEnum+    shrink ColonKeySeparator = []+    shrink EqualsKeySeparator = [ColonKeySeparator]++instance Arbitrary Ini where+    arbitrary = do+        numSections <- choose (0, 2)+        sections <- vectorOf numSections $ do+            name <- genSectionName+            numPairs <- choose (0, 2)+            pairs <- vectorOf numPairs genKeyValue+            pure (name, pairs)+        numGlobals <- choose (0, 3)+        globals <- vectorOf numGlobals genKeyValue+        pure $+            Ini+                { iniSections = HM.fromList sections+                , iniGlobals = globals+                }+    shrink ini =+        -- Shrink by removing sections+        [ ini{iniSections = HM.fromList secs'}+        | secs' <- shrinkList (const []) (HM.toList (iniSections ini))+        ]+            +++            -- Shrink by removing key-value pairs from sections+            [ ini{iniSections = HM.fromList secs'}+            | secs' <- shrinkOne shrinkSection (HM.toList (iniSections ini))+            ]+            +++            -- Shrink by removing globals+            [ ini{iniGlobals = globals'}+            | globals' <- shrinkList (const []) (iniGlobals ini)+            ]+      where+        shrinkSection (name, pairs) = [(name, pairs') | pairs' <- shrinkList (const []) pairs]+        shrinkOne _ [] = []+        shrinkOne f (x : xs) = [x' : xs | x' <- f x] ++ [x : xs' | xs' <- shrinkOne f xs]++instance Arbitrary WriteIniSettings where+    arbitrary = WriteIniSettings <$> arbitrary+    shrink (WriteIniSettings sep) = WriteIniSettings <$> shrink sep