diff --git a/Data/AList.hs b/Data/AList.hs
new file mode 100644
--- /dev/null
+++ b/Data/AList.hs
@@ -0,0 +1,120 @@
+{- |
+Module      : Data.AList
+Description : An association list data type.
+Copyright   : &#xa9; Mike Meyer, 2015
+License     : BSD3
+Maintainer  : mwm@mired.org
+Stability   : experimental
+
+A module to encapsulate associative lists.
+
+-}
+
+module Data.AList
+    (
+     -- * Data
+     AList,
+     -- * Convert
+     fromList,
+     toList,
+     toMap,
+     fromMap,
+     -- * Query
+     isEmpty,
+     lookupAll,
+     lookupFirst,
+     lookupBy,
+     member,
+     values,
+     keys,
+     -- * Modify
+     insert,
+     append,
+     deleteAll,
+     deleteFirst,
+     deleteBy) where
+     
+
+import Prelude hiding (deleteBy)
+
+import qualified Data.Map as M
+import Data.List (delete)
+import Data.Monoid ((<>), mappend, mempty, Monoid)
+import Safe (headMay)
+
+
+-- | An 'AList' is a list of of /(key, value)/ pairs. Such pairs
+-- are referred to as an /item/ in the following.
+data AList key value = AList [(key, value)] deriving (Show)
+
+instance Monoid (AList k v) where
+    mempty = AList []
+    mappend (AList a) (AList b) = AList $ a ++ b
+
+-- | 'isEmpty' returns 'True' if the 'AList' is empty.
+isEmpty :: AList k v -> Bool
+isEmpty (AList al) = null al
+
+-- | 'lookupAll' returns a 'List' of all items with the given key.
+lookupAll :: Eq k => k -> AList k v -> [v]
+lookupAll k (AList al) = map snd $ filter ((== k) . fst) al
+
+-- | 'lookupFirst' finds the first value with a matching key, if there is one.
+lookupFirst :: Eq k => k -> AList k v -> Maybe v
+lookupFirst k (AList l) = lookup k l
+
+-- | 'lookupBy' returns an 'AList' of all items with keys matched by the function.
+lookupBy :: (k -> Bool) -> AList k v -> AList k v
+lookupBy f (AList l) = AList $ filter (f . fst) l
+
+-- | 'member' returns true if the given key is used in the 'AList'.
+member :: Eq k => k -> AList k v -> Bool
+member k = elem k . keys 
+
+-- | 'values' returns a 'List' of the values in the 'AList'.
+values :: AList k v -> [v]
+values (AList al) = map snd al
+
+-- | 'keys' returns a 'List' of the keys in the 'AList'.
+keys :: AList k v -> [k]
+keys (AList al) = map fst al
+
+-- | 'insert' a key, value pair at the head of the 'AList'.
+insert :: k -> v -> AList k v -> AList k v
+insert k v (AList l) = AList $ (k, v):l
+
+-- | 'append' a key, value pair to the end of the 'AList'.
+append :: k -> v -> AList k v -> AList k v
+append k v (AList l) = AList $ l ++ [(k, v)]
+
+-- | 'deleteAll' all values associated with a key.
+deleteAll :: Eq k => k -> AList k v -> AList k v
+deleteAll k = deleteBy (== k)
+
+-- | 'deleteFirst' deletes the first value with a given key.
+deleteFirst :: Eq k  => k -> AList k v -> AList k v
+deleteFirst _ (AList []) = AList []
+deleteFirst t (AList ((k, v):is))
+    | t == k    = AList is
+    | otherwise = insert k v . deleteFirst t $  AList is
+
+-- | 'deleteBy' deletes all values with keys matched by the function.
+deleteBy :: (k -> Bool) -> AList k v -> AList k v
+deleteBy f = lookupBy (not . f)
+
+-- | 'fromList' creates an 'AList' from a 'List' of items.
+fromList :: [(k, v)] -> AList k v
+fromList = AList
+
+-- | 'toList' returns a 'List' of the items in an 'AList'.
+toList :: AList k v -> [(k, v)]
+toList (AList l) = l
+
+-- | 'toMap' converts to a 'Map'. Behavior with multiple keys is undefined.
+toMap :: Ord k => AList k v -> M.Map k v
+toMap (AList l) = M.fromList l
+
+-- | 'fromMap' creates an 'AList' from a 'Map', though I'm not sure why
+-- you'd want to do that.
+fromMap :: Ord k => M.Map k v -> AList k v
+fromMap = AList . M.toList
diff --git a/Data/Ini/List.hs b/Data/Ini/List.hs
new file mode 100644
--- /dev/null
+++ b/Data/Ini/List.hs
@@ -0,0 +1,390 @@
+{-# Language FlexibleInstances, FunctionalDependencies, OverlappingInstances,
+             OverloadedStrings, TupleSections, UndecidableInstances #-}
+
+{- |
+Module      : Data.Ini.List
+Description : Ini config file parser using Lists, not Maps.
+Copyright   : &#xa9; Mike Meyer, 2015
+License     : BSD3
+Maintainer  : mwm@mired.org
+Stability   : experimental
+
+Most ini config files turn into 1-1 maps quite nicely. However,
+some encode lists of objects, which require having more than one
+section of a given name, or more than one value in a section with a
+given name - and order matters.
+
+This package parsers Ini files, but instead of creating maps, it
+creates a list of sections, each section of which is a list of
+values. As a result, the 'get' function can now return a list of
+values for options that can occur multiple times, and there are plural
+versions of the Option and Section fetchers.
+
+-}
+
+module Data.Ini.List
+    (
+     -- * Types
+     Section,
+     Config,
+     OptionName,
+     SectionName,
+     Option,
+     SectionItem,
+     ConfigItem,
+     UpdateValue,
+     UpdateOption,
+     Value(value, getValue),
+     -- * Build
+     config,
+     setDefault,
+     (<+), (+>),
+     -- * Convert
+     toList,
+     fromList,
+     -- * Query
+     get,
+     getDefault,
+     getSection,
+     getSections,
+     getSectionsBy,
+     -- * Update
+     updateValues,
+     updateDefaultValues,
+     updateSectionValues,
+     updateOptions,
+     updateDefaultOptions,
+     updateSectionOptions,
+     -- * Format
+     formatConfig,
+     writeConfig,
+     writeConfigFile,
+     hWriteConfig,
+     -- * Parse
+     parseConfig,
+     parseFile,
+     parseFileEx
+    ) where
+
+
+import Control.Applicative ((<$>), (<|>), (<*>), (<*), (*>), Applicative, 
+                            many, optional, pure, some)
+import qualified Data.AList as AL
+import Data.Bifunctor (second)
+import Data.Char (isPrint, isSpace, toLower)
+import Data.Maybe (fromMaybe)
+import Data.List (isPrefixOf, intercalate)
+import Data.Monoid (Monoid, mempty, mappend, (<>))
+import Safe (readMay, headMay)
+import System.IO
+import Text.Trifecta ((<?>), char, CharParsing, eof, manyTill, newline, oneOf,
+                      option, parseFromFile, parseFromFileEx, parseString,
+                      Result, runUnlined, satisfy, sepEndBy, sepEndBy1,
+                      TokenParsing, try, whiteSpace)
+
+
+-- My types
+-- | A config is an unmaned 'Section' and an 'AList' of 'SectionItem's.
+data Config = Config Section (AL.AList SectionName Section) deriving (Show)
+
+type OptionName = String
+-- | Names are all 'String's.
+type SectionName = String
+
+-- | As are values.
+type Option = String
+
+-- | A 'Section' is just an 'AList'.
+type Section = AL.AList OptionName Option
+
+type SectionItem = (OptionName, Option)
+-- | Convient names for items from a 'Section' or 'Config'
+type ConfigItem = (SectionName, Section)
+
+-- | An 'UpdateValue' is a function that takes a 'SectionName',
+-- 'OptionName' and 'Option' and returns a Nothing if it doesn't want
+-- to change the given 'SectionItem', or 'Just' 'Option' if it does.
+type UpdateValue = SectionName -> OptionName -> Option -> Maybe Option
+
+-- | An 'UpdateOption' is like an 'UpdateValue', except it returns a
+-- 'Maybe' 'SectionItem', allowing it to change the key as well as the
+-- value of the 'SectionItem' in question.
+type UpdateOption = SectionName -> OptionName -> Option -> Maybe SectionItem
+
+-- | The 'Value' class is one that the /get/ functions can
+-- return.  Most notably, names that occur multiple times in a section
+-- can become a 'List', returning a singleton or empty 'List' for
+-- single or missing names in a context where a 'List' is needed.
+--
+-- @0/1@, @yes/no@, @on/off@, @true/false@ and @enabled/disabled@ values
+-- will be returned as 'Bool' in the appropriate contexts.
+--
+-- Finally, any value that has a 'Read' instance will be converted
+-- if possible, so that integer and floating point values can be
+-- used immediately.
+class Value a where
+    -- | @'getValue' 'Section' 'OptionName'@ gets the value for the
+    -- named 'Option' from the 'Section'. 
+    getValue :: Section -> OptionName -> Maybe a
+                
+    -- The default is to just convert the current string.
+    getValue s o = value <$> getOption s o
+
+    -- | 'value' converts a single 'Option' 'String' into a value of
+    -- the type of the instance.
+    value :: Option -> a
+
+instance Value String where
+    value v = v
+
+instance Value Bool where
+    value v = value' $ map toLower v
+      where value' v' | elem v' ["1", "yes", "on", "enabled", "true"] = True
+                      | elem v' ["0", "no", "off", "disabled", "false"] = False
+            value' v' = error $ "couldn't parse '" ++ v' ++ "' as Bool"
+
+instance Value t => Value [t] where
+    getValue s o = Just . map value $ getOptions s o 
+    value o = [value o]
+
+instance Read t => Value t where
+    value v = fromMaybe (error $ "couldn't parse '" ++ v ++ "'") $ readMay v
+
+
+-- Constructors
+-- We'd like Config & Section tweaking ops to be polymorphic.
+-- Laws? Pragmatism strikes again!
+class Cons container item | container -> item where
+    -- | 'fromList' creates a 'Config' or 'Section' from a list of items.
+    -- A Config gets an empty default section.
+    fromList :: [item] -> container
+    -- | 'toList' returns the 'AList' in the 'Config' or 'Section' as a list
+    -- of items.
+    toList :: container -> [item]
+    cons :: item -> container -> container
+    -- | (+>) and (<+) operators add items to a 'Config' or 'Section'
+    (+>) :: item -> container -> container
+    (+>) = cons
+    (<+) :: container -> item -> container
+    (<+) = flip (+>)
+
+infixl 7 <+
+infixr 7 +>
+
+instance Cons Section SectionItem where
+    fromList = AL.fromList
+    toList = AL.toList
+    cons i os = os <> AL.fromList [i]
+
+instance Cons Config ConfigItem where
+    fromList l = Config (AL.fromList []) (AL.fromList l)
+    toList (Config _ sl) = AL.toList sl
+    cons i (Config d sl)= Config d $ sl <> AL.fromList [i]
+
+
+-- | 'Config' as a Monoid is a bit off. The default sections are
+-- append to each other. This is required for it to obey the Monoid
+-- laws.
+instance Monoid Config where
+    mempty  = Config (AL.fromList []) $ AL.fromList []
+    mappend (Config ad al) (Config bd bl) = Config (ad <> bd) $ al <> bl
+
+
+-- | 'config' creates a 'Config' from the given default 'Section'
+-- and list of 'ConfigItem''s.
+config :: Section -> [ConfigItem] -> Config
+config s l = Config s $ AL.fromList l
+
+-- | 'setDefault' sets the default 'Section' for the 'Config'.
+setDefault :: Config -> Section -> Config
+setDefault (Config _ sl) s = Config s sl
+
+-- | 'getDefault' returns the default section from a 'Config'.
+getDefault :: Config -> Section
+getDefault (Config d _) = d
+
+-- | 'getSections' returns the 'List' of 'Section's with /name/ in the 'Config'.
+getSections :: Config -> SectionName -> [Section]
+getSections (Config _ sl) n = AL.lookupAll n sl
+
+-- | 'getSection' returns the first 'Section' /name/ from 'Config' if one
+-- exists.
+getSection :: Config -> SectionName -> Maybe Section
+getSection c n = headMay $ getSections c n
+
+-- | 'getSectionsBy' returns a list of 'ConfigItem''s from a 'Config'
+-- chosen by the provided function.
+getSectionsBy :: Config -> (SectionName -> Bool) -> [ConfigItem]
+getSectionsBy (Config _ sl) sel = filter (\(n, _) -> sel n) $ AL.toList sl
+
+-- | 'get' a value from a 'Config' selected by 'Maybe' 'SectionName'
+-- and 'OptionName'.  'Nothing' as the 'SectionName' gets 'Option' values
+-- from the default 'Section'.
+get :: Value a => Config -> Maybe SectionName -> OptionName -> Maybe a
+get c s o = get' s >>= (flip getValue) o where
+    get' (Just n) = getSection c n
+    get' Nothing  = Just $ getDefault c
+
+
+-- And finally, the things to update a Config
+
+-- So it turns out both types of updaters are also Monoids. Who knew?
+emptyUpdate :: SectionName -> OptionName -> Option -> Maybe a
+emptyUpdate _ _ _ = Nothing
+
+mappendUpdate :: (SectionName -> OptionName -> Option -> Maybe a)
+              -> (SectionName -> OptionName -> Option -> Maybe a)
+              -> (SectionName -> OptionName -> Option -> Maybe a)
+(a `mappendUpdate` b) s o v  =  maybe (b s o v) Just $ a s o v
+
+instance Monoid (SectionName -> OptionName -> Option -> Maybe a) where
+    mempty = emptyUpdate
+    mappend = mappendUpdate
+
+-- | 'updateOptions' uses an 'UpdateOption' to update all the options
+-- in the 'Config'.
+updateOptions :: UpdateOption -> Config -> Config
+updateOptions f (Config d sl) =
+    Config d . AL.fromList . map (\(n, s) -> (n, updateSectionOptions (f n) s))
+           $ AL.toList sl
+
+-- | 'updateOptions' uses an 'UpdateValue' to update all the values in
+-- the 'Config'.
+updateValues :: UpdateValue -> Config -> Config
+updateValues f (Config d sl) =
+    Config d . AL.fromList . map (\(n, s) -> (n, updateSectionValues (f n) s))
+               $ AL.toList sl
+
+-- | 'updateDefaultOptions' updates the options in the default
+-- 'Section' of the 'Config' with the given function, which is similar
+-- to an 'UpdateOption' without the 'SectionName' argument.
+updateDefaultOptions :: (OptionName -> Option -> Maybe SectionItem)
+                     -> Config
+                     -> Config
+updateDefaultOptions f (Config d sl) = Config (updateSectionOptions f d) sl
+
+-- | 'updateDefaultOptions' updates the values in the default
+-- 'Section' of the 'Config' with the given function, which is similar
+-- to an 'UpdateValue' without the 'SectionName' argument.
+updateDefaultValues :: (OptionName -> Option -> Maybe Option) -> Config -> Config
+updateDefaultValues f (Config d sl) = Config (updateSectionValues f d) sl
+
+updateSectionOptions :: (OptionName -> Option -> Maybe SectionItem)
+                     -> Section
+                     -> Section
+-- | 'updateSectionOptions' updates the options in the named 'Section'
+-- 'Section' of the 'Config' with the given function, which is similar
+-- to an 'UpdateOption' without the 'SectionName' argument.
+updateSectionOptions f os = AL.fromList . map (updateOption f) $ AL.toList os where
+    updateOption f o@(n, v) = fromMaybe o $ f n v
+
+-- | 'updateSectionValues' updates the values in the named 'Section'
+-- of the 'Config' with the given function, which is similar to an
+-- 'UpdateValue' without the 'SectionName' argument.
+updateSectionValues :: (OptionName -> Option -> Maybe Option) -> Section -> Section
+updateSectionValues f s = updateSectionOptions (updateValue f) s where
+    updateValue f n o = (n ,) <$> f n o
+
+
+-- Internal routines for getting options. Users should use
+-- getValue
+getOptions :: Section -> OptionName -> [Option]
+getOptions os k =  map snd . filter (\(n, _) -> n == k) $ AL.toList os
+
+getOption :: Section -> OptionName -> Maybe Option
+getOption os n = AL.lookupFirst n os
+
+
+-- And output things
+formatOption :: SectionItem -> String
+formatOption (name, val) = name ++ "=" ++ val
+
+formatSection :: ConfigItem -> String
+formatSection (name, section) =
+  unlines $ ("[" ++ name ++ "]") : (map formatOption $ AL.toList section)
+
+-- | 'formatConfig' converts a 'Config' to a 'String' representation
+-- for use in a @.ini@ file.
+formatConfig :: Config -> String
+formatConfig (Config options sections) =
+  intercalate "\n" $
+                  (unlines . map formatOption $ AL.toList options)
+    :(map formatSection $ AL.toList sections)
+
+-- | 'writeConfigFile' formats a 'Config' and writes it to 'FilePath'.
+writeConfigFile :: FilePath -> Config -> IO ()
+writeConfigFile f c = writeFile f $ formatConfig c
+
+-- | 'hwriteConfigFile' formats a 'Config' and writes it to a 'Handle'.
+hWriteConfig :: Handle -> Config -> IO ()
+hWriteConfig h c = hPutStr h $ formatConfig c
+
+-- | 'writeConfig' formats a 'Config' and writes it to 'stdout'.
+writeConfig :: Config -> IO ()
+writeConfig = hWriteConfig stdout
+
+
+-- | 'parseConfig' parses a 'String' into a 'Config', returning the
+-- 'Config' wrapped in a a 'Result'
+parseConfig :: String -> Result Config
+parseConfig = parseString parser mempty
+
+-- | 'parseFile' parses the named @.ini@ file, sending diagnostic messages to
+-- the console.
+parseFile :: FilePath -> IO (Maybe Config)
+parseFile = parseFromFile parser
+
+-- | 'parseFileEx' parses the named @.ini@ file, returning either the
+-- 'Config' or diagnostic messages in the 'Result'.
+parseFileEx :: FilePath -> IO (Result Config)
+parseFileEx = parseFromFileEx parser
+
+parser :: (Monad m, TokenParsing m) => m Config
+parser = runUnlined
+         $ optional lineSep *>
+         (Config <$> (AL.fromList <$> sepEndBy (try optionLine) lineSep)
+                 <*> (AL.fromList <$> many sectionLine))
+         <* many (char '\0') -- Deal with file system oddness
+         <* eof
+         <?> "config"
+
+sectionLine :: (Monad m, TokenParsing m) => m ConfigItem
+sectionLine = (,) <$> sectionName <* lineSep
+                  <*> (AL.fromList <$> sepEndBy1 (try optionLine) lineSep)
+              <?> "section"
+
+sectionName :: (Monad m, TokenParsing m) => m String
+sectionName = whiteSpace
+              *> char '['
+              *> ((:) <$> satisfy (\c -> c /= ']' && isPrint c)
+                      <*> manyTill printing (char ']')
+                      <?> "section name") 
+              <* many anyChar
+              <?> "section start line"
+
+optionLine :: (Monad m, TokenParsing m) => m SectionItem
+optionLine = (,) <$> (strip <$> optionName)
+                 <*> (unComment <$> many printing <?> "option value")
+             <?> "option line" 
+  where -- Handle some context-sensitive bits of the syntax
+    strip = reverse . dropWhile isSpace . reverse
+    unComment = dropWhile isSpace . reverse . dropWhile isSpace . unComment' ""
+    unComment' r v | null v            = r
+                   | isPrefixOf " ;" v = r
+                   | otherwise         = unComment' (head v:r) (tail v)
+
+optionName :: (Monad m, TokenParsing m) => m OptionName
+optionName = whiteSpace *> ((:) <$> satisfy (\c -> isPrint c && c /= '[')
+                                <*> manyTill printing (oneOf ":="))
+             <?> "option name"
+
+-- Lines are separated by a newline and zero or more comment lines.
+lineSep :: (Monad m, TokenParsing m) => m ()
+lineSep = some (whiteSpace <* optional (oneOf ";#" <* many anyChar) <* newline)
+          *> pure ()
+          <?> "line separator"
+
+-- Some basic character types
+printing, anyChar :: CharParsing m => m Char
+printing = satisfy isPrint
+anyChar = satisfy (/= '\n')
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2015
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Mike Meyer or the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/UnitTest.hs b/UnitTest.hs
new file mode 100644
--- /dev/null
+++ b/UnitTest.hs
@@ -0,0 +1,241 @@
+{-# Language OverloadedStrings, FlexibleInstances #-}
+
+module Main where
+
+import Control.DeepSeq (deepseq)
+import Control.Exception (ErrorCall(ErrorCall), evaluate)
+import Data.List (isInfixOf)
+import Data.Monoid ((<>), mconcat, mempty)
+import qualified Data.Map as M
+import Text.Trifecta (TokenParsing, Result(Success, Failure))
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.HUnit.Tools
+
+import Data.AList as AL
+import Data.Ini.List as IL
+
+
+-- Add some Eq instances. Not in general valid, but we want them for testing.
+instance Eq a => Eq (Result a) where
+    (Success a) == (Success b) = a == b
+    _ == _ = False		-- Can't test failures for equality.
+
+instance (Eq k, Eq v) => Eq (AL.AList k v) where
+    a == b = AL.toList a == AL.toList b
+
+instance Eq Config where
+    a == b = getDefault a == getDefault b && IL.toList a == IL.toList b
+
+testError msg err code =
+    testCase msg . assertRaises "Check error" (ErrorCall err) . evaluate
+                 $ deepseq (show code) ()
+
+
+testFailure msg err code =
+    let expect = "Expected failure containg: " ++ err ++ "\n"
+    in testCase msg $ case code of
+                        Success _ -> assertFailure $ expect ++ "but succeeded"
+                        Failure x -> let res = show x
+                                     in assertBool (expect ++ "but got " ++ res)
+                                        . isInfixOf err $ res
+
+
+trues = ["1", "yes", "on", "enabled", "true"]
+falses = ["0", "no", "off", "disabled", "false"]
+section1 = IL.fromList [("Value1", "23"), ("Value1", "35"), ("Value 2", "foo bar")]
+section2 = IL.fromList [("Value1", "34"), ("Value2", "")]
+def = IL.fromList $ ("Value1", "12"):(map (\x -> (x, x)) $ trues ++ falses)
+test = config def [("Section1", section1),
+                   ("Section 2", section2),
+                   ("Section1", section2)]
+
+
+updateValue "Value1" _ = Just "Updated"
+updateValue _        _ = Nothing
+updateOption "Value1" _ = Just ("Changed", "New")
+updateOption _        _ = Nothing
+def2 = IL.fromList $ ("Value1", "Updated"):(map (\x -> (x, x)) $ trues ++ falses)
+updateSection "Section 2" o v = updateValue o v
+updateSection _ _ _ = Nothing
+def3 = IL.fromList $ ("Changed", "New"):(map (\x -> (x, x)) $ trues ++ falses)
+updateSO "Section 2" o v = updateOption o v
+updateSO _ _ _ = Nothing
+
+testString = "Value1=12\n1=1\nyes=yes\non=on\nenabled=enabled\ntrue=true\n0=0\nno=no\noff=off\ndisabled=disabled\nfalse=false\n\n[Section1]\nValue1=23\nValue1=35\nValue 2=foo bar\n\n[Section 2]\nValue1=34\nValue2=\n\n[Section1]\nValue1=34\nValue2=\n"
+
+commentString = ";c\n#c\n ;c\n #c\nValue1=12\n\n1=1 ;c \n#c\nyes=yes\n;\non=on\n ; \nenabled=enabled\n # \ntrue=true\n0=0\nno=no\noff=off\ndisabled=disabled\nfalse=false\n\n[Section1] garbage\nValue1=23\nValue1=35\nValue 2=foo bar ; c\n\n[Section 2]\n\nValue1=34\nValue2=\n\n[Section1]\n;c\n#c\nValue1=34\nValue2=\n"
+
+
+testAList = testGroup "AList" [
+    testGroup "isEmpty" [
+        testCase "True" . assertBool "" . isEmpty $ mempty,
+        testCase "False" . assertBool "" . not . isEmpty $ AL.fromList [(1, 2)]
+        ],
+    testGroup "lookupAll" [
+        testCase "empty" $ [] @=? lookupAll 1 (AL.fromList [(2,3)]),
+        testCase "singleton" $ [3] @=? lookupAll 2 (AL.fromList [(2,3)]),
+        testCase "list"
+            $ [3, 5] @=? lookupAll 2 (AL.fromList [(1, 2), (2,3), (3, 4), (2,5)])
+        ], 
+    testGroup "lookupFirst" [
+        testCase "Something" $ Just 5 @=? lookupFirst 3 (AL.fromList [(3, 5)]),
+        testCase "Nothing" $ Nothing @=? lookupFirst 3 (AL.fromList [(2, 5)])
+        ],
+    testGroup "lookupBy" [
+        testCase "empty" $ mempty @=? AL.lookupBy odd (AL.fromList [(2, 3)]),
+        testCase "singleton"
+            $ AL.fromList [(2, 3)] @=? AL.lookupBy even (AL.fromList [(2, 3)]),
+        testCase "list"
+            $ lookupBy even (AL.fromList [(1, 2), (2,3), (3, 4), (4, 5)])
+              @?= AL.fromList [(2, 3), (4, 5)]
+        ],
+    testGroup "member" [
+        testCase "True" . assertBool "" . member 1 $ AL.fromList [(1, 2)],
+        testCase "False" . assertBool "" . not . member 1 $ AL.fromList [(3, 2)]
+        ],
+    testGroup "values" [
+        testCase "empty" $ [] @=? AL.values (mempty :: AList Int Int),
+        testCase "singleton" $ [3] @=? AL.values (AL.fromList [(2, 3)]),
+        testCase "list" $ [3, 5] @=? AL.values (AL.fromList [(2, 3), (4, 5)])
+        ],
+    testGroup "keys" [
+        testCase "empty" $ [] @=? AL.keys (mempty :: AList Int Int),
+        testCase "singleton" $ [2] @=? AL.keys (AL.fromList [(2, 3)]),
+        testCase "list" $ [2, 4] @=? AL.keys (AL.fromList [(2, 3), (4, 5)])
+        ],
+    testGroup "insert" [
+        testCase "empty" $ AL.fromList [(1, 2)] @=? insert 1 2 mempty,
+        testCase "full"
+            $ AL.fromList [(1, 2), (3, 4)] @=? insert 1 2 (AL.fromList [(3, 4)])
+        ],
+    testGroup "append" [
+        testCase "empty" $ AL.fromList [(1, 2)] @=? append 1 2 mempty,
+        testCase "full"
+            $ AL.fromList [(3, 4), (1, 2)] @=? append 1 2 (AL.fromList [(3, 4)])
+        ],
+    testGroup "deleteAll" [
+        testCase "empty" $ mempty @=? deleteAll 2 (AL.fromList [(2,3)]),
+        testCase "singleton"
+            $ AL.fromList[(2, 3)] @=? deleteAll 1 (AL.fromList [(2,3)]),
+        testCase "list"
+            $ deleteAll 2 (AL.fromList [(1, 2), (2,3), (3, 4), (2,5)])
+              @?= AL.fromList [(1, 2), (3, 4)]
+        ],
+    testGroup "deleteFirst" [
+        testCase "empty" $ mempty @=? deleteAll 2 (AL.fromList [(2,3)]),
+        testCase "singleton"
+            $ AL.fromList[(2, 3)] @=? deleteAll 1 (AL.fromList [(2,3)]),
+        testCase "list"
+            $ deleteFirst 2 (AL.fromList [(1, 2), (2,3), (3, 4), (2,5)])
+              @?= AL.fromList [(1, 2), (3, 4), (2, 5)]
+        ],
+    testGroup "deleteBy" [
+        testCase "empty" $ mempty @=? AL.deleteBy even (AL.fromList [(2, 3)]),
+        testCase "singleton"
+            $ AL.fromList [(2, 3)] @=? AL.deleteBy odd (AL.fromList [(2, 3)]),
+        testCase "list"
+            $ deleteBy odd (AL.fromList [(1, 2), (2,3), (3, 4), (4, 5)])
+              @?= AL.fromList [(2, 3), (4, 5)]
+        ],
+    testGroup "convert" [
+        testCase "fromList"
+            $ (AL.insert 1 2 . AL.insert 3 4 $ AL.insert 5 6 mempty)
+              @=? AL.fromList [(1, 2), (3, 4), (5, 6)],
+        testCase "toList"
+            $ AL.toList (AL.insert 1 2 . AL.insert 3 4 $ AL.insert 5 6 mempty)
+              @=? [(1, 2), (3, 4), (5, 6)],
+        testCase "toMap" $ M.fromList [(1, 2), (3, 4), (5, 6)]
+                 @=? AL.toMap (AL.fromList[(3, 4), (1, 2), (5, 6)]),
+        -- This probably depends on the Map implementation 
+        testCase "fromMap"  $ AL.fromMap (M.fromList [(1, 2), (3, 4), (5, 6)])
+                 @=? (AL.fromList[(1, 2), (3, 4), (5, 6)])
+        ]
+    ]
+
+testIO = testGroup "IO" [
+    testCase "format" $ testString @=? formatConfig test,
+    testCase "parse" $ Success test @=? parseConfig testString,
+    testCase "comments" $ Success test @=? parseConfig commentString,
+    testGroup "errors" [
+        testFailure "Missing config" "config" $ parseConfig "=23\n",
+        testFailure "Missing section name" "section name" $ parseConfig "[]\nx=23",
+        testFailure "Empty section" "option line" $ parseConfig "[a]\n[b]\n",
+        testFailure "Broken section name" "\"]\"" $ parseConfig "[a\n",
+        testFailure "Missing option name" "new-line" $ parseConfig "[a]\n=23"
+        ]
+    ]
+
+testCreate = testGroup "create" [
+    -- Ought to test config/section/option, but without access to the
+    -- constuctors, that would be pointless.
+    testCase "default" $ section1 @=? (getDefault $ setDefault test section1),
+    testCase "section <+"
+        $ section1 @=? mempty <+ ("Value1", "23") <+ ("Value1", "35")
+                              <+ ("Value 2", "foo bar"),
+    testCase "section +>"
+        $ section1 @=? ("Value 2", "foo bar") +> ("Value1", "35")
+                       +> ("Value1", "23") +> mempty,
+    testCase "config <+"
+        $ test @=? setDefault mempty def <+ ("Section1", section1)
+                                         <+ ("Section 2", section2)
+                                         <+ ("Section1", section2),
+    testCase "config +>"
+        $ test @=? ("Section1", section2) +> ("Section 2", section2)
+                   +> ("Section1", section1) +> setDefault mempty def,
+    testGroup "update" [
+        testCase "defaultValues"
+            $ def2 @=? getDefault (updateDefaultValues updateValue test),
+        testCase "sectionValues" $ def2 @=? updateSectionValues updateValue def,
+        testCase "configValues"
+            $ Just ("Updated" :: String) @=? get (updateValues updateSection test)
+                                                 (Just "Section 2") "Value1",
+        testCase "defaultOptions" 
+            $ def3 @=? getDefault(updateDefaultOptions updateOption test),
+        testCase "sectionOptions" $ def3 @=? updateSectionOptions updateOption def,
+        testCase "configOptions"
+            $ Just ("New" :: String) @=? get (updateOptions updateSO test)
+                                             (Just "Section 2") "Changed"
+        ]
+    ]
+
+testGet = testGroup "gets" [
+    testGroup "get sections" [
+        testCase "Default" $ def @=? getDefault test,
+        testCase "Section 1" $ Just section2 @=? getSection test "Section 2",
+        testCase "Section 2" $ Nothing @=? getSection test "NoSection",
+        testCase "Sections 1" $ [] @=? getSections test "NoSection",
+        testCase "Sections 2"
+                 $ [section1, section2] @=? getSections test "Section1",
+        testCase "SectionsBy" $
+            ["Section 2"] @=? map fst (getSectionsBy test $ elem ' ')
+        ],
+    testGroup "get" [
+        testCase "Nothing" $ (Nothing :: Maybe Int) @=? get test Nothing "NoValue",
+        testCase "String"
+                 $ (Just "12" :: Maybe String) @=? get test Nothing "Value1",
+        testGroup "Bool" $ (testError "Error" "couldn't parse '12' as Bool"
+                           $ (get test Nothing "Value1" :: Maybe Bool)):
+                  map (\x -> testCase x $ Just True @=? get test Nothing x) trues
+                  ++ map (\x -> testCase x $ Just False @=? get test Nothing x)
+                         falses,
+        testGroup "List" [
+            testCase "[]"
+                     $ Just ([] :: [Int]) @=? get test (Just "Section1") "NoValue",
+            testCase "[x]"
+                     $ Just ([23, 35] :: [Int])
+                       @=? get test (Just "Section1") "Value1"
+           ],
+        testGroup "Num" [
+            testCase "Int" $ Just (12 :: Int) @=? get test Nothing "Value1",
+            testCase "Float" $ Just (12.0 :: Float) @=? get test Nothing "Value1",
+            testError "Error" "couldn't parse 'false'"
+                      $ (get test Nothing "false" :: Maybe Int)
+           ]
+        ]
+    ]
+     
+
+main :: IO ()
+main = defaultMain $ testGroup "All" [testAList, testGet, testCreate, testIO]
diff --git a/inilist.cabal b/inilist.cabal
new file mode 100644
--- /dev/null
+++ b/inilist.cabal
@@ -0,0 +1,36 @@
+name:                inilist
+version:             0.1.0.0
+synopsis:            Processing for .ini files with duplicate sections and options
+description:         Please see README.md
+homepage:            https://chiselapp.com/user/mwm/repository/inilist
+license:             BSD3
+license-file:        LICENSE
+author:              Mike Meyer
+maintainer:          mwm@mired.org
+-- copyright:           
+category:            Library
+build-type:          Simple
+-- extra-source-files:  
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      .
+  exposed-modules:     Data.Ini.List, Data.AList
+  build-depends:       base >= 4.7 && < 5, safe >= 0.3.9, trifecta >= 1.5.1.3,
+                       bifunctors >= 4.2.1, containers >= 0.5.5.1
+  default-language:    Haskell2010
+
+test-suite inilist-unittest
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      .
+  main-is:             UnitTest.hs
+  build-depends:       tasty >= 0.8, testpack >= 2.1.2.1,
+                       base >= 4.7, inilist, safe >= 0.3.9, trifecta >= 1.5.1.3,
+                       deepseq >= 1.3.0.2, HUnit >= 1.2, tasty-hunit >= 0.9,
+                       bifunctors >= 4.2.1, containers >= 0.5.5.1
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     fossil
+  location: 
