diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+
+  ©2010 Jason Dusek.
+
+  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.
+
+ .  Names of the contributors to this software may not be used to endorse or
+    promote products derived from this software without specific prior written
+    permission.
+
+  This software is provided by the 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 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/Makefile b/Makefile
new file mode 100644
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,47 @@
+
+OPTS ?= --bash
+RUNS ?= 10000
+GHC = ghc --make -outputdir
+
+
+clean:
+	rm -rf */build */echo bench/bench test/lengths* test/chars*
+
+
+.PHONY: bench
+bench: bench/bench
+	cd bench && ./bench
+
+bench/bench: bench/Bench.hs
+	mkdir -p bench/build
+	${GHC} bench/build -O $< -o $@
+
+
+.PHONY: prof
+prof: prof/echo
+	cd prof && ./echo ${RUNS} +RTS -hb -RTS && hp2ps echo.hp
+
+prof/echo: test/Echo.hs
+	mkdir -p prof/build
+	${GHC} prof/build test/Echo.hs -o prof/echo -prof -auto-all
+
+
+test: test/chars.histogram test/lengths.histogram
+
+test/echo: test/Echo.hs
+	mkdir -p test/build
+	${GHC} test/build -O2 test/Echo.hs -o test/echo
+
+.PHONY: test_run
+test_run: test/echo
+	rm -f test/chars* test/lengths*
+	cd test && ./echo ${OPTS} ${RUNS}
+
+test/chars: test_run
+test/chars.histogram: test/chars
+	sort < $< | uniq -c > $@
+
+test/lengths: test_run
+test/lengths.histogram: test/lengths
+	sort -n < $< | uniq -c > $@
+
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,1 @@
+  Library for generating fully escaped strings for use with the shell.
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/Text/ShellEscape.hs b/Text/ShellEscape.hs
new file mode 100644
--- /dev/null
+++ b/Text/ShellEscape.hs
@@ -0,0 +1,14 @@
+
+{-| Typed shell escaping for Bourne Shell and Bash.
+ -}
+
+module Text.ShellEscape
+  ( Text.ShellEscape.Escape.Escape(..)
+  , Text.ShellEscape.Sh.Sh()
+  , Text.ShellEscape.Bash.Bash()
+  ) where
+
+import Text.ShellEscape.Escape
+import Text.ShellEscape.Sh
+import Text.ShellEscape.Bash
+
diff --git a/Text/ShellEscape/Bash.hs b/Text/ShellEscape/Bash.hs
new file mode 100644
--- /dev/null
+++ b/Text/ShellEscape/Bash.hs
@@ -0,0 +1,75 @@
+
+module Text.ShellEscape.Bash where
+
+import Data.Maybe
+import Data.Char
+import Text.Printf
+import Data.ByteString (ByteString)
+
+import qualified Data.Vector as Vector
+
+import Text.ShellEscape.Escape
+import qualified Text.ShellEscape.Put as Put
+import Text.ShellEscape.EscapeVector
+
+{-| A Bash escaped 'ByteString'. The strings are wrapped in @$\'...\'@ if any
+    bytes within them must be escaped; otherwise, they are left as is.
+    Newlines and other control characters are represented as ANSI escape
+    sequences. High bytes are represented as hex codes. Thus Bash escaped
+    strings will always fit on one line and never contain non-ASCII bytes.
+ -}
+newtype Bash                 =  Bash (EscapeVector EscapingMode)
+ deriving (Eq, Ord, Show)
+
+
+instance Escape Bash where
+  escape                     =  Bash . escWith classify
+  unescape (Bash v)          =  stripEsc v
+  bytes (Bash v) | literal v =  stripEsc v
+                 | otherwise =  interpretEsc v renderANSI' end (begin, Literal)
+   where
+    literal                  =  isNothing . Vector.find ((/= Literal) . snd)
+    begin                    =  [      Put.putString "$'"]
+    end                      =  const (Put.putChar '\'')
+    renderANSI' _ (c, e)     =  (renderANSI c, e)
+
+
+{-| Bash escaping modes.
+ -}
+data EscapingMode            =  ANSIHex | ANSIBackslash | Literal | Quoted
+ deriving (Eq, Ord, Show)
+
+renderANSI c =
+  case classify c of
+    Literal                 ->  Put.putChar     c
+    Quoted                  ->  Put.putChar     c
+    ANSIHex                 ->  Put.putString $ hexify c
+    ANSIBackslash           ->  Put.putString $ backslashify c
+
+backslashify                ::  Char -> String
+backslashify '\ESC'          =  "\\e"
+backslashify c               =  (take 2 . drop 1 . show) c
+
+hexify                      ::  Char -> String
+hexify                       =  printf "\\x%02X" . ord
+
+classify                    ::  Char -> EscapingMode
+classify c | c <= '\ACK'     =  ANSIHex
+           | c <= '\r'       =  ANSIBackslash
+           | c <= '\SUB'     =  ANSIHex
+           | c == '\ESC'     =  ANSIBackslash
+           | c <= '\US'      =  ANSIHex
+           | c <= '&'        =  Quoted
+           | c == '\''       =  ANSIBackslash
+           | c <= '+'        =  Quoted
+           | c <= '9'        =  Literal
+           | c <= '?'        =  Quoted
+           | c <= 'Z'        =  Literal
+           | c == '['        =  Quoted
+           | c == '\\'       =  ANSIBackslash
+           | c <= '`'        =  Quoted
+           | c <= 'z'        =  Literal
+           | c <= '~'        =  Quoted
+           | c == '\DEL'     =  ANSIHex
+           | otherwise       =  ANSIHex
+
diff --git a/Text/ShellEscape/Escape.hs b/Text/ShellEscape/Escape.hs
new file mode 100644
--- /dev/null
+++ b/Text/ShellEscape/Escape.hs
@@ -0,0 +1,21 @@
+
+module Text.ShellEscape.Escape where
+
+import Data.ByteString
+
+{-| A type class for objects that represent an intermediate state of
+    escaping.
+ -}
+class Escape t where
+  -- |    Transform a 'ByteString' into the escaped intermediate form.
+  escape                    ::  ByteString -> t
+  -- |    Recover the original 'ByteString'.
+  unescape                  ::  t -> ByteString
+  -- |    Yield the escaped 'ByteString'.
+  bytes                     ::  t -> ByteString
+
+instance Escape ByteString where
+  escape                     =  id
+  unescape                   =  id
+  bytes                      =  id
+
diff --git a/Text/ShellEscape/EscapeVector.hs b/Text/ShellEscape/EscapeVector.hs
new file mode 100644
--- /dev/null
+++ b/Text/ShellEscape/EscapeVector.hs
@@ -0,0 +1,39 @@
+
+module Text.ShellEscape.EscapeVector where
+
+import Data.ByteString.Char8 (ByteString)
+import qualified Data.ByteString.Char8 as ByteString
+
+import Data.Vector (Vector)
+import qualified Data.Vector as Vector
+import qualified Data.Vector.Mutable as Vector
+
+import qualified Text.ShellEscape.Put as Put
+
+
+type EscapeVector escapingMode = Vector (Char, escapingMode)
+
+escWith :: (Char -> escapingMode) -> ByteString -> EscapeVector escapingMode
+escWith cf b                 =  Vector.create $ do
+  v                         <-  Vector.new (ByteString.length b)
+  sequence_ . snd $ ByteString.foldl' (f v) (0, []) b
+  return v
+ where
+  f v (i, ops) c             =  (i + 1, Vector.write v i (c, cf c) : ops)
+
+
+stripEsc                    ::  Vector (Char, escapingMode) -> ByteString
+stripEsc v                   =  ByteString.unfoldr f . fst $ Vector.unzip v
+ where
+  f v | Vector.null v        =  Nothing
+      | otherwise            =  Just (Vector.unsafeHead v, Vector.unsafeTail v)
+
+interpretEsc v f finish init =  (eval . (finish lastMode :)) instructions
+ where
+  eval                       =  Put.runPut' . sequence_ . reverse
+  (instructions, lastMode)   =  Vector.foldl' f' init v
+   where
+    f' (list, mode) (c, e)   =  (put:list, mode')
+     where
+      (put, mode')           =  f mode (c, e)
+
diff --git a/Text/ShellEscape/Put.hs b/Text/ShellEscape/Put.hs
new file mode 100644
--- /dev/null
+++ b/Text/ShellEscape/Put.hs
@@ -0,0 +1,24 @@
+
+module Text.ShellEscape.Put
+  ( module Data.Binary.Put
+  , putChar
+  , putString
+  , runPut'
+  ) where
+
+import Prelude hiding (putChar)
+import Data.Binary.Put
+import Data.Char
+import Data.ByteString.Lazy
+import Data.ByteString
+import Data.ByteString.Char8
+
+putChar                     ::  Char -> Put
+putChar                      =  putWord8 . toEnum . fromEnum
+
+putString                   ::  String -> Put
+putString                    =  putByteString . Data.ByteString.Char8.pack
+
+runPut'                     ::  Put -> Data.ByteString.ByteString
+runPut' = Data.ByteString.concat . Data.ByteString.Lazy.toChunks . runPut 
+
diff --git a/Text/ShellEscape/Sh.hs b/Text/ShellEscape/Sh.hs
new file mode 100644
--- /dev/null
+++ b/Text/ShellEscape/Sh.hs
@@ -0,0 +1,64 @@
+
+module Text.ShellEscape.Sh where
+
+import Data.ByteString (ByteString)
+
+import Text.ShellEscape.Escape
+import qualified Text.ShellEscape.Put as Put
+import Text.ShellEscape.EscapeVector
+
+
+{-| A Bourne Shell escaped 'ByteString'. An oddity of Bourne shell escaping is
+    the absence of escape codes for newline and other ASCII control
+    characters. These bytes are simply placed literally in single quotes; the
+    effect is that a Bourne Shell escaped string may cover several lines and
+    contain non-ASCII bytes. Runs of bytes that must be escaped are wrapped in
+    @\'...\'@; bytes that are acceptable as literals in Bourne Shell are left
+    as is.
+ -}
+newtype Sh                   =  Sh (EscapeVector EscapingMode)
+ deriving (Eq, Ord, Show)
+
+instance Escape Sh where
+  escape                     =  Sh . escWith classify
+  unescape (Sh v)            =  stripEsc v
+  bytes (Sh v)               =  interpretEsc v act finish ([], Literal)
+   where
+    finish Quote             =  Put.putChar '\''
+    finish Backslash         =  Put.putChar '\\'
+    finish Literal           =  return ()
+
+
+{-| Accept the present escaping mode and desired escaping mode and yield an
+    action and the resulting mode.
+ -}
+act :: EscapingMode -> (Char, EscapingMode) -> (Put.Put, EscapingMode)
+act Quote (c, Quote)         =  (Put.putChar c                 , Quote)
+act Quote (c, Literal)       =  (Put.putString ['\'', c]       , Literal)
+act Quote (c, Backslash)     =  (Put.putString ['\'', '\\', c] , Literal)
+act Backslash (c, Backslash) =  (Put.putChar c                 , Literal)
+act Backslash (c, Quote)     =  (Put.putString ['\\', '\'', c] , Quote)
+act Backslash (c, Literal)   =  (Put.putString ['\\', c]       , Literal)
+act Literal (c, Literal)     =  (Put.putChar c                 , Literal)
+act Literal (c, Backslash)   =  (Put.putString ['\\', c]       , Literal)
+act Literal (c, Quote)       =  (Put.putString ['\'', c]       , Quote)
+
+classify                    ::  Char -> EscapingMode
+classify c | c <= '&'        =  Quote           --  0x00..0x26
+           | c == '\''       =  Backslash       --  0x27
+           | c <= ','        =  Quote           --  0x28..0x2c
+           | c <= '9'        =  Literal         --  0x2d..0x39
+           | c <= '?'        =  Quote           --  0x3a..0x3f
+           | c <= 'Z'        =  Literal         --  0x40..0x5a
+           | c <= '^'        =  Quote           --  0x5b..0x5e
+           | c == '_'        =  Literal         --  0x5f
+           | c == '`'        =  Quote           --  0x60
+           | c <= 'z'        =  Literal         --  0x61..0x7a
+           | c <= '\DEL'     =  Quote           --  0x7b..0x7f
+           | otherwise       =  Quote           --  0x80..0xff
+
+{-| Bourne Shell escaping modes. 
+ -}
+data EscapingMode            =  Backslash | Literal | Quote
+ deriving (Eq, Ord, Show)
+
diff --git a/bench/Bench.hs b/bench/Bench.hs
new file mode 100644
--- /dev/null
+++ b/bench/Bench.hs
@@ -0,0 +1,26 @@
+
+{-# LANGUAGE OverloadedStrings
+  #-}
+
+import Data.ByteString.Char8 (ByteString)
+
+import Criterion.Main
+
+import Text.ShellEscape
+
+
+strings                     ::  [ByteString]
+strings =
+  [ "echo * $PWD"
+  , ""
+  , "~/Music/M.I.A. & Diplo - Piracy Funds Terrorism Vol. 1 (2004)"
+  , "abcds"
+  , "\x00\n\204\DEL"
+  , "\x00\n\204\DELecho * $PWD" ]
+
+main = (defaultMain . concat . fmap (`fmap` strings)) [benchBash, benchSh]
+ where
+  bench' d f s = bench (d ++ show s) (whnf f s)
+  benchBash = bench' "bash:" (escape :: ByteString -> Bash)
+  benchSh = bench' "sh:" (escape :: ByteString -> Sh)
+
diff --git a/shell-escape.cabal b/shell-escape.cabal
new file mode 100644
--- /dev/null
+++ b/shell-escape.cabal
@@ -0,0 +1,33 @@
+name                          : shell-escape
+version                       : 0.0.0
+category                      : Text
+license                       : BSD3
+license-file                  : LICENSE
+author                        : Jason Dusek
+maintainer                    : oss@solidsnack.be
+homepage                      : http://github.com/solidsnack/shell-escape
+synopsis                      : Shell escaping library.
+description                   :
+  Shell escaping library, offering both Bourne shell and Bash style escaping
+  of ByteStrings.
+
+cabal-version                 : >= 1.6
+build-type                    : Simple
+extra-source-files            : README
+                              , bench/Bench.hs
+                              , test/Echo.hs
+                              , Makefile
+
+library
+  build-depends               : base >= 2 && <= 5
+                              , binary
+                              , containers
+                              , vector >= 0.6.0.2
+                              , bytestring >= 0.9
+  exposed-modules             : Text.ShellEscape
+  other-modules               : Text.ShellEscape.Escape
+                                Text.ShellEscape.EscapeVector
+                                Text.ShellEscape.Sh
+                                Text.ShellEscape.Bash
+                                Text.ShellEscape.Put
+
diff --git a/test/Echo.hs b/test/Echo.hs
new file mode 100644
--- /dev/null
+++ b/test/Echo.hs
@@ -0,0 +1,140 @@
+{-# LANGUAGE OverloadedStrings
+  #-}
+
+import System.Environment
+import System.Process (runInteractiveProcess, waitForProcess, ProcessHandle)
+import System.IO (Handle, stderr, stdout, stdin)
+import Data.ByteString (ByteString, pack, hGetContents, hPutStrLn)
+import qualified Data.ByteString
+import Data.ByteString.Char8 (unpack)
+import qualified Data.ByteString.Char8 as Char8
+import Data.Word
+import Control.Monad
+import qualified Text.Printf
+import Data.IORef
+import Data.List
+
+import Test.QuickCheck
+import Test.QuickCheck.Monadic
+
+import Text.ShellEscape
+
+
+--  It is best to implement the echo test with `printf':
+--
+-- .  Some echo implementations always interprete backslash escapes like \3
+--    and give us no way to turn it off. Dash is like this.
+--
+-- .  GNU echo can not be made to simply ignore options like `--help'.
+--
+--  The `printf' implementations available to me -- Bash and Dash --
+--  consistently ignore backslash escape sequences and any options that follow
+--  the format argument.
+--
+printf                      ::  (Shell t, Escape t) => t -> IO ByteString
+printf escaped               =  do
+  (i, o, e, p)              <-  shell escaped cmd
+  exit                      <-  waitForProcess p
+  hGetContents o
+ where
+  cmd                        =  "printf '%s' " ++ unpack raw
+  raw                        =  bytes escaped
+
+
+prop_echoBash               ::  ByteString -> Property
+prop_echoBash                =  something_prop escapeBash
+
+prop_echoSh                 ::  ByteString -> Property
+prop_echoSh                  =  something_prop escapeSh
+
+something_prop esc b         =  monadicIO $ do
+  assert =<< run (test printf b (esc b))
+
+escapeSh                    ::  ByteString -> Sh
+escapeSh b                   =  escape b
+
+escapeBash                  ::  ByteString -> Bash
+escapeBash b                 =  escape b
+
+
+
+test cmd original escaped    =  do
+  b                         <-  cmd escaped
+  Data.ByteString.appendFile "./lengths" (displayLength original)
+  Data.ByteString.appendFile "./chars" (Char8.unlines $ displayBytes original)
+  when (b /= original) $ do
+    err "Result differs from unescaped result:"
+    err "Escaped form:"
+    err (Char8.unwords . displayBytes $ bytes escaped)
+    err "Output:"
+    err (Char8.unwords . displayBytes $ b)
+    err "Original:"
+    err (Char8.unwords . displayBytes $ original)
+  return (b == original)
+
+
+displayBytes                 =  fmap Char8.pack
+                             .  fmap (\w ->  Text.Printf.printf "0x%02x" w)
+                             .  Data.ByteString.unpack
+
+displayLength                =  Char8.pack
+                             .  (\w ->  Text.Printf.printf "%4d\n" w)
+                             .  Data.ByteString.length
+
+class (Escape t) => Shell t where
+  shell :: t -> String -> IO (Handle, Handle, Handle, ProcessHandle)
+instance Shell Sh where
+  shell _                    =  sh
+instance Shell Bash where
+  shell _                    =  bash
+
+
+sh s = runInteractiveProcess "sh" ["-c", s] Nothing Nothing
+bash s = runInteractiveProcess "bash" ["-c", s] Nothing Nothing
+
+instance Arbitrary ByteString where
+  arbitrary                  =  do
+    bytes                   <-  arbitrary :: Gen [Word8]
+    NonEmpty bytes'         <-  arbitrary :: Gen (NonEmptyList Word8)
+    pack `fmap` elements [bytes', bytes', bytes, bytes', bytes']
+
+instance Arbitrary Word8 where
+  arbitrary                  =  fmap fromIntegral (choose ( 0x01 :: Int
+                                                          , 0xFF :: Int ))
+
+main                         =  do
+  runSh                     <-  newIORef True
+  runBash                   <-  newIORef True
+  testsR                    <-  newIORef 10000
+  let testsRwrite            =  writeIORef testsR
+      noBash                 =  writeIORef runBash False
+      noSh                   =  writeIORef runSh False
+  args                      <-  getArgs
+  case (sort . nub) args of
+    ["--bash", "--sh", d]   ->  testsRwrite (read d)
+    ["--bash", "--sh"   ]   ->  return ()
+    ["--bash",         d]   ->  noSh >> testsRwrite (read d)
+    [          "--sh", d]   ->  noBash >> testsRwrite (read d)
+    [                  d]   ->  testsRwrite (read d)
+    [                   ]   ->  return ()
+    _                       ->  error "Invalid arguments."
+  tests                     <-  readIORef testsR
+  let msg                    =  "Performing " ++ show tests ++ " tests."
+      qcArgs                 =  Args Nothing tests tests 32
+      qc                     =  quickCheckWith qcArgs
+  err "Tests are random ByteStrings, containing any byte but null."
+  runSh ?> do
+    err "Testing Sh escaping."
+    err (Char8.pack msg)
+    qc prop_echoSh
+  runBash ?> do
+    err "Testing Bash escaping."
+    err (Char8.pack msg)
+    qc prop_echoBash
+
+err                          =  hPutStrLn stderr
+
+(?>)                        ::  IORef Bool -> IO () -> IO ()
+ref ?> action                =  readIORef ref >>= (`when` action)
+
+
