diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright Li-yao Xia (c) 2018
+
+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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+# Haskell scanf [![Build Status](https://travis-ci.org/Lysxia/scanf.svg?branch=master)](https://travis-ci.org/Lysxia/scanf)
+
+A lightweight library for one-off parsing and printing.
+
+```haskell
+scanf [fmt|%d lazy %s and %d strict %s|]
+      "3 lazy functions and 2 strict fields"
+  = Just (3 :+ "functions" :+ 2 :+ "fields" :+ ())
+```
+
+Quasiquotes are optional. Format strings can also be defined with pure
+Haskell combinators.
+
+```haskell
+printf (fmt_ (int . " lazy " % string . " and " % int . " strict " % string)
+       (8 :+ "dogs" :+ 9 :+ "cats" :+ ())
+  = "8 lazy dogs and 9 strict cats"
+```
+
+With quasiquotes, the following conversion strings are supported:
+
+- `%d`: signed integer (`Int`)
+- `%l`: signed integer (`Integer`, unbounded)
+- `%f`: floating point (`Double`)
+- `%s`: string of non-space characters (`String`)
+- `%c`: single character (`Char`)
+- `%%`: parse/print a literal percent character
+
+N.B.: in `scanf`, spaces in the format string match any number of whitespace
+character until the next nonspace character.
+
+---
+
+Though it also offers a `printf` function, *scanf* is thus named as
+there are only few other preexisting Haskell implementations,
+whereas many Haskell *printf* libraries can already be found on Hackage.
+
+Similar to:
+
+- [xformat](https://hackage.haskell.org/package/xformat) (scanf and printf)
+- [fmt](https://hackage.haskell.org/package/fmt) (printf)
+- [formatting](https://hackage.haskell.org/package/formatting) (printf)
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/scanf.cabal b/scanf.cabal
new file mode 100644
--- /dev/null
+++ b/scanf.cabal
@@ -0,0 +1,46 @@
+name:                scanf
+version:             0.1.0.0
+synopsis:            Easy and type-safe format strings for parsing and printing
+description:
+
+  A lightweight library for one-off parsing and printing.
+  .
+  See README.
+
+homepage:            https://github.com/Lysxia/scanf#readme
+license:             MIT
+license-file:        LICENSE
+author:              Li-yao Xia
+maintainer:          lysxia@gmail.com
+copyright:           2018 Li-yao Xia
+category:            Text
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+tested-with:         GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.1
+
+library
+  hs-source-dirs:      src
+  exposed-modules:
+    Text.Scanf
+    Text.Scanf.Internal
+    Text.Scanf.TH
+  build-depends:
+    template-haskell,
+    base >= 4.7 && < 5
+  ghc-options:         -Wall
+  default-language:    Haskell2010
+
+test-suite example-test
+  hs-source-dirs: test
+  main-is: example.hs
+  build-depends:
+    scanf,
+    base
+  ghc-options: -Wall
+  default-language: Haskell2010
+  type: exitcode-stdio-1.0
+
+source-repository head
+  type:     git
+  location: https://github.com/Lysxia/scanf
diff --git a/src/Text/Scanf.hs b/src/Text/Scanf.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Scanf.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE TypeOperators #-}
+
+-- | Dead simple and type-safe @'scanf'@/@'printf'@.
+
+module Text.Scanf
+  ( -- * Main definitions
+    scanf
+  , printf
+  , (:+)(..)
+  , fmt
+  , fmt_
+  , Format
+
+    -- * Format constructors
+  , (%)
+  , integer
+  , int
+  , double
+  , string
+  , char
+  ) where
+
+import Text.Scanf.Internal
+import Text.Scanf.TH
diff --git a/src/Text/Scanf/Internal.hs b/src/Text/Scanf/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Scanf/Internal.hs
@@ -0,0 +1,147 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TypeOperators #-}
+
+module Text.Scanf.Internal where
+
+import Data.Char (isSpace)
+import Data.List (stripPrefix)
+
+-- | A pretty pair type to build lists with values of different types. 
+-- Remember to close lists with @()@.
+--
+-- @
+-- 3 ':+' "14" ':+' () :: 'Int' ':+' 'String' ':+' ()
+-- @
+data a :+ b = a :+ b
+  deriving (Eq, Ord, Show)
+
+infixr 1 :+
+
+-- | Typed @'scanf'@/@'printf'@ format strings.
+-- They can be built using the 'Text.Scanf.fmt' quasiquote or
+-- with the 'fmt_' function and format combinators.
+data Format t where
+  Empty :: Format ()
+  Constant :: String -> Format t -> Format t
+  Whitespace :: String -> Format t -> Format t
+  Readable :: (Read a, Show a) => Format t -> Format (a :+ t)
+  String :: Format t -> Format (String :+ t)  -- whitespace delimited
+  Char :: Format t -> Format (Char :+ t)
+
+deriving instance Show (Format t)
+
+emptyFmt :: Format ()
+emptyFmt = Empty
+
+-- | Construct a format string. This is an alternative to 'Text.Scanf.fmt'
+-- that doesn't rely on Template Haskell.
+--
+-- The components of a format string are composed using @('.')@ (function
+-- composition) and @('%')@ (wrapper for constant strings).
+--
+-- @
+-- 'fmt_' ('int' '.' \" lazy \" '%' 'string' '.' \" and \" '%' 'int' '.' \" strict \" '%' 'string')
+--   :: 'Format' ('Int' ':+' 'String' ':+' 'Int' ':+' 'String' ':+' ())
+-- @
+fmt_ :: (Format () -> Format t) -> Format t
+fmt_ f = f emptyFmt
+
+-- | Append a constant string to a format string component.
+--
+-- N.B.: in 'scanf', spaces in the format string match any number of whitespace
+-- character until the next nonspace character.
+(%) :: String -> (Format t -> Format q) -> Format t -> Format q
+(%) s f = constant s . f
+
+infixr 9 %
+
+-- | Append a constant string to a format string.
+constant :: String -> Format t -> Format t
+constant "" f = f
+constant s@(c : _) f | isSpace c =
+  let (s0, s') = span isSpace s
+  in whitespace s0 (constant s' f)
+constant s f =
+  let (s0, s') = break isSpace s
+  in constant' s0 (constant s' f)
+
+-- | Append a constant string with no whitespace to a format string.
+constant' :: String -> Format t -> Format t
+constant' s (Constant s' f) = Constant (s ++ s') f
+constant' "" f = f
+constant' s f = Constant s f
+
+-- | Append a constant whitespace string to a format string.
+whitespace :: String -> Format t -> Format t
+whitespace s (Whitespace s' f) = Whitespace (s ++ s') f
+whitespace s f = Whitespace s f
+
+readable :: (Read a, Show a) => Format t -> Format (a :+ t)
+readable = Readable
+
+-- | Format an 'Integer'.
+integer :: Format t -> Format (Integer :+ t)
+integer = Readable
+
+-- | Format an 'Int'.
+int :: Format t -> Format (Int :+ t)
+int = Readable
+
+-- | Format a 'Double'.
+double :: Format t -> Format (Double :+ t)
+double = Readable
+
+-- | Format a 'String'.
+string :: Format t -> Format (String :+ t)
+string = String
+
+-- | Format a 'Char'.
+char :: Format t -> Format (Char :+ t)
+char = Char
+
+readmap :: (a -> b) -> ReadS a -> ReadS b
+readmap f = (fmap . fmap) (\(r, s) -> (f r, s))
+
+readsFormat :: Format t -> ReadS t
+readsFormat (Constant z f) s = do
+  Just s' <- pure (stripPrefix z s)
+  readsFormat f s'
+readsFormat (Readable f) s = do
+  (a, s') <- readsPrec 0 s
+  readmap (a :+) (readsFormat f) s'
+readsFormat (String f) s = do
+  let (s0, s') = break isSpace s
+  readmap (s0 :+) (readsFormat f) s'
+readsFormat (Char f) s = do
+  c : s' <- pure s
+  readmap (c :+) (readsFormat f) s'
+readsFormat (Whitespace _ f) s = do
+  let s' = dropWhile isSpace s
+  readsFormat f s'
+readsFormat Empty s = pure ((), s)
+
+-- | Parse a string according to a format string.
+--
+-- @
+-- 'scanf' ['Text.Scanf.fmt'|Hello %s|] \"Hello world!\" :: 'Maybe' ('String' ':+' ())
+--   = (\"world!\" ':+' ())
+-- @
+scanf :: Format t -> String -> Maybe t
+scanf f s = do
+  (r, _) : _ <- pure (readsFormat f s)
+  pure r
+
+-- | Print a string according to a format string.
+--
+-- @
+-- 'printf' ['Text.Scanf.fmt'|Hello %s|] (\"everyone!\" ':+' ())
+--   = \"Hello everyone!\" 
+-- @
+printf :: Format t -> t -> String
+printf (Constant z f) t = z ++ printf f t
+printf (Readable f) (a :+ t) = show a ++ printf f t
+printf (String f) (s :+ t) = s ++ printf f t
+printf (Char f) (c :+ t) = c : printf f t
+printf (Whitespace s f) t = s ++ printf f t
+printf Empty _ = ""
diff --git a/src/Text/Scanf/TH.hs b/src/Text/Scanf/TH.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Scanf/TH.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Text.Scanf.TH where
+
+import Data.Char (isSpace)
+
+import Language.Haskell.TH
+import Language.Haskell.TH.Syntax
+import Language.Haskell.TH.Quote
+
+import Text.Scanf.Internal
+
+formatString :: String -> Q Exp
+formatString "" = [|Empty|]
+formatString ('%' : s1) =
+  case s1 of
+    'c' : s' -> [|char $(formatString s')|]
+    'd' : s' -> [|int $(formatString s')|]
+    'f' : s' -> [|double $(formatString s')|]
+    'l' : s' -> [|integer $(formatString s')|]
+    's' : s' -> [|string $(formatString s')|]
+    '%' : s' -> [|constant "%" $(formatString s')|]
+    _ -> error "Invalid format string"
+formatString s@(c : _) | isSpace c =
+  let (s0, s') = span isSpace s
+  in [|whitespace $(lift s0) $(formatString s')|]
+formatString s =
+  let (s0, s') = break (\c -> isSpace c || c == '%') s
+  in [|constant $(lift s0) $(formatString s')|]
+
+-- | Parse a typed 'Format' string.
+--
+-- The following conversion strings are supported:
+--
+-- - @%d@: signed integer ('Int')
+-- - @%l@: signed integer ('Integer', unbounded)
+-- - @%f@: floating point ('Double')
+-- - @%s@: string of non-space characters ('String')
+-- - @%c@: single character ('Char')
+-- - @%%@: parse/print a literal percent character
+--
+-- N.B.: in 'scanf', spaces in the format string match any number of whitespace
+-- character until the next nonspace character.
+--
+-- @
+-- ['fmt'|%d lazy %s and %d strict %s|]
+--   :: 'Format' ('Int' ':+' 'String' ':+' 'Int' ':+' 'String' ':+' ())
+-- @
+fmt :: QuasiQuoter
+fmt = QuasiQuoter
+  { quoteExp = formatString
+  , quotePat = undefined
+  , quoteType = undefined
+  , quoteDec = undefined
+  }
diff --git a/test/example.hs b/test/example.hs
new file mode 100644
--- /dev/null
+++ b/test/example.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+import Text.Scanf
+
+main :: IO ()
+main = do
+  scanf [fmt|%c|] "34"
+    =? Just ('3' :+ ())
+
+  scanf [fmt|%d|] "10"
+    =? Just (10 :+ ())
+
+  scanf [fmt|%f|] "4.3"
+    =? Just (4.3 :+ ())
+
+  scanf [fmt| %l|]
+        "    1000000000000000000000000000000000"
+    =? Just (1000000000000000000000000000000000 :+ ())
+
+  scanf [fmt|%s %s|] " a b"
+    =? Just ("" :+ "a" :+ ())
+
+  scanf [fmt|%%|] "%"
+    =? Just ()
+
+  scanf [fmt|a a|] "a  a b"
+    =? Just ()
+
+  let s0 = "10-11-12   Fri  4.3C"
+      f0 = [fmt|%d-%d-%l %s %f %c|]
+      f0' = fmt_ (int . "-" % int . "-" % integer . " " % string . " " % double . " " % char)
+      r0 = Just
+        ((10 :: Int) :+ (11 :: Int) :+ (12 :: Integer)
+          :+ "Fri" :+ (4.3 :: Double) :+ 'C' :+ ())
+  scanf f0 s0 =? r0
+  scanf f0' s0 =? r0
+
+  let s1 = "3 lazy functions and 4 strict fields"
+      f1 = [fmt|%d lazy %s and %d strict %s|]
+      f1' = fmt_ (int . " lazy " % string . " and " % int . " strict " % string)
+      r1 = Just ((3 :: Int) :+ "functions" :+ (4 :: Int) :+ "fields" :+ ())
+      p1 = (2 :: Int) :+ "dogs" :+ (3 :: Int) :+ "cats" :+ ()
+      q1 = "2 lazy dogs and 3 strict cats"
+  scanf f1  s1 =? r1
+  scanf f1' s1 =? r1
+
+  printf f1  p1 =? q1
+  printf f1' p1 =? q1
+
+assertEq, (=?) :: (Eq a, Show a) => a -> a -> IO ()
+assertEq a b = do
+  if a == b then
+    return ()
+  else
+    fail $ show a ++ " /= " ++ show b
+
+(=?) = assertEq
+
