diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Justin Leitgeb
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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/keyword-args.cabal b/keyword-args.cabal
new file mode 100644
--- /dev/null
+++ b/keyword-args.cabal
@@ -0,0 +1,57 @@
+name:                keyword-args
+version:             0.1.0.0
+synopsis:            Extract data from a keyword-args config file format
+
+description: Extracts data from a configuration file with keywords
+             separated fram arguments by one or more spaces. Removes
+             comments and unnecessary whitespace.
+
+license:             MIT
+license-file:        LICENSE
+author:              Justin Leitgeb
+maintainer:          justin@stackbuilders.com
+copyright:           2015 Stack Builders Inc.
+category:            System
+build-type:          Simple
+cabal-version:       >=1.10
+
+
+executable keyword-args
+  main-is:             Main.hs
+  other-modules:       Data.KeywordArgs.Parse
+
+  build-depends:         base >=4.5 && <4.8
+                       , parsec >= 3.1.0 && <= 3.2
+                       , containers
+                       , cassava
+                       , bytestring
+
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+
+library
+  exposed-modules:    Data.KeywordArgs.Parse
+  build-depends:         base >=4.5 && <4.8
+                       , parsec >= 3.1.0 && <= 3.2
+                       , containers
+
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+
+test-suite keyword-args-test
+  type: exitcode-stdio-1.0
+  hs-source-dirs: spec, src
+  main-is: Spec.hs
+  build-depends:       base >=4.5 && <4.8
+                       , parsec >= 3.1.0 && <= 3.2
+                       , containers
+
+                       , hspec
+
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/stackbuilders/keyword-args
diff --git a/spec/Spec.hs b/spec/Spec.hs
new file mode 100644
--- /dev/null
+++ b/spec/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/src/Data/KeywordArgs/Parse.hs b/src/Data/KeywordArgs/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/KeywordArgs/Parse.hs
@@ -0,0 +1,48 @@
+module Data.KeywordArgs.Parse (configParser) where
+
+import Data.Maybe (catMaybes)
+import Data.Char (isSpace)
+
+import Text.Parsec ((<|>), many, try, manyTill, char, anyChar, many1, satisfy)
+import Text.Parsec.String (Parser)
+
+import Text.ParserCombinators.Parsec.Char (string, space, tab, newline,
+                                           alphaNum)
+
+import Text.Parsec.Combinator (eof)
+
+import Control.Applicative ((<*), (*>), (<$>))
+
+configParser :: Parser [(String, String)]
+configParser = catMaybes <$> many line
+
+
+configurationOption :: Parser (String, String)
+configurationOption = do
+  many space
+
+  option <- many1 (satisfy (not . isSpace))
+
+  many1 space
+
+  let
+    endOfOption   = comment <|> endOfLineOrInput
+    quotedValue   = char '"' *> manyTill anyChar (try (char '"')) <* endOfOption
+    unquotedValue = manyTill anyChar endOfOption
+
+  value <- quotedValue <|> unquotedValue
+
+  return (option, value)
+
+comment :: Parser ()
+comment =
+  -- ^ use `try` to avoid consuming leading spaces if no match
+  try (many space *> char '#')
+  *> manyTill anyChar endOfLineOrInput
+  *> return ()
+
+endOfLineOrInput :: Parser ()
+endOfLineOrInput = newline *> return () <|> eof
+
+line :: Parser (Maybe (String, String))
+line = comment *> return Nothing <|> Just <$> configurationOption
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import System.Console.GetOpt
+
+import System.Environment (getArgs, getProgName)
+import System.Exit (ExitCode(..), exitWith, exitSuccess)
+import System.IO.Error (ioError)
+
+import Paths_keyword_args (version)
+import Data.Maybe (fromMaybe)
+
+import Data.Version (showVersion)
+
+import Data.ByteString.Lazy.Char8 (unpack)
+
+import Data.KeywordArgs.Parse
+
+import Text.Parsec
+import Text.ParserCombinators.Parsec.Error (ParseError)
+
+import Data.Csv (encode)
+
+data Options = Options
+    { optShowVersion :: Bool
+    , optShowHelp    :: Bool
+    } deriving Show
+
+defaultOptions = Options
+    { optShowVersion = False
+    , optShowHelp    = False
+    }
+
+options :: [OptDescr (Options -> Options)]
+options =
+  [ Option ['V'] ["version"] (NoArg (\opts -> opts { optShowVersion = True }))
+    "show version number"
+
+  , Option ['h'] ["help"]    (NoArg (\opts -> opts { optShowHelp = True }))
+    "show help"
+  ]
+
+showHelp = do
+  prg <- getProgName
+  putStrLn (usageInfo prg options)
+  exitSuccess
+
+headerMessage :: String -> String
+headerMessage progName = "Usage: " ++ progName ++ " [OPTION...]"
+
+lintOpts :: [String] -> IO (Options, [String])
+lintOpts argv = do
+  name <- getProgName
+
+  case getOpt Permute options argv of
+    (o, n, []  ) -> return (foldl (flip id) defaultOptions o, n)
+    (_, _, errs) ->
+      ioError (userError (concat errs ++ usageInfo (headerMessage name) options))
+
+parseConfig :: String -> Either ParseError [(String, String)]
+parseConfig = parse configParser "(unknown)"
+
+runCheck :: IO ()
+runCheck = do
+
+  f <- getContents
+
+  case parseConfig f of
+    Left e -> do
+      ioError $ userError $ "Parse error: " ++ show e
+
+    Right config -> putStr $ unpack $ encode config
+
+
+main :: IO ()
+main = do
+  opts <- getArgs >>= lintOpts
+
+  name <- getProgName
+
+  if optShowHelp $ fst opts
+  then
+    putStrLn $ usageInfo (headerMessage name) options
+  else
+    if optShowVersion $ fst opts then
+      putStrLn $ name ++ " " ++ showVersion version ++
+      " (C) 2015 Stack Builders Inc."
+    else
+      runCheck
