diff --git a/ChangeLog.rst b/ChangeLog.rst
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -1,3 +1,8 @@
+1.0.0
+=====
+
+- Redo of the entire API
+
 0.2.2
 =====
 
diff --git a/library/System/OsRelease.hs b/library/System/OsRelease.hs
--- a/library/System/OsRelease.hs
+++ b/library/System/OsRelease.hs
@@ -1,114 +1,192 @@
-{-# LANGUAGE FlexibleContexts, TypeSynonymInstances, FlexibleInstances, GeneralizedNewtypeDeriving, CPP #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeOperators #-}
 
+-- | A module to retrieve os-release information according to the
+-- freedesktop standard:
+-- https://www.freedesktop.org/software/systemd/man/os-release.html
+--
+-- Usage example:
+--
+-- @
+-- do
+--   Just (OsRelease {..}) <- fmap osRelease <$\> parseOsRelease
+--   putStrLn name
+-- @
 module System.OsRelease
-    ( parseOs
-    , readOs
-    , readOs'
-    , OsReleaseValue (..)
-    , OsReleaseKey (..)
-    , OsReleaseLine
-    , OsRelease
-    , OsReleaseError
-    )
+  (
+  -- * data types
+    OsReleaseResult(..)
+  , OsRelease(..)
+
+  -- * read/parse os-release
+  , parseOsRelease
+  , readOsRelease
+
+  -- * defaults
+  , defaultOsRelease
+  , defaultAssignments
+
+  -- * low-level
+  , parseAssignments
+  , parseAssignment
+  , getAllAssignments
+  , getOsRelease
+  , parseOsRelease'
+  )
 where
 
-import Text.ParserCombinators.Parsec
-import Text.Parsec.Prim hiding (try)
-import Data.Map.Lazy hiding (foldl)
-import Data.Functor.Identity
-import Data.String
-import Data.Monoid
-import Data.Either
+import           System.OsRelease.Megaparsec
 
-#if MIN_VERSION_base(4,8,0)
--- note: using hscpp it should be possible to use `#if not COND`
-#else
-import Control.Applicative ((<$>))
-#endif
+import           Control.Applicative
+import           Control.Monad
+import           Control.Exception.Safe
+import           Data.Aeson
+import           Data.Aeson.TH
+import           Data.Char
+import           Data.Either
+import           Data.List
+import           Data.Maybe
+import           Data.Void
+import           GHC.Generics
+import           Prelude                 hiding ( id
+                                                )
 
-import Control.Monad
-import qualified Control.Exception as E
+import qualified Data.HashMap.Strict           as HM
+import qualified Data.Text                     as T
+import qualified Text.Megaparsec               as MP
 
-type OsReleaseLine  = (OsReleaseKey, OsReleaseValue)
-newtype OsReleaseKey   = OsReleaseKey String
-    deriving (Ord, Eq, IsString, Show)
 
-newtype OsReleaseValue = OsReleaseValue String
-    deriving (IsString, Eq, Show)
+data OsReleaseResult = OsReleaseResult {
+    osRelease :: !OsRelease
+  , unknown_fields :: [(String, String)]
+  , parse_errors :: [MP.ParseError String Void]
+} deriving (Show)
 
-type OsRelease      = Map OsReleaseKey OsReleaseValue
 
-class Parsable a where
-    parser :: ParsecT String () Identity a
+-- | All the explicitly documented fields of @os-release@.
+data OsRelease = OsRelease {
+    name :: !(String)
+  , version :: !(Maybe String)
+  , id :: !(String)
+  , id_like :: !(Maybe String)
+  , version_codename :: !(Maybe String)
+  , version_id :: !(Maybe String)
+  , pretty_name :: !(String)
+  , ansi_color :: !(Maybe String)
+  , cpe_name :: !(Maybe String)
+  , home_url :: !(Maybe String)
+  , documentation_url :: !(Maybe String)
+  , support_url :: !(Maybe String)
+  , bug_report_url :: !(Maybe String)
+  , privacy_policy_url :: !(Maybe String)
+  , build_id :: !(Maybe String)
+  , variant :: !(Maybe String)
+  , variant_id :: !(Maybe String)
+  , logo :: !(Maybe String)
+} deriving (Generic, Show)
 
-instance Parsable OsReleaseKey where
-    parser = OsReleaseKey <$> many1 (alphaNum <|> char '_')
 
-instance Parsable OsReleaseValue where
-    parser = OsReleaseValue <$> (qVal <|> nqVal)
-      where
-        qVal :: Parser String
-        qVal  = do
-            quote <- oneOf "'\""
-            value <- manyTill (qValInside quote) (char quote)
-            noJunk
-            return (foldl mappend "" value)
+class GetRecords a where
+  getRecords :: a -> [String]
 
-        noJunk = try . lookAhead $ eof <|> (void newline)
+instance {-# OVERLAPPABLE #-} GetRecords (f p) => GetRecords (M1 i c f p) where
+  getRecords (M1 x) = getRecords x
 
-        qValInside :: Char -> ParsecT String () Identity String
-        qValInside quote = (qSpecial quote) <|> (:[]) <$> (noneOf $ specials quote)
+instance {-# OVERLAPPING #-} Selector c => GetRecords (M1 S c f p) where
+  getRecords x = [selName x]
 
-        qSpecial :: Char -> ParsecT String () Identity String
-        qSpecial quote = (\x -> [x!!1]) <$>
-            foldl1 (<|>) [try (string $ "\\" <> [x]) | x <- specials quote]
+instance (GetRecords (a p), GetRecords (b p)) => GetRecords ((a :*: b) p) where
+  getRecords (a :*: b) = getRecords a ++ getRecords b
 
-        specials :: Char -> [Char]
-        specials quote =
-            [ quote
-            , '\\'
-            , '$'
-            , '`'
-            ]
 
-        nqVal :: Parser String
-        nqVal = do
-            x <- many alphaNum
-            noJunk
-            return x
 
-instance Parsable OsReleaseLine where
-    parser = do
-        var <- parser :: ParsecT String () Identity OsReleaseKey
-        _ <- char '='
-        val <- parser :: ParsecT String () Identity OsReleaseValue
-        return (var, val)
+-- | The defaults as per the spec:
+--
+-- @
+-- NAME=Linux
+-- ID=linux
+-- PRETTY_NAME=Linux
+-- @
+defaultOsRelease :: OsRelease
+defaultOsRelease = OsRelease { name               = "Linux"
+                             , version            = Nothing
+                             , id                 = "linux"
+                             , id_like            = Nothing
+                             , version_codename   = Nothing
+                             , version_id         = Nothing
+                             , pretty_name        = "Linux"
+                             , ansi_color         = Nothing
+                             , cpe_name           = Nothing
+                             , home_url           = Nothing
+                             , documentation_url  = Nothing
+                             , support_url        = Nothing
+                             , bug_report_url     = Nothing
+                             , privacy_policy_url = Nothing
+                             , build_id           = Nothing
+                             , variant            = Nothing
+                             , variant_id         = Nothing
+                             , logo               = Nothing
+                             }
 
-instance Parsable OsRelease where
-    parser = fromList <$> (sepEndBy (parser :: ParsecT String () Identity OsReleaseLine) newline)
+-- | Like `defaultOsRelease`, except as key-value pair.
+defaultAssignments :: [(String, String)]
+defaultAssignments =
+  [("NAME", "Linux"), ("ID", "linux"), ("PRETTY_NAME", "Linux")]
 
 
-readOs :: IO (Either OsReleaseError OsRelease)
-readOs = do
-    xs <- readOs' ["/etc/os-release", "/usr/lib/os-release"]
-    case xs of
-        Left  e -> return $ Left e
-        Right x -> return . h $ parseOs x
-  where
-    h (Left  e) = Left $ OsReleaseParseError e
-    h (Right x) = Right x
+-- | Get all allAssignments as @(key, val)@ from the @os-release@
+-- file contents.
+getAllAssignments :: String  -- ^ file contents of os-release
+                  -> [Either (MP.ParseError String Void) (String, String)]
+getAllAssignments = fromRight [] . MP.parse parseAssignments "os-release"
 
-data OsReleaseError = OsReleaseError String
-                    | OsReleaseParseError ParseError
-    deriving (Show)
 
-readOs' :: [FilePath] -> IO (Either OsReleaseError String)
-readOs' fs = do
-    xs <- mapM (E.try . readFile) fs :: IO [Either E.IOException String]
-    return . h $ rights xs
-  where
-    h [] = Left . OsReleaseError $ "Neither of " <> unwords fs <> " could be read"
-    h (x:_) = Right x
+-- | Parse the assignments into `OsRelease`. This is merged with the
+-- defaults as per the spec. In case of no assignments, also returns
+-- the defaults.
+getOsRelease :: [(String, String)]  -- ^ assignments
+             -> OsRelease
+getOsRelease =
+  (\case
+      Error   _ -> defaultOsRelease
+      Success v -> v
+    )
+    . fromJSON
+    . Object
+    . (\x -> HM.union x (HM.fromList . aesonify $ defaultAssignments))
+    . HM.fromList
+    . aesonify
+ where
+  aesonify = fmap (\(k, v) -> (T.toLower . T.pack $ k, String . T.pack $ v))
 
-parseOs :: String -> Either ParseError OsRelease
-parseOs xs = parse (parser :: ParsecT String () Identity OsRelease) "os-release" xs
+
+-- | Tries to read @\"\/etc\/os-release\"@ and @\"\/usr\/lib\/os_release\"@ in order.
+--
+-- Throws @IOError@ if both files could not be read.
+readOsRelease :: IO String
+readOsRelease = readFile "/etc/os-release" <|> readFile "/usr/lib/os-release"
+
+
+-- | Tries to read @\"\/etc\/os-release\"@ and @\"\/usr\/lib\/os_release\"@ in order
+-- and parses into `OsReleaseResult`. Returns @Nothing@ if both files could
+-- not be read.
+parseOsRelease :: IO (Maybe OsReleaseResult)
+parseOsRelease =
+  handleIO (\_ -> pure Nothing) . fmap (Just . parseOsRelease') $ readOsRelease
+
+
+-- | Like `parseOsRelease`, except taking the input String explicitly.
+-- Primarily for tests.
+parseOsRelease' :: String -> OsReleaseResult
+parseOsRelease' s =
+  let (errs, ass) = partitionEithers . getAllAssignments $ s
+      osr         = getOsRelease ass
+      unknown_fields' =
+          HM.toList
+            . foldr (\x y -> HM.delete (fmap toUpper x) y) (HM.fromList ass)
+            $ (init . getRecords . from $ defaultOsRelease)
+  in  OsReleaseResult osr unknown_fields' errs
+
+
+deriveJSON defaultOptions ''OsRelease
diff --git a/library/System/OsRelease/Megaparsec.hs b/library/System/OsRelease/Megaparsec.hs
new file mode 100644
--- /dev/null
+++ b/library/System/OsRelease/Megaparsec.hs
@@ -0,0 +1,102 @@
+{-# LANGUAGE CPP #-}
+
+module System.OsRelease.Megaparsec where
+
+import           Control.Applicative
+import           Control.Monad
+#if !MIN_VERSION_base(4,13,0)
+import           Control.Monad.Fail             ( MonadFail )
+#endif
+import           Data.Char
+import           Data.Functor
+import           Data.Void
+
+import qualified Text.Megaparsec               as MP
+import qualified Text.Megaparsec.Char          as MP
+
+
+-- | Parse the entire file, handling newlines and comments gracefully.
+--
+-- This parser generally shouldn't fail, but instead report a failed
+-- parsed line as @Left@ value.
+parseAssignments :: MP.Parsec
+                      Void
+                      String
+                      [Either (MP.ParseError String Void) (String, String)]
+parseAssignments =
+  (\xs x -> join xs ++ x) <$> many (line MP.eol) <*> line MP.eof
+ where
+  line eol = choice'
+    [ comment $> []
+    , blank $> []
+    , fmap
+      (: [])
+      ( MP.withRecovery (\e -> parseUntil eol $> Left e)
+      . fmap Right
+      $ (parseAssignment <* eol)
+      )
+    ]
+   where
+    comment = pWs *> MP.char '#' *> parseUntil eol *> eol
+    blank   = pWs *> eol
+
+
+-- | Parse a single line assignment and extract the right hand side.
+-- This is only a subset of a shell parser, refer to the spec for
+-- details.
+parseAssignment :: MP.Parsec Void String (String, String)
+parseAssignment =
+  (,) <$> (pWs *> key) <*> (MP.char '=' *> (MP.try qval <|> mempty) <* pWs)
+ where
+  dropSpace :: String -> String
+  dropSpace = reverse . dropWhile (\x -> x == ' ' || x == '\t') . reverse
+
+  key :: MP.Parsec Void String String
+  key = some (MP.try MP.alphaNumChar <|> MP.char '_')
+
+  qval :: MP.Parsec Void String String
+  qval = do
+    c <- MP.lookAhead MP.printChar
+    case c of
+      ' '  -> pure ""
+      '"'  -> MP.char c *> val c <* MP.char c
+      '\'' -> MP.char c *> val c <* MP.char c
+      -- no quote, have to drop trailing spaces
+      _    -> fmap
+        dropSpace
+        (some $ MP.satisfy (\x -> isAlphaNum x || (x `elem` ['_', '-', '.']))) -- this is more lax than the spec
+  val :: Char -> MP.Parsec Void String String
+  val !q = many (qspecial q <|> MP.noneOf (specials q)) -- noneOf may be too lax
+
+  qspecial :: Char -> MP.Parsec Void String Char
+  qspecial !q =
+    fmap (!! 1)
+      . (\xs -> choice' xs)
+      . fmap (\s -> MP.try . MP.chunk $ ['\\', s])
+      $ (specials q)
+
+  specials :: Char -> [Char]
+  specials !q = [q, '\\', '$', '`']
+
+
+parseUntil :: MP.Parsec Void String a -> MP.Parsec Void String String
+parseUntil !p = do
+  (MP.try (MP.lookAhead p) $> [])
+    <|> (do
+          c  <- MP.anySingle
+          c2 <- parseUntil p
+          pure ([c] `mappend` c2)
+        )
+
+
+-- | Parse one or more white spaces or tabs.
+pWs :: MP.Parsec Void String ()
+pWs = many (MP.satisfy (\x -> x == ' ' || x == '\t')) $> ()
+
+
+-- | Try all parses in order, failing if all failed. Also fails
+-- on empty list.
+choice' :: (MonadFail f, MP.MonadParsec e s f) => [f a] -> f a
+choice' = \case
+  [] -> fail "Empty list"
+  xs -> foldr1 (\x y -> MP.try x <|> MP.try y) xs
diff --git a/os-release.cabal b/os-release.cabal
--- a/os-release.cabal
+++ b/os-release.cabal
@@ -1,94 +1,131 @@
-author:         Jan Matějka
-category:       System
-license-file:   LICENSE
-build-type:     Simple
-cabal-version:  >= 1.16
-copyright:      2014 Jan Matějka <yac@blesmrt.net>
-license:        BSD3
-maintainer:     <yac@blesmrt.net>
-homepage:       https://github.com/yaccz/os-release
-name:           os-release
-synopsis:       /etc/os-release helpers
-version:        0.2.2
-description:    /etc/os-release helpers
-extra-doc-files: ChangeLog.rst README.rst
+cabal-version:      3.0
+author:             Jan Matějka
+category:           System
+license-file:       LICENSE
+build-type:         Simple
+copyright:          2014 Jan Matějka <yac@blesmrt.net>
+license:            BSD-3-Clause
+maintainer:         Julian Ospald <hasufell@posteo.de>
+homepage:           https://github.com/yaccz/os-release
+name:               os-release
+synopsis:           /etc/os-release helpers
+version:            1.0.0
+description:
+  \/etc\/os-release helpers as per the freedesktop spec: https://www.freedesktop.org/software/systemd/man/os-release.html
 
-Flag devel
-  Description:  Enables -Werror
-  Default:      False
-  Manual:       True
+extra-doc-files:
+  ChangeLog.rst
+  README.rst
 
+extra-source-files: tests/Golden/data/*.golden tests/Golden/data/*.in
+
+flag devel
+  description: Enables -Werror
+  default:     False
+  manual:      True
+
 source-repository head
   type:     git
   location: https://github.com/yaccz/os-release.git
 
-library
-    build-depends:
-        base < 5
-      , parsec
-      , containers
-      , parsec
-      , containers
-      , transformers
-    default-language:   Haskell2010
-    exposed-modules:    System.OsRelease
+common base
+  build-depends: base >=4.11 && <5
+
+common bytestring
+  build-depends: bytestring
+
+common aeson
+  build-depends: aeson >=1.4
+
+common filepath
+  build-depends: filepath >=1.4.2.1
+
+common hspec
+  build-depends: hspec >=2.7.1
+
+common hspec-megaparsec
+  build-depends: hspec-megaparsec >=2.1.0
+
+common megaparsec
+  build-depends: megaparsec >=8.0.0
+
+common pretty-simple
+  build-depends: pretty-simple >=1.0.0.0
+
+common safe-exceptions
+  build-depends: safe-exceptions >=0.1.7.0
+
+common tasty
+  build-depends: tasty >=1.3
+
+common tasty-golden
+  build-depends: tasty-golden >=2.3.4
+
+common tasty-hspec
+  build-depends: tasty-hspec >=1.1.5.1
+
+common text
+  build-depends: text >=1.2
+
+common unordered-containers
+  build-depends: unordered-containers >=0.2.10.0
+
+common config
+  default-language:   Haskell2010
+
+  if flag(devel)
     ghc-options:
-        -Wall
-    ghc-prof-options:
-        -auto-all
-        -prof
-    hs-source-dirs:     library
+      -Werror -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns
+      -fwarn-incomplete-record-updates
 
-test-suite tests
-    build-depends:
-        base < 5
-      , os-release
-      , hspec
-      , parsec
-      , containers
-      , transformers
-      , temporary
-    default-language:
-        Haskell2010
+  else
+    ghc-options:
+      -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns
+      -fwarn-incomplete-record-updates
 
-    if flag(devel)
-       ghc-options: -Wall -Werror
-    else
-       ghc-options: -Wall
-    hs-source-dirs:
-        library
-        tests/unit
-    main-is:
-        Spec.hs
-    other-modules:
-        System.OsRelease
-        OsReleaseSpec
-    type:
-        exitcode-stdio-1.0
+  default-extensions:
+    BangPatterns
+    LambdaCase
+    OverloadedStrings
+    QuasiQuotes
+    TemplateHaskell
+    TupleSections
+    TypeFamilies
 
-test-suite documentation
-    build-depends:
-        base < 5
-      , process == 1.*
-      , regex-compat == 0.*
-    default-language:
-        Haskell2010
-    hs-source-dirs:
-        tests
-    main-is:
-        Haddock.hs
-    type:
-        exitcode-stdio-1.0
+library
+  import:
+    config
+    , base
+    , aeson
+    , megaparsec
+    , safe-exceptions
+    , text
+    , unordered-containers
 
-test-suite style
-    build-depends:
-        base < 5
-      , hlint == 1.*
-    default-language:
-        Haskell2010
-    hs-source-dirs:
-        tests
-    main-is:
-        HLint.hs
-    type:
-        exitcode-stdio-1.0
+  exposed-modules: System.OsRelease
+  other-modules:   System.OsRelease.Megaparsec
+  hs-source-dirs:  library
+
+test-suite tests
+  import:
+    config
+    , base
+    , bytestring
+    , filepath
+    , hspec
+    , hspec-megaparsec
+    , megaparsec
+    , pretty-simple
+    , tasty
+    , tasty-golden
+    , tasty-hspec
+    , text
+
+  build-depends:  os-release
+  hs-source-dirs: tests
+  main-is:        Main.hs
+  other-modules:
+    Golden.Real
+    Specs.Megaparsec
+
+  type:           exitcode-stdio-1.0
diff --git a/tests/Golden/Real.hs b/tests/Golden/Real.hs
new file mode 100644
--- /dev/null
+++ b/tests/Golden/Real.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE CPP #-}
+
+module Golden.Real where
+
+import System.OsRelease
+
+import System.FilePath
+import Text.Pretty.Simple
+import Test.Tasty
+import Test.Tasty.Golden
+
+import qualified Data.ByteString.Lazy          as B
+import qualified Data.Text.Lazy                as L
+import qualified Data.Text.Encoding            as E
+
+
+goldenTests :: IO TestTree
+goldenTests = do
+  files <- findByExtension [".in"] (takeDirectory (__FILE__) </> "data")
+  return $ testGroup
+    "Parse os-release into OsRelease"
+    (flip fmap files $ \file ->
+      let out = replaceExtension file ".golden"
+      in  goldenVsString (takeBaseName file) out (parse file)
+    )
+ where
+  parse f = do
+    c <- readFile f
+    pure
+      . B.fromStrict
+      . E.encodeUtf8
+      . L.toStrict
+      . pShowNoColor
+      . parseOsRelease'
+      $ c
diff --git a/tests/Golden/data/alpine.golden b/tests/Golden/data/alpine.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/alpine.golden
@@ -0,0 +1,24 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Alpine Linux"
+        , version = Nothing
+        , id = "alpine"
+        , id_like = Nothing
+        , version_codename = Nothing
+        , version_id = Just "3.11.3"
+        , pretty_name = "Alpine Linux v3.11"
+        , ansi_color = Nothing
+        , cpe_name = Nothing
+        , home_url = Just "https://alpinelinux.org/"
+        , documentation_url = Nothing
+        , support_url = Nothing
+        , bug_report_url = Just "https://bugs.alpinelinux.org/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields = []
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/alpine.in b/tests/Golden/data/alpine.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/alpine.in
@@ -0,0 +1,7 @@
+NAME="Alpine Linux"
+ID=alpine
+VERSION_ID=3.11.3
+PRETTY_NAME="Alpine Linux v3.11"
+HOME_URL="https://alpinelinux.org/"
+BUG_REPORT_URL="https://bugs.alpinelinux.org/"
+
diff --git a/tests/Golden/data/arch.golden b/tests/Golden/data/arch.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/arch.golden
@@ -0,0 +1,29 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Arch Linux"
+        , version = Nothing
+        , id = "arch"
+        , id_like = Nothing
+        , version_codename = Nothing
+        , version_id = Nothing
+        , pretty_name = "Arch Linux"
+        , ansi_color = Just "38;2;23;147;209"
+        , cpe_name = Nothing
+        , home_url = Just "https://www.archlinux.org/"
+        , documentation_url = Just "https://wiki.archlinux.org/"
+        , support_url = Just "https://bbs.archlinux.org/"
+        , bug_report_url = Just "https://bugs.archlinux.org/"
+        , privacy_policy_url = Nothing
+        , build_id = Just "rolling"
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Just "archlinux"
+        }
+    , unknown_fields =
+        [
+            ( "LOGO"
+            , "archlinux"
+            )
+        ]
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/arch.in b/tests/Golden/data/arch.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/arch.in
@@ -0,0 +1,11 @@
+NAME="Arch Linux"
+PRETTY_NAME="Arch Linux"
+ID=arch
+BUILD_ID=rolling
+ANSI_COLOR="38;2;23;147;209"
+HOME_URL="https://www.archlinux.org/"
+DOCUMENTATION_URL="https://wiki.archlinux.org/"
+SUPPORT_URL="https://bbs.archlinux.org/"
+BUG_REPORT_URL="https://bugs.archlinux.org/"
+LOGO=archlinux
+
diff --git a/tests/Golden/data/centos.golden b/tests/Golden/data/centos.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/centos.golden
@@ -0,0 +1,45 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "CentOS Linux"
+        , version = Just "8 (Core)"
+        , id = "centos"
+        , id_like = Just "rhel fedora"
+        , version_codename = Nothing
+        , version_id = Just "8"
+        , pretty_name = "CentOS Linux 8 (Core)"
+        , ansi_color = Just "0;31"
+        , cpe_name = Just "cpe:/o:centos:centos:8"
+        , home_url = Just "https://www.centos.org/"
+        , documentation_url = Nothing
+        , support_url = Nothing
+        , bug_report_url = Just "https://bugs.centos.org/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields =
+        [
+            ( "REDHAT_SUPPORT_PRODUCT_VERSION"
+            , "8"
+            )
+        ,
+            ( "CENTOS_MANTISBT_PROJECT"
+            , "CentOS-8"
+            )
+        ,
+            ( "REDHAT_SUPPORT_PRODUCT"
+            , "centos"
+            )
+        ,
+            ( "PLATFORM_ID"
+            , "platform:el8"
+            )
+        ,
+            ( "CENTOS_MANTISBT_PROJECT_VERSION"
+            , "8"
+            )
+        ]
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/centos.in b/tests/Golden/data/centos.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/centos.in
@@ -0,0 +1,17 @@
+NAME="CentOS Linux"
+VERSION="8 (Core)"
+ID="centos"
+ID_LIKE="rhel fedora"
+VERSION_ID="8"
+PLATFORM_ID="platform:el8"
+PRETTY_NAME="CentOS Linux 8 (Core)"
+ANSI_COLOR="0;31"
+CPE_NAME="cpe:/o:centos:centos:8"
+HOME_URL="https://www.centos.org/"
+BUG_REPORT_URL="https://bugs.centos.org/"
+
+CENTOS_MANTISBT_PROJECT="CentOS-8"
+CENTOS_MANTISBT_PROJECT_VERSION="8"
+REDHAT_SUPPORT_PRODUCT="centos"
+REDHAT_SUPPORT_PRODUCT_VERSION="8"
+
diff --git a/tests/Golden/data/debian.golden b/tests/Golden/data/debian.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/debian.golden
@@ -0,0 +1,24 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Debian GNU/Linux"
+        , version = Just "8 (jessie)"
+        , id = "debian"
+        , id_like = Nothing
+        , version_codename = Nothing
+        , version_id = Just "8"
+        , pretty_name = "Debian GNU/Linux 8 (jessie)"
+        , ansi_color = Nothing
+        , cpe_name = Nothing
+        , home_url = Just "http://www.debian.org/"
+        , documentation_url = Nothing
+        , support_url = Just "http://www.debian.org/support/"
+        , bug_report_url = Just "https://bugs.debian.org/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields = []
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/debian.in b/tests/Golden/data/debian.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/debian.in
@@ -0,0 +1,8 @@
+PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
+NAME="Debian GNU/Linux"
+VERSION_ID="8"
+VERSION="8 (jessie)"
+ID=debian
+HOME_URL="http://www.debian.org/"
+SUPPORT_URL="http://www.debian.org/support/"
+BUG_REPORT_URL="https://bugs.debian.org/"
diff --git a/tests/Golden/data/empty.golden b/tests/Golden/data/empty.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/empty.golden
@@ -0,0 +1,24 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Linux"
+        , version = Nothing
+        , id = "linux"
+        , id_like = Nothing
+        , version_codename = Nothing
+        , version_id = Nothing
+        , pretty_name = "Linux"
+        , ansi_color = Nothing
+        , cpe_name = Nothing
+        , home_url = Nothing
+        , documentation_url = Nothing
+        , support_url = Nothing
+        , bug_report_url = Nothing
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields = []
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/empty.in b/tests/Golden/data/empty.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/empty.in
@@ -0,0 +1,1 @@
+
diff --git a/tests/Golden/data/exherbo.golden b/tests/Golden/data/exherbo.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/exherbo.golden
@@ -0,0 +1,24 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Exherbo"
+        , version = Nothing
+        , id = "exherbo"
+        , id_like = Nothing
+        , version_codename = Nothing
+        , version_id = Nothing
+        , pretty_name = "Exherbo Linux"
+        , ansi_color = Just "0;32"
+        , cpe_name = Nothing
+        , home_url = Just "https://www.exherbo.org/"
+        , documentation_url = Nothing
+        , support_url = Just "irc://irc.freenode.net/#exherbo"
+        , bug_report_url = Just "https://bugs.exherbo.org/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields = []
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/exherbo.in b/tests/Golden/data/exherbo.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/exherbo.in
@@ -0,0 +1,9 @@
+NAME="Exherbo"
+PRETTY_NAME="Exherbo Linux"
+ID="exherbo"
+# comment
+ANSI_COLOR="0;32"
+HOME_URL="https://www.exherbo.org/"
+SUPPORT_URL="irc://irc.freenode.net/#exherbo"
+BUG_REPORT_URL="https://bugs.exherbo.org/"
+
diff --git a/tests/Golden/data/fedora.golden b/tests/Golden/data/fedora.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/fedora.golden
@@ -0,0 +1,49 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Fedora"
+        , version = Just "31 (Container Image)"
+        , id = "fedora"
+        , id_like = Nothing
+        , version_codename = Just ""
+        , version_id = Just "31"
+        , pretty_name = "Fedora 31 (Container Image)"
+        , ansi_color = Just "0;34"
+        , cpe_name = Just "cpe:/o:fedoraproject:fedora:31"
+        , home_url = Just "https://fedoraproject.org/"
+        , documentation_url = Just "https://docs.fedoraproject.org/en-US/fedora/f31/system-administrators-guide/"
+        , support_url = Just "https://fedoraproject.org/wiki/Communicating_and_getting_help"
+        , bug_report_url = Just "https://bugzilla.redhat.com/"
+        , privacy_policy_url = Just "https://fedoraproject.org/wiki/Legal:PrivacyPolicy"
+        , build_id = Nothing
+        , variant = Just "Container Image"
+        , variant_id = Just "container"
+        , logo = Just "fedora-logo-icon"
+        }
+    , unknown_fields =
+        [
+            ( "REDHAT_SUPPORT_PRODUCT_VERSION"
+            , "31"
+            )
+        ,
+            ( "REDHAT_BUGZILLA_PRODUCT"
+            , "Fedora"
+            )
+        ,
+            ( "REDHAT_SUPPORT_PRODUCT"
+            , "Fedora"
+            )
+        ,
+            ( "PLATFORM_ID"
+            , "platform:f31"
+            )
+        ,
+            ( "REDHAT_BUGZILLA_PRODUCT_VERSION"
+            , "31"
+            )
+        ,
+            ( "LOGO"
+            , "fedora-logo-icon"
+            )
+        ]
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/fedora.in b/tests/Golden/data/fedora.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/fedora.in
@@ -0,0 +1,21 @@
+NAME=Fedora
+VERSION="31 (Container Image)"
+ID=fedora
+VERSION_ID=31
+VERSION_CODENAME=""
+PLATFORM_ID="platform:f31"
+PRETTY_NAME="Fedora 31 (Container Image)"
+ANSI_COLOR="0;34"
+LOGO=fedora-logo-icon
+CPE_NAME="cpe:/o:fedoraproject:fedora:31"
+HOME_URL="https://fedoraproject.org/"
+DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f31/system-administrators-guide/"
+SUPPORT_URL="https://fedoraproject.org/wiki/Communicating_and_getting_help"
+BUG_REPORT_URL="https://bugzilla.redhat.com/"
+REDHAT_BUGZILLA_PRODUCT="Fedora"
+REDHAT_BUGZILLA_PRODUCT_VERSION=31
+REDHAT_SUPPORT_PRODUCT="Fedora"
+REDHAT_SUPPORT_PRODUCT_VERSION=31
+PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy"
+VARIANT="Container Image"
+VARIANT_ID=container
diff --git a/tests/Golden/data/gentoo.golden b/tests/Golden/data/gentoo.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/gentoo.golden
@@ -0,0 +1,24 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Gentoo"
+        , version = Nothing
+        , id = "gentoo"
+        , id_like = Nothing
+        , version_codename = Nothing
+        , version_id = Nothing
+        , pretty_name = "Gentoo/Linux"
+        , ansi_color = Just "1;32"
+        , cpe_name = Nothing
+        , home_url = Just "https://www.gentoo.org/"
+        , documentation_url = Nothing
+        , support_url = Just "https://www.gentoo.org/support/"
+        , bug_report_url = Just "https://bugs.gentoo.org/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields = []
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/gentoo.in b/tests/Golden/data/gentoo.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/gentoo.in
@@ -0,0 +1,8 @@
+NAME=Gentoo
+ID=gentoo
+PRETTY_NAME="Gentoo/Linux"
+ANSI_COLOR="1;32"
+HOME_URL="https://www.gentoo.org/"
+SUPPORT_URL="https://www.gentoo.org/support/"
+BUG_REPORT_URL="https://bugs.gentoo.org/"
+
diff --git a/tests/Golden/data/mint.golden b/tests/Golden/data/mint.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/mint.golden
@@ -0,0 +1,29 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Linux Mint"
+        , version = Just "18.2 (Sonya)"
+        , id = "linuxmint"
+        , id_like = Just "ubuntu"
+        , version_codename = Just "sonya"
+        , version_id = Just "18.2"
+        , pretty_name = "Linux Mint 18.2"
+        , ansi_color = Nothing
+        , cpe_name = Nothing
+        , home_url = Just "http://www.linuxmint.com/"
+        , documentation_url = Nothing
+        , support_url = Just "http://forums.linuxmint.com/"
+        , bug_report_url = Just "http://bugs.launchpad.net/linuxmint/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields =
+        [
+            ( "UBUNTU_CODENAME"
+            , "xenial"
+            )
+        ]
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/mint.in b/tests/Golden/data/mint.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/mint.in
@@ -0,0 +1,12 @@
+NAME="Linux Mint"
+VERSION="18.2 (Sonya)"
+ID=linuxmint
+ID_LIKE=ubuntu
+PRETTY_NAME="Linux Mint 18.2"
+VERSION_ID="18.2"
+HOME_URL="http://www.linuxmint.com/"
+SUPPORT_URL="http://forums.linuxmint.com/"
+BUG_REPORT_URL="http://bugs.launchpad.net/linuxmint/"
+VERSION_CODENAME=sonya
+UBUNTU_CODENAME=xenial
+
diff --git a/tests/Golden/data/red-hat.golden b/tests/Golden/data/red-hat.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/red-hat.golden
@@ -0,0 +1,41 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Red Hat Enterprise Linux Server"
+        , version = Just "7.6 (Maipo)"
+        , id = "rhel"
+        , id_like = Just "fedora"
+        , version_codename = Nothing
+        , version_id = Just "7.6"
+        , pretty_name = "Red Hat Enterprise Linux"
+        , ansi_color = Just "0;31"
+        , cpe_name = Just "cpe:/o:redhat:enterprise_linux:7.6:GA:server"
+        , home_url = Just "https://www.redhat.com/"
+        , documentation_url = Nothing
+        , support_url = Nothing
+        , bug_report_url = Just "https://bugzilla.redhat.com/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Just "Server"
+        , variant_id = Just "server"
+        , logo = Nothing
+        }
+    , unknown_fields =
+        [
+            ( "REDHAT_SUPPORT_PRODUCT_VERSION"
+            , "7.6"
+            )
+        ,
+            ( "REDHAT_BUGZILLA_PRODUCT"
+            , "Red Hat Enterprise Linux 7"
+            )
+        ,
+            ( "REDHAT_SUPPORT_PRODUCT"
+            , "Red Hat Enterprise Linux"
+            )
+        ,
+            ( "REDHAT_BUGZILLA_PRODUCT_VERSION"
+            , "7.6"
+            )
+        ]
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/red-hat.in b/tests/Golden/data/red-hat.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/red-hat.in
@@ -0,0 +1,17 @@
+NAME="Red Hat Enterprise Linux Server"
+VERSION="7.6 (Maipo)"
+ID="rhel"
+ID_LIKE="fedora"
+VARIANT="Server"
+VARIANT_ID="server"
+VERSION_ID="7.6"
+PRETTY_NAME="Red Hat Enterprise Linux"
+ANSI_COLOR="0;31"
+CPE_NAME="cpe:/o:redhat:enterprise_linux:7.6:GA:server"
+HOME_URL="https://www.redhat.com/"
+BUG_REPORT_URL="https://bugzilla.redhat.com/"
+ 
+REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 7"
+REDHAT_BUGZILLA_PRODUCT_VERSION=7.6
+REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
+REDHAT_SUPPORT_PRODUCT_VERSION="7.6"
diff --git a/tests/Golden/data/red-hat_parseErrors.golden b/tests/Golden/data/red-hat_parseErrors.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/red-hat_parseErrors.golden
@@ -0,0 +1,49 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Red Hat Enterprise Linux Server"
+        , version = Just "7.6 (Maipo)"
+        , id = "rhel"
+        , id_like = Just "fedora"
+        , version_codename = Nothing
+        , version_id = Just "7.6"
+        , pretty_name = "Red Hat Enterprise Linux"
+        , ansi_color = Just "0;31"
+        , cpe_name = Just "cpe:/o:redhat:enterprise_linux:7.6:GA:server"
+        , home_url = Just "https://www.redhat.com/"
+        , documentation_url = Nothing
+        , support_url = Nothing
+        , bug_report_url = Just "https://bugzilla.redhat.com/"
+        , privacy_policy_url = Nothing
+        , build_id = Nothing
+        , variant = Just "Server"
+        , variant_id = Just "server"
+        , logo = Nothing
+        }
+    , unknown_fields =
+        [
+            ( "REDHAT_SUPPORT_PRODUCT_VERSION"
+            , "7.6"
+            )
+        ,
+            ( "REDHAT_BUGZILLA_PRODUCT"
+            , "Red Hat Enterprise Linux 7"
+            )
+        ,
+            ( "REDHAT_SUPPORT_PRODUCT"
+            , "Red Hat Enterprise Linux"
+            )
+        ,
+            ( "REDHAT_BUGZILLA_PRODUCT_VERSION"
+            , "7.6"
+            )
+        ]
+    , parse_errors =
+        [ TrivialError 520
+            ( Just
+                ( Tokens ( '\'' :| "" ) )
+            )
+            ( fromList
+                [ Label ( 'e' :| "nd of line" ) ]
+            )
+        ]
+    }
diff --git a/tests/Golden/data/red-hat_parseErrors.in b/tests/Golden/data/red-hat_parseErrors.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/red-hat_parseErrors.in
@@ -0,0 +1,18 @@
+NAME="Red Hat Enterprise Linux Server"
+VERSION="7.6 (Maipo)"
+ID="rhel"
+ID_LIKE="fedora"
+VARIANT="Server"
+VARIANT_ID="server"
+VERSION_ID="7.6"
+PRETTY_NAME="Red Hat Enterprise Linux"
+ANSI_COLOR="0;31"
+CPE_NAME="cpe:/o:redhat:enterprise_linux:7.6:GA:server"
+HOME_URL="https://www.redhat.com/"
+BUG_REPORT_URL="https://bugzilla.redhat.com/"
+ 
+REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 7"
+REDHAT_BUGZILLA_PRODUCT_VERSION=7.6
+REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
+REDHAT_SUPPORT_PRODUCT_VERSION="7.6"
+foo=1.'
diff --git a/tests/Golden/data/ubuntu.golden b/tests/Golden/data/ubuntu.golden
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/ubuntu.golden
@@ -0,0 +1,29 @@
+OsReleaseResult
+    { osRelease = OsRelease
+        { name = "Ubuntu"
+        , version = Just "20.04 LTS (Focal Fossa)"
+        , id = "ubuntu"
+        , id_like = Just "debian"
+        , version_codename = Just "focal"
+        , version_id = Just "20.04"
+        , pretty_name = "Ubuntu 20.04 LTS"
+        , ansi_color = Nothing
+        , cpe_name = Nothing
+        , home_url = Just "https://www.ubuntu.com/"
+        , documentation_url = Nothing
+        , support_url = Just "https://help.ubuntu.com/"
+        , bug_report_url = Just "https://bugs.launchpad.net/ubuntu/"
+        , privacy_policy_url = Just "https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
+        , build_id = Nothing
+        , variant = Nothing
+        , variant_id = Nothing
+        , logo = Nothing
+        }
+    , unknown_fields =
+        [
+            ( "UBUNTU_CODENAME"
+            , "focal"
+            )
+        ]
+    , parse_errors = []
+    }
diff --git a/tests/Golden/data/ubuntu.in b/tests/Golden/data/ubuntu.in
new file mode 100644
--- /dev/null
+++ b/tests/Golden/data/ubuntu.in
@@ -0,0 +1,13 @@
+NAME="Ubuntu"
+VERSION="20.04 LTS (Focal Fossa)"
+ID=ubuntu
+ID_LIKE=debian
+PRETTY_NAME="Ubuntu 20.04 LTS"
+VERSION_ID="20.04"
+HOME_URL="https://www.ubuntu.com/"
+SUPPORT_URL="https://help.ubuntu.com/"
+BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
+PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
+VERSION_CODENAME=focal
+UBUNTU_CODENAME=focal
+
diff --git a/tests/HLint.hs b/tests/HLint.hs
deleted file mode 100644
--- a/tests/HLint.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Main (main) where
-
-import           Language.Haskell.HLint (hlint)
-import           System.Exit            (exitFailure, exitSuccess)
-
-arguments :: [String]
-arguments =
-    [
-      "library"
-    , "--cpp-file=dist/build/autogen/cabal_macros.h"
-    , "tests"
-    ]
-
-main :: IO ()
-main = do
-    hints <- hlint arguments
-    if null hints then exitSuccess else exitFailure
diff --git a/tests/Haddock.hs b/tests/Haddock.hs
deleted file mode 100644
--- a/tests/Haddock.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-module Main (main) where
-
-import           Data.List      (genericLength)
-import           Data.Maybe     (catMaybes)
-import           System.Exit    (exitFailure, exitSuccess)
-import           System.Process (readProcess)
-import           Text.Regex     (matchRegex, mkRegex)
-
-arguments :: [String]
-arguments =
-    [ "haddock"
-    ]
-
-average :: (Fractional a, Real b) => [b] -> a
-average xs = realToFrac (sum xs) / genericLength xs
-
-expected :: Fractional a => a
-expected = 0
-
-main :: IO ()
-main = do
-    output <- readProcess "cabal" arguments ""
-    if average (match output) >= expected
-        then exitSuccess
-        else putStr output >> exitFailure
-
-match :: String -> [Int]
-match = fmap read . concat . catMaybes . fmap (matchRegex pattern) . lines
-  where
-    pattern = mkRegex "^ *([0-9]*)% "
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/Main.hs
@@ -0,0 +1,16 @@
+import Golden.Real
+import Specs.Megaparsec
+
+import Test.Tasty
+import Test.Tasty.Hspec
+
+
+main :: IO ()
+main = do
+  ms <- testSpec "megaparsec spec" megaparsecSpec
+  gs <- goldenTests
+  defaultMain (tests [ms, gs])
+
+tests :: [TestTree] -> TestTree
+tests ts = testGroup "Tests" (ts++[])
+
diff --git a/tests/Specs/Megaparsec.hs b/tests/Specs/Megaparsec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Specs/Megaparsec.hs
@@ -0,0 +1,88 @@
+module Specs.Megaparsec where
+
+import           System.OsRelease
+
+import           Data.Either
+import           Data.Void
+import           Test.Hspec
+import           Test.Hspec.Megaparsec
+
+import qualified Text.Megaparsec               as MP
+
+
+megaparsecSpec :: Spec
+megaparsecSpec = do
+  describe "parseAssignment" $ do
+    it "parses simple value" $ shouldParse' "foo=bar" ("foo", "bar")
+    it "parses single quoted value" $ shouldParse' "foo='bar'" ("foo", "bar")
+    it "parses double quoted value" $ shouldParse' "foo=\"bar\"" ("foo", "bar")
+
+    it "parses _ var" $ shouldParse' "f_x=''" ("f_x", "")
+
+    -- this is not valid per spec, but many files do this
+    it "parses ._- in unquoted assignment"
+      $ shouldParse' "VERSION_ID=bar-1.9_rc2" ("VERSION_ID", "bar-1.9_rc2")
+
+    it "parses quoted space" $ shouldParse' "f='a b'" ("f", "a b")
+    it "parses quoted -" $ shouldParse' "f='a-b'" ("f", "a-b")
+
+    it "parses special \\" $ shouldParse' "foo='ba\\\\r'" ("foo", "ba\\r")
+    it "parses special `" $ shouldParse' "foo='ba\\`r'" ("foo", "ba`r")
+    it "parses special '" $ shouldParse' "foo='ba\\'r'" ("foo", "ba'r")
+    it "parses special $" $ shouldParse' "foo='ba\\$r'" ("foo", "ba$r")
+    it "parses special \"" $ shouldParse' "foo=\"ba\\\"r\"" ("foo", "ba\"r")
+
+    it "parses empty val noquotes" $ shouldParse' "foo=" ("foo", "")
+    it "parses empty val quote \"" $ shouldParse' "foo=\"\"" ("foo", "")
+    it "parses empty val quote '" $ shouldParse' "foo=''" ("foo", "")
+
+    it "breaks on comments" $ shouldFail' "# foo=\"bar'"
+    it "breaks on misquoting 1" $ shouldFail' "foo=\"bar'"
+    it "breaks on misquoting 2" $ shouldFail' "foo='bar\""
+    it "breaks on unquoted $" $ shouldFail' "foo='ba$r'"
+    it "breaks on unquoted `" $ shouldFail' "foo='ba`r'"
+    it "breaks on unquoted \"" $ shouldFail' "foo=\"ba\"r\""
+    it "breaks on unquoted '" $ shouldFail' "foo='ba'r'"
+    it "breaks on unquoted \\" $ shouldFail' "foo='ba\\r'"
+    it "breaks on unquoted val with space" $ shouldFail' "foo=ba r"
+    it "breaks on unquoted val with ;" $ shouldFail' "foo=ba;r"
+    it "breaks on unquoted val with \\" $ shouldFail' "foo=ba\\r"
+    it "breaks on trailing NL" $ shouldFail' "foo=bar\n"
+
+
+  describe "parseAssignments" $ do
+    it "parses simple values" $ shouldParseMany "foo=bar" [("foo", "bar")]
+
+    it "parses multiple values"
+      $ shouldParseMany "foo=bar\nbar=baz" [("foo", "bar"), ("bar", "baz")]
+    it "parses multiple values with comments" $ shouldParseMany
+      "foo=bar\n# comment\nbar=baz"
+      [("foo", "bar"), ("bar", "baz")]
+
+    it "parses gracefully" $ shouldParseMany "foo=bar\nbar=baz\"" [("foo", "bar")]
+
+    it "parses empty files" $ shouldParseMany "" []
+    it "parses empty files with newlines" $ shouldParseMany "\n\n" []
+    it "parses empty files with newlines and comments"
+      $ shouldParseMany "\n\n#\n" []
+
+    it "parses comments with leading spaces" $ shouldParseMany "  #" []
+
+parse :: String -> Either (MP.ParseErrorBundle String Void) (String, String)
+parse = MP.parse (parseAssignment <* MP.eof) ""
+
+shouldParse' :: String -> (String, String) -> Expectation
+shouldParse' s s' = parse s `shouldParse` s'
+
+shouldFail' :: String -> Expectation
+shouldFail' s = parse `shouldFailOn` s
+
+parseMany :: String
+          -> Either (MP.ParseErrorBundle String Void) [(String, String)]
+parseMany = fmap rights . MP.parse parseAssignments ""
+
+shouldParseMany :: String -> [(String, String)] -> Expectation
+shouldParseMany s s' = parseMany s `shouldParse` s'
+
+shouldFailMany :: String -> Expectation
+shouldFailMany s = parseMany `shouldFailOn` s
diff --git a/tests/unit/OsReleaseSpec.hs b/tests/unit/OsReleaseSpec.hs
deleted file mode 100644
--- a/tests/unit/OsReleaseSpec.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-module OsReleaseSpec (spec) where
-
-import           Test.Hspec
-import           System.OsRelease
-import           Data.Map.Lazy
-import           Data.Monoid
-import           OsReleaseSpec.Reals
-import           System.IO.Temp
-import           System.IO
-
-spec :: Spec
-spec = do
-    describe "parseOs" $ do
-        it "parses simple value"                   $ fooBar "foo=bar"
-        it "parses simple value with trailing NL"  $ fooBar "foo=bar\n"
-        it "parses single quoted value"            $ fooBar "foo='bar'"
-        it "parses double quoted value"            $ fooBar "foo=\"bar\""
-
-        it "parses _ var" $ parseCase "f_x=''" [("f_x", "")]
-
-        it "parses quoted space" $ parseCase "f='a b'" [("f", "a b")]
-        it "parses quoted -"     $ parseCase "f='a-b'" [("f", "a-b")]
-
-        it "parses special \\" $ specialFooBar "foo='ba\\\\r'" "\\"
-        it "parses special `"  $ specialFooBar "foo='ba\\`r'" "`"
-        it "parses special '"  $ specialFooBar "foo='ba\\'r'" "'"
-        it "parses special $"  $ specialFooBar "foo='ba\\$r'" "$"
-        it "parses special \"" $ specialFooBar "foo=\"ba\\\"r\"" "\""
-
-        it "parses multiple values" $ parseCase
-            "foo=bar\nqux=quux"
-            [("foo", "bar"), ("qux", "quux")]
-
-        it "parses empty val noquotes" $ parseCase "foo=" [("foo", "")]
-        it "parses empty val quote \"" $ parseCase "foo=\"\"" [("foo", "")]
-        it "parses empty val quote '"  $ parseCase "foo=''" [("foo", "")]
-
-        it "breaks on misquoting 1" $ errCase "foo=\"bar'"
-        it "breaks on misquoting 2" $ errCase "foo='bar\""
-        it "breaks on unquoted $"   $ errCase "foo='ba$r'"
-        it "breaks on unquoted `"   $ errCase "foo='ba`r'"
-        it "breaks on unquoted \""  $ errCase "foo=\"ba\"r\""
-        it "breaks on unquoted '"   $ errCase "foo='ba'r'"
-        it "breaks on unquoted \\"  $ errCase "foo='ba\\r'"
-        it "breaks on unquoted val with space"  $ errCase "foo=ba r"
-        it "breaks on unquoted val with ;"      $ errCase "foo=ba;r"
-        it "breaks on unquoted val with \\"     $ errCase "foo=ba\\r"
-
-        it "parses Gentoo os-release"           $ realCase Gentoo
-        it "parses OpenSUSE Factory os-release" $ realCase OpenSUSEFactory
-        it "parses Arch os-release"             $ realCase Arch
-
-    describe "readOs'" $ do
-        it "reads 1st" $ do
-            withSystemTempFile "1st.txt" $ \f1 hf1 -> do
-                withSystemTempFile "2nd.txt" $ \f2 hf2 -> do
-                    hPutStr hf1 "1"
-                    hPutStr hf2 "2"
-                    mapM_ hClose [hf1, hf2]
-
-                    xs <- readOs' [f1, f2]
-                    case xs of
-                        Right x -> x `shouldBe` "1"
-                        Left e -> expectationFailure $ show e
-
-        it "reads 2nd" $ do
-            withSystemTempFile "a-file.txt" $ \f h -> do
-                hPutStr h "1"
-                hClose h
-
-                xs <- readOs' ["/oh-well-let's-hope-no-one-makes-this-file", f]
-                case xs of
-                    Right x -> x `shouldBe` "1"
-                    Left e -> expectationFailure $ show e
-
-    where
-        fooBar x = parseCase x [("foo", "bar")]
-
-        specialFooBar :: String -> String -> Expectation
-        specialFooBar x y = parseCase x [("foo", OsReleaseValue $ "ba" <> y <> "r")]
-
-        realCase x = parseCase (input x) (output x)
-
-        parseCase x y =
-            case parseOs x of
-                Left  e -> expectationFailure $ show e
-                Right z -> z `shouldBe` (fromList y)
-
-        errCase x =
-            case parseOs x of
-                Left  _ -> ("1"::String) `shouldBe` "1"
-                Right z -> expectationFailure (show z)
diff --git a/tests/unit/Spec.hs b/tests/unit/Spec.hs
deleted file mode 100644
--- a/tests/unit/Spec.hs
+++ /dev/null
@@ -1,1 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
