diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Matthieu Monsch (c) 2019
+
+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 Matthieu Monsch nor 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# Applicative flags
+
+Simple flags parsing, inspired by
+[`optparse-applicative`](http://hackage.haskell.org/package/optparse-applicative):
+
+```haskell
+data Options = Options
+  { rootPath :: Text
+  , logLevel :: Int
+  , context :: Maybe Text
+  } deriving Show
+
+optionsParser :: FlagParser Options
+optionsParser = Options <$> textFlag "root" "path to the root"
+                        <*> (numericFlag decimal "log_level" "" <|> pure 0)
+                        <*> (optional $ textFlag "context" "")
+```
+
+See the [documentation](http://hackage.haskell.org/package/flags-applicative)
+for more information.
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/app/SimpleExample.hs b/app/SimpleExample.hs
new file mode 100644
--- /dev/null
+++ b/app/SimpleExample.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Control.Applicative ((<|>), optional)
+import Data.Text (Text, pack)
+import Data.Text.Read (decimal)
+import Flags.Applicative
+import System.Environment (getArgs)
+
+data Options = Options
+  { rootPath :: Text
+  , logLevel :: Int
+  , context :: Maybe Text
+  } deriving Show
+
+optionsParser :: FlagParser Options
+optionsParser = Options <$> textFlag "root" "path to the root"
+                        <*> (numericFlag decimal "log_level" "" <|> pure 0)
+                        <*> (optional $ textFlag "context" "")
+
+main :: IO ()
+main = do
+  args <- getArgs
+  print $ parseFlags optionsParser args
diff --git a/flags-applicative.cabal b/flags-applicative.cabal
new file mode 100644
--- /dev/null
+++ b/flags-applicative.cabal
@@ -0,0 +1,49 @@
+name:                flags-applicative
+version:             0.0.1.0
+synopsis:            Applicative flag parsing
+description:         https://github.com/mtth/flags-applicative
+homepage:            https://github.com/mtth/flags-applicative
+license:             BSD3
+license-file:        LICENSE
+author:              Matthieu Monsch
+maintainer:          matthieu.monsch@gmail.com
+copyright:           2019 Matthieu Monsch
+category:            Web
+build-type:          Simple
+cabal-version:       >=1.10
+extra-source-files:  README.md
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Flags.Applicative
+  build-depends:       base >= 4.7 && < 5
+                     , containers >= 0.6 && < 0.7
+                     , mtl >= 2.2 && < 2.3
+                     , text >= 1.2 && < 1.3
+  default-language:    Haskell2010
+
+test-suite flags-applicative-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , text >= 1.2 && < 1.3
+    , hspec >=2.6 && <2.7
+    , flags-applicative
+  default-language: Haskell2010
+
+executable simple-example
+  hs-source-dirs:      app
+  main-is:             SimpleExample.hs
+  build-depends:
+      base >=4.7 && <5
+    , text >= 1.2 && < 1.3
+    , flags-applicative
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/mtth/flags-applicative
diff --git a/src/Flags/Applicative.hs b/src/Flags/Applicative.hs
new file mode 100644
--- /dev/null
+++ b/src/Flags/Applicative.hs
@@ -0,0 +1,254 @@
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TupleSections #-}
+
+-- | Simple flags parsing module, inspired by @optparse-applicative@.
+--
+-- Sample usage (note the default log level and optional context):
+--
+-- @
+-- module Main where
+--
+-- import Control.Applicative ((\<|\>), optional)
+-- import Data.Text (Text)
+-- import Data.Text.Read (decimal)
+-- import Flags.Applicative
+-- import System.Environment (getArgs)
+--
+-- data Options = Options
+--   { rootPath :: Text
+--   , logLevel :: Int
+--   , context :: Maybe Text
+--   } deriving Show
+--
+-- optionsParser :: FlagParser Options
+-- optionsParser = Options \<$\> textFlag "root" "path to the root"
+--                         \<*\> (numericFlag decimal "log_level" "" \<|\> pure 0)
+--                         \<*\> (optional $ textFlag "context" "")
+--
+-- main :: IO ()
+-- main = do
+--   args <- getArgs
+--   print $ parseFlags optionsParser args
+-- @
+
+-- TODO: Add @--help@ support.
+
+module Flags.Applicative
+  ( Name, FlagParser, FlagError(..), ParserError(..)
+  , parseFlags
+  -- * Nullary flags
+  , boolFlag
+  -- * Unary flags
+  , unaryFlag, textFlag, numericFlag
+  ) where
+
+import Control.Applicative ((<|>), Alternative, empty, optional)
+import Control.Monad (when)
+import Control.Monad.Except (Except, catchError, runExcept, throwError)
+import Control.Monad.RWS.Strict (RWST, runRWST)
+import Control.Monad.Reader (asks)
+import Control.Monad.State.Strict (get, modify, put)
+import Control.Monad.Writer.Strict (tell)
+import Data.Bifunctor (first, second)
+import Data.Foldable (toList)
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.Map.Strict (Map)
+import Data.Maybe (isJust)
+import Data.Set (Set)
+import Data.Text (Text)
+import System.Environment (getArgs)
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import qualified Data.Text.Read as T
+
+-- | The name of a flag, can use all characters but @=@ (the value delimiter). It's good practice
+-- for flag names to be lowercase ASCII with underscores.
+type Name = Text
+
+data Arity = Nullary | Unary deriving Eq
+data Flag = Flag Arity Text
+
+-- The errors which can happen during flag parsing.
+data ValueError
+  = MissingValue Name
+  | InvalidValue Name Text String
+
+type Action a = RWST
+  (Map Name Text) -- Flag values (or empty for nullary flags).
+  (Set Name) -- Read flags.
+  (Set Name) -- Used flags.
+  (Except ValueError) -- Eventual parsing error.
+  a
+
+-- | Parser definition errors.
+data ParserError
+  = DuplicateFlag Name -- ^ The same flag name was declared multiple times.
+  | Empty -- ^ The parser is empty (this should not happen if you use standard combinators).
+  deriving (Eq, Show)
+
+-- | Flags parser.
+--
+-- There are two types of flags:
+--
+-- * Nullary flags created with 'boolFlag' which are 'True' when set and 'False' otherwise (a.k.a.
+-- switches). For example @--version@ or @--enable_foo@.
+-- * Unary flags created with 'unaryFlag' and its convenience variants (e.g. 'textFlag',
+-- 'numericFlag'). These expect a value to be passed in either after an equal sign (@--foo=value@)
+-- or as the following input value (@--foo value@). If the value starts with @--@, only the first
+-- form is accepted.
+--
+-- You can run a parser using 'parseFlags'.
+data FlagParser a
+  = Actionable (Action a) (Map Name Flag)
+  | Invalid ParserError
+  deriving Functor
+
+-- Returns the combined map of flags if there are no duplicates, otherwise the name of one of the
+-- duplicate flags.
+mergeFlags :: Map Name Flag -> Map Name Flag -> Either Name (Map Name Flag)
+mergeFlags flags1 flags2 = case Map.minViewWithKey $ flags1 `Map.intersection` flags2 of
+  Just ((name, _), _) -> Left name
+  Nothing -> Right $ flags1 `Map.union` flags2
+
+instance Applicative FlagParser where
+  pure res = Actionable (pure res) Map.empty
+
+  Invalid err <*> _ = Invalid err
+  _ <*> Invalid err = Invalid err
+  Actionable action1 flags1 <*> Actionable action2 flags2 =
+    case mergeFlags flags1 flags2 of
+      Left name -> Invalid $ DuplicateFlag name
+      Right flags -> Actionable (action1 <*> action2) flags
+
+instance Alternative FlagParser where
+  empty = Invalid Empty
+
+  Invalid Empty <|> parser = parser
+  parser <|> Invalid Empty = parser
+  Invalid err <|> _ = Invalid err
+  _ <|> Invalid err = Invalid err
+  Actionable action1 flags1 <|> Actionable action2 flags2 = case mergeFlags flags1 flags2 of
+    Left name -> Invalid $ DuplicateFlag name
+    Right flags -> Actionable action flags where
+      wrap action = catchError (Just <$> action) $ \case
+        (MissingValue _) -> pure Nothing
+        err -> throwError err
+      action = do
+        used <- get
+        wrap action1 >>= \case
+          Nothing -> put used >> action2
+          Just res -> do
+            used' <- get
+            _ <- wrap action2
+            put used'
+            pure res
+
+-- | The possible parsing errors.
+data FlagError
+  -- | At least one unary flag was specified multiple times with different values.
+  = InconsistentFlagValues Name
+  -- | A unary flag's value failed to parse.
+  | InvalidFlagValue Name Text String
+  -- | The parser is invalid. Unlike other 'FlagError' constructors, this indicates an issue with
+  -- the parser's declaration (rather than the input tokens).
+  | InvalidParser ParserError
+  -- | A required flag was missing.
+  | MissingFlag Name
+  -- | A unary flag was missing a value. This can happen either if a value-less unary flag was the
+  -- last token or was followed by a value which is also a flag name (in which case you should use
+  -- the single-token form: @--flag=--value@).
+  | MissingFlagValue Name
+  -- | At least one flag was set but unused. This can happen when optional flags are set but their
+  -- branch is not selected.
+  | UnexpectedFlags (NonEmpty Name)
+  -- | An unknown flag was set.
+  | UnknownFlag Name
+  deriving (Eq, Show)
+
+-- Tries to gather all raw flag values into a map.
+gatherValues :: Map Name Flag -> [Text] -> Either FlagError ((Map Name Text), [Text])
+gatherValues flags = go where
+  go [] = Right (Map.empty, [])
+  go (token:tokens) = case T.stripPrefix "--" token of
+    Nothing -> second (token:) <$> go tokens
+    Just "" -> Right (Map.empty, tokens)
+    Just suffix ->
+      let
+        (name, pval) = T.breakOn "=" suffix
+        missing = Left $ MissingFlagValue name
+        insert val tokens' = do
+          (vals', args') <- go tokens'
+          case Map.lookup name vals' of
+            Nothing -> Right (Map.insert name val vals', args')
+            Just val' -> if val == val'
+              then Right (vals', args')
+              else Left $ InconsistentFlagValues name
+      in case Map.lookup name flags of
+        Nothing -> Left (UnknownFlag name)
+        Just (Flag Nullary _) -> insert "" tokens
+        Just (Flag Unary _) -> case T.uncons pval of
+          Nothing -> case tokens of
+            (token':tokens') -> if T.isPrefixOf "--" token'
+              then missing
+              else insert token' tokens'
+            _ -> missing
+          Just (_, val) -> insert val tokens
+
+-- | Runs a parser on a list of tokens, returning the parsed flags alongside other non-flag
+-- arguments (i.e. which don't start with @--@). If the special @--@ token is found, all following
+-- tokens will be considered arguments (even if they look like flags).
+parseFlags :: FlagParser a -> [String] -> Either FlagError (a, [Text])
+parseFlags parser tokens = case parser of
+  Invalid err -> Left $ InvalidParser err
+  Actionable action flags -> case gatherValues flags (fmap T.pack tokens) of
+    Left err -> Left err
+    Right (values, args) -> case runExcept $ runRWST action values Set.empty of
+      Right (res, usedNames, readNames) ->
+        let unused = Set.difference readNames usedNames
+        in case Set.minView unused of
+          Nothing -> Right (res, args)
+          Just (name, names) -> Left $ UnexpectedFlags $ name :| toList names
+      Left (MissingValue name) -> Left $ MissingFlag name
+      Left (InvalidValue name val msg) -> Left $ InvalidFlagValue name val msg
+
+useFlag :: Name -> Action ()
+useFlag name = tell (Set.singleton name) >> modify (Set.insert name)
+
+-- | Returns a nullary parser with the given name and description.
+boolFlag :: Name -> Text -> FlagParser Bool
+boolFlag name desc = Actionable action flags where
+  action = do
+    present <- asks (Map.member name)
+    when present $ useFlag name
+    pure present
+  flags = Map.singleton name (Flag Nullary desc)
+
+-- | Returns a unary parser using the given parsing function, name, and description.
+unaryFlag :: (Text -> Either String a) -> Name -> Text -> FlagParser a
+unaryFlag convert name desc = Actionable action flags where
+  action = do
+    useFlag name
+    asks (Map.lookup name) >>= \case
+      Nothing -> throwError $ MissingValue name
+      Just val -> case convert val of
+        Left err -> throwError $ InvalidValue name val err
+        Right res -> pure res
+  flags = Map.singleton name (Flag Unary desc)
+
+-- | Returns a flag with 'Text' values.
+textFlag :: Name -> Text -> FlagParser Text
+textFlag = unaryFlag Right
+
+-- | Returns a flag which can parse numbers using the helper methods in "Data.Text.Read" (e.g.
+-- 'Data.Text.Read.decimal').
+numericFlag :: T.Reader a -> Name -> Text -> FlagParser a
+numericFlag reader = unaryFlag go where
+  go val = case reader val of
+    Right (res, "") -> Right res
+    Left msg -> Left msg
+    _ -> Left "trailing value data"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Applicative ((<|>))
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.Text.Read (decimal)
+import Flags.Applicative
+import Test.Hspec
+import Test.Hspec.QuickCheck
+
+main :: IO ()
+main = hspec $ do
+  describe "parse" $ do
+    it "should parse a single flag" $ do
+      let
+        parser = textFlag "foo" ""
+        res = parseFlags parser ["--foo=abc", "hi"]
+      res `shouldBe` Right ("abc", ["hi"])
+    it "should fail on duplicate flag" $ do
+      let
+        parser = (,) <$> textFlag "foo" "" <*> textFlag "foo" ""
+        res = parseFlags parser []
+      res `shouldBe` Left (InvalidParser $ DuplicateFlag "foo")
+    it "should fail on unknown flags" $ do
+      let
+        parser = textFlag "foo" ""
+        res = parseFlags parser ["hi", "--bar"]
+      res `shouldBe` Left (UnknownFlag "bar")
+    it "should detect unexpected flags" $ do
+      let
+        parser = boolFlag "bar" "" <|> boolFlag "foo" ""
+        res = parseFlags parser ["--bar", "--foo"]
+      res `shouldBe` Left (UnexpectedFlags ("foo" :| []))
+    it "should branch correctly with unary flags" $ do
+      let
+        parser = (Right <$> textFlag "ok" "") <|> (Left <$> textFlag "fail" "")
+        res = parseFlags parser ["--ok", "yes", "no"]
+      res `shouldBe` Right (Right "yes", ["no"])
+    it "should branch correctly with nullary flags" $ do
+      let
+        parser = (True <$ boolFlag "true" "") <|> (False <$ boolFlag "false" "")
+        res = parseFlags parser ["--true", "b", "a"]
+      res `shouldBe` Right (True, ["b", "a"])
+    it "should fail on inconsistent flag values" $ do
+      let
+        parser = textFlag "foo" ""
+        res = parseFlags parser ["--foo=1", "--foo=2"]
+      res `shouldBe` Left (InconsistentFlagValues "foo")
+    it "should support the same flag value multiple times" $ do
+      let
+        parser = numericFlag decimal "foo" "" :: FlagParser Int
+        res = parseFlags parser ["--foo=1", "--foo=1"]
+      res `shouldBe` Right (1, [])
