diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 dist/
-cabal-dev/
+.cabal-sandbox/
+cabal.sandbox.config
 *.o
 *.hi
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
 language-bash
 -------------
 
+[![Build Status](https://travis-ci.org/knrafto/language-bash.svg)](https://travis-ci.org/knrafto/language-bash)
+
 A library for parsing and pretty-printing Bash shell scripts.
diff --git a/language-bash.cabal b/language-bash.cabal
--- a/language-bash.cabal
+++ b/language-bash.cabal
@@ -1,15 +1,16 @@
 name:               language-bash
-version:            0.6.0
+version:            0.6.1
 category:           Language
 license:            BSD3
 license-file:       LICENSE
 author:             Kyle Raftogianis
 maintainer:         Kyle Raftogianis <kylerafto@gmail.com>
-copyright:          Copyright (c) 2013 Kyle Raftogianis
+copyright:          Copyright (c) 2013-2015 Kyle Raftogianis
 build-type:         Simple
 cabal-version:      >= 1.8
 homepage:           http://github.com/knrafto/language-bash/
 bug-reports:        http://github.com/knrafto/language-bash/issues
+tested-with:        GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.1, GHC == 7.10.2
 synopsis:           Parsing and pretty-printing Bash shell scripts
 description:
     A library for parsing, pretty-printing, and manipulating
@@ -41,9 +42,23 @@
     Language.Bash.Parse.Internal
 
   build-depends:
-    base         >= 4   && < 5,
+    base         >= 4.6 && < 5,
     parsec       >= 3.0 && < 4.0,
     pretty       >= 1.0 && < 2.0,
-    transformers >= 0.2 && < 0.4
+    transformers >= 0.2 && < 0.5
 
   ghc-options: -Wall
+
+test-suite tests
+  type: exitcode-stdio-1.0
+  hs-source-dirs: tests
+  main-is: Tests.hs
+
+  build-depends:
+    base,
+    language-bash,
+    parsec,
+    process,
+    QuickCheck,
+    tasty,
+    tasty-quickcheck
diff --git a/src/Language/Bash/Cond.hs b/src/Language/Bash/Cond.hs
--- a/src/Language/Bash/Cond.hs
+++ b/src/Language/Bash/Cond.hs
@@ -1,5 +1,7 @@
 {-# LANGUAGE
-    DeriveFoldable
+    DeriveDataTypeable
+  , DeriveFoldable
+  , CPP
   , DeriveFunctor
   , DeriveTraversable
   , OverloadedStrings
@@ -15,12 +17,17 @@
 import Prelude                hiding (negate)
 
 import Control.Applicative
-import Data.Foldable          (Foldable)
-import Data.Traversable       (Traversable)
+import Data.Data              (Data)
+import Data.Typeable          (Typeable)
 import Text.Parsec            hiding ((<|>), token)
 import Text.Parsec.Expr       hiding (Operator)
 import Text.PrettyPrint       hiding (parens)
 
+#if __GLASGOW_HASKELL__ < 710
+import Data.Foldable (Foldable)
+import Data.Traversable (Traversable)
+#endif
+
 import Language.Bash.Operator
 import Language.Bash.Pretty
 
@@ -31,7 +38,7 @@
     | Not (CondExpr a)
     | And (CondExpr a) (CondExpr a)
     | Or (CondExpr a) (CondExpr a)
-    deriving (Eq, Read, Show, Functor, Foldable, Traversable)
+    deriving (Data, Eq, Read, Show, Typeable, Functor, Foldable, Traversable)
 
 instance Pretty a => Pretty (CondExpr a) where
     pretty = go (0 :: Int)
@@ -70,7 +77,7 @@
     | Varname        -- ^ @-v@
     | ZeroString     -- ^ @-z@
     | NonzeroString  -- ^ @-n /string/@ or @/string/@
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Operator UnaryOp where
     operatorTable =
@@ -99,7 +106,7 @@
     | ArithLE    -- ^ @-le@
     | ArithGT    -- ^ @-gt@
     | ArithGE    -- ^ @-ge@
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Operator BinaryOp where
     operatorTable =
diff --git a/src/Language/Bash/Expand.hs b/src/Language/Bash/Expand.hs
--- a/src/Language/Bash/Expand.hs
+++ b/src/Language/Bash/Expand.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE LambdaCase, OverloadedStrings, PatternGuards #-}
+{-# LANGUAGE OverloadedStrings, PatternGuards, CPP #-}
 -- | Shell expansions.
 module Language.Bash.Expand
     ( braceExpand
@@ -7,21 +7,32 @@
     , splitWord
     ) where
 
+#if __GLASGOW_HASKELL__ >= 710
+import Prelude hiding (Word)
+#else
+import Data.Traversable (traverse)
+#endif
+
 import Control.Applicative
 import Control.Monad
 import Data.Char
-import Data.Traversable
 import Text.Parsec.Combinator hiding (optional, manyTill)
 import Text.Parsec.Prim       hiding ((<|>), many, token)
 import Text.Parsec.String     ()
 import Text.PrettyPrint       hiding (char)
 
 import Language.Bash.Pretty
-import Language.Bash.Word
+import Language.Bash.Word     hiding (prefix)
 
 -- | A parser over words.
 type Parser = Parsec Word ()
 
+infixl 3 </>
+
+-- | Backtracking choice.
+(</>) :: ParsecT s u m a -> ParsecT s u m a -> ParsecT s u m a
+p </> q = try p <|> q
+
 -- | Run a 'Parser', failing on a parse error.
 parseUnsafe :: String -> Parser a -> Word -> a
 parseUnsafe f p w = case parse p (prettyText w) w of
@@ -33,27 +44,35 @@
 token = tokenPrim (const "") (\pos _ _ -> pos)
 
 -- | Parse an unquoted character satisfying a predicate.
-satisfy :: (Char -> Bool) -> Parser Char
-satisfy p = token $ \case
+satisfy :: (Char -> Bool) -> Parser Span
+satisfy p = token $ \t -> case t of
+    Char c | p c -> Just t
+    _            -> Nothing
+
+-- | Parse an unquoted character satisfying a predicate.
+satisfy' :: (Char -> Bool) -> Parser Char
+satisfy' p = token $ \t -> case t of
     Char c | p c -> Just c
     _            -> Nothing
 
 -- | Parse a span that is not an unquoted character satisfying a predicate.
 except :: (Char -> Bool) -> Parser Span
-except p = token $ \case
+except p = token $ \t -> case t of
     Char c | p c -> Nothing
-    s            -> Just s
+    _            -> Just t
 
 -- | Parse an unquoted character.
-char :: Char -> Parser Char
-char c = satisfy (== c)
+char :: Char -> Parser Span
+char c = token $ \t -> case t of
+    Char d | c == d -> Just t
+    _               -> Nothing
 
 -- | Parse an unquoted string.
-string :: String -> Parser String
+string :: String -> Parser Word
 string = traverse char
 
 -- | Parse one of the given characters.
-oneOf :: [Char] -> Parser Char
+oneOf :: [Char] -> Parser Span
 oneOf cs = satisfy (`elem` cs)
 
 -- | Parse anything but a quoted character.
@@ -85,25 +104,42 @@
 
 -- | Brace expand a word.
 braceExpand :: Word -> [Word]
-braceExpand = parseUnsafe "braceExpand" (go "")
+braceExpand = parseUnsafe "braceExpand" start
   where
-    go delims = try (brace delims)
-            <|> (:[]) <$> many (noneOf delims)
+    prefix a bs = map (a ++) bs
+    cross as bs = [a ++ b | a <- as, b <- bs]
 
-    brace delims = do
-        a  <- many (noneOf ('{':delims))
-        _  <- char '{'
-        bs <- try sequenceExpand <|> braceParts
-        _  <- char '}'
-        cs <- go delims
-        return [ a ++ b ++ c | b <- bs, c <- cs ]
+    -- A beginning empty brace is ignored.
+    start = prefix <$> string "{}" <*> expr ""
+        </> expr ""
 
-    braceParts = concatParts <$> go ",}" `sepBy` char ','
+    expr delims = foldr ($) [""] <$> many (exprPart delims)
 
-    concatParts []   = ["{}"]
-    concatParts [xs] = map (\x -> "{" ++ x ++ "}") xs
-    concatParts xss  = concat xss
+    exprPart delims = cross <$ char '{' <*> brace delims <* char '}'
+                  </> prefix <$> emptyBrace
+                  </> prefix . (:[]) <$> noneOf delims
 
+    brace delims = concat <$> braceParts delims
+               </> sequenceExpand
+               </> map (\s -> "{" ++ s ++ "}") <$> expr ",}"
+
+    -- The first part of the outermost brace expression is not delimited by
+    -- a close brace.
+    braceParts delims =
+        (:) <$> expr (if ',' `elem` delims then ",}" else ",") <* char ','
+            <*> expr ",}" `sepBy1` char ','
+
+    emptyBrace = do
+        a <- token $ \t -> case t of
+            Char c   | c `elem` ws -> Just t
+            Escape c | c `elem` ws -> Just t
+            _                      -> Nothing
+        b <- char '{'
+        c <- char '}' <|> oneOf ws
+        return [a, b, c]
+      where
+        ws = " \t\r\n"
+
     sequenceExpand = do
         a   <- sequencePart
         b   <- string ".." *> sequencePart
@@ -111,7 +147,7 @@
         inc <- traverse readNumber c
         map fromString <$> (numExpand a b inc <|> charExpand a b inc)
       where
-        sequencePart = many1 (satisfy isAlphaNum)
+        sequencePart = many1 (satisfy' isAlphaNum)
 
     charExpand a b inc = do
         x <- readAlpha a
@@ -159,7 +195,7 @@
     ('~':s, w') -> Just (readPrefix s, w')
     _           -> Nothing
   where
-    split = (,) <$> many (satisfy (/= '/')) <*> getInput
+    split = (,) <$> many (satisfy' (/= '/')) <*> getInput
 
     readPrefix s
         | s == ""                = Home
diff --git a/src/Language/Bash/Parse.hs b/src/Language/Bash/Parse.hs
--- a/src/Language/Bash/Parse.hs
+++ b/src/Language/Bash/Parse.hs
@@ -20,7 +20,7 @@
 import           Language.Bash.Parse.Internal
 import           Language.Bash.Pretty
 import           Language.Bash.Syntax
-import           Language.Bash.Word           (Word, fromString, unquote)
+import           Language.Bash.Word           (fromString, unquote)
 
 -- | User state.
 data U = U { postHeredoc :: Maybe (State D U) }
@@ -291,13 +291,13 @@
            <*  word "do" <*> compoundList <* word "done"
 
 -- | Parse a list of words for a @for@ or @select@ command.
-wordList :: Parser [Word]
-wordList = ["$@\""] <$ operator ";" <* newlineList
+wordList :: Parser WordList
+wordList = Args <$ operator ";" <* newlineList
        <|> newlineList *> inList
        <?> "word list"
   where
-    inList = word "in" *> many anyWord <* listTerm
-         <|> return ["$@\""]
+    inList = WordList <$ word "in" <*> many anyWord <* listTerm
+         <|> pure Args
 
 -- | Parse a @for@ command.
 forCommand :: Parser ShellCommand
diff --git a/src/Language/Bash/Parse/Internal.hs b/src/Language/Bash/Parse/Internal.hs
--- a/src/Language/Bash/Parse/Internal.hs
+++ b/src/Language/Bash/Parse/Internal.hs
@@ -2,6 +2,7 @@
     FlexibleContexts
   , FlexibleInstances
   , LambdaCase
+  , CPP
   , MultiParamTypeClasses
   , OverloadedStrings
   , PatternGuards
@@ -35,6 +36,10 @@
       -- * Here documents
     , heredocWord
     ) where
+
+#if __GLASGOW_HASKELL__ >= 710
+import Prelude hiding (Word)
+#endif
 
 import           Control.Applicative
 import           Control.Monad
diff --git a/src/Language/Bash/Parse/Word.hs b/src/Language/Bash/Parse/Word.hs
--- a/src/Language/Bash/Parse/Word.hs
+++ b/src/Language/Bash/Parse/Word.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE FlexibleContexts, RecordWildCards #-}
+{-# LANGUAGE FlexibleContexts, RecordWildCards, CPP #-}
 -- | Word-level parsers.
 module Language.Bash.Parse.Word
     ( skipSpace
@@ -10,6 +10,10 @@
     , assign
     , operator
     ) where
+
+#if __GLASGOW_HASKELL__ >= 710
+import Prelude hiding (Word)
+#endif
 
 import           Control.Applicative
 import           Control.Monad
diff --git a/src/Language/Bash/Syntax.hs b/src/Language/Bash/Syntax.hs
--- a/src/Language/Bash/Syntax.hs
+++ b/src/Language/Bash/Syntax.hs
@@ -1,10 +1,11 @@
-{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
+{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, RecordWildCards, CPP #-}
 -- | Shell script types.
 module Language.Bash.Syntax
     (
       -- * Commands
       Command(..)
     , ShellCommand(..)
+    , WordList(..)
     , CaseClause(..)
     , CaseTerm(..)
       -- * Redirections
@@ -24,6 +25,12 @@
     , RValue(..)
     ) where
 
+#if __GLASGOW_HASKELL__ >= 710
+import Prelude hiding (Word)
+#endif
+
+import Data.Data        (Data)
+import Data.Typeable    (Typeable)
 import Text.PrettyPrint
 
 import Language.Bash.Cond     (CondExpr)
@@ -41,7 +48,7 @@
 
 -- | A Bash command with redirections.
 data Command = Command ShellCommand [Redir]
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty Command where
     pretty (Command c rs) = pretty c <+> pretty rs
@@ -67,12 +74,12 @@
     | Cond (CondExpr Word)
       -- | A @for /name/ in /words/@ command. If @in /words/@ is absent,
       -- the word list defaults to @\"$\@\"@.
-    | For String [Word] List
+    | For String WordList List
       -- | An arithmetic @for ((...))@ command.
     | ArithFor String List
       -- | A @select /name/ in /words/@ command. If @in /words/@ is absent,
       -- the word list defaults to @\"$\@\"@.
-    | Select String [Word] List
+    | Select String WordList List
       -- | A @case@ command.
     | Case Word [CaseClause]
       -- | An @if@ command, with a predicate, consequent, and alternative.
@@ -82,7 +89,7 @@
     | Until List List
       -- | A @while@ command.
     | While List List
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty ShellCommand where
     pretty (SimpleCommand as ws)  = pretty as <+> pretty ws
@@ -100,13 +107,13 @@
     pretty (Cond e) =
         "[[" <+> pretty e <+> "]]"
     pretty (For w ws l) =
-        "for" <+> pretty w <+> "in" <+> pretty ws <> ";" $+$ doDone l
+        "for" <+> pretty w <+> pretty ws <> ";" $+$ doDone l
     pretty (ArithFor s l) =
         "for" <+> "((" <> text s <> "))" $+$ doDone l
     pretty (Select w ws l) =
-        "select" <+> pretty w <+> "in" <+> pretty ws <> ";" $+$ doDone l
+        "select" <+> pretty w <+> pretty ws <> ";" $+$ doDone l
     pretty (Case w cs) =
-        "case" <+> pretty w <+> "in" $+$ indent cs $+$ "esac"
+        "case" <+> pretty w <+> "in" $+$ (vcat $ map indent cs) $+$ "esac"
     pretty (If p t f) =
         "if" <+> pretty p <+> "then" $+$ indent t $+$
         pretty (fmap (\l -> "else" $+$ indent l) f) $+$
@@ -116,22 +123,32 @@
     pretty (While p l) =
         "while" <+> pretty p <+> doDone l
 
+-- | A word list or @\"$\@\"@.
+data WordList
+    = Args
+    | WordList [Word]
+    deriving (Data, Eq, Read, Show, Typeable)
+
+instance Pretty WordList where
+    pretty Args          = empty
+    pretty (WordList ws) = "in" <+> pretty ws
+
 -- | A single case clause.
 data CaseClause = CaseClause [Word] List CaseTerm
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty CaseClause where
     pretty (CaseClause ps l term) =
         hcat (punctuate " | " (map pretty ps)) <> ")" $+$
         indent l $+$
-        pretty term
+        (indent $ pretty term)
 
 -- | A case clause terminator.
 data CaseTerm
     = Break        -- ^ @;;@
     | FallThrough  -- ^ @;&@
     | Continue     -- ^ @;;&@
-    deriving (Eq, Ord, Read, Show, Bounded, Enum)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Bounded, Enum)
 
 instance Operator CaseTerm where
     operatorTable = zip [minBound .. maxBound] [";;", ";&", ";;&"]
@@ -163,7 +180,7 @@
           -- and command substitutions take place.
         , hereDocument       :: Word
         }
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty Redir where
     pretty Redir{..} =
@@ -186,7 +203,7 @@
     = IONumber Int
       -- | A variable @{/varname/}@ to allocate a file descriptor for.
     | IOVar String
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty IODesc where
     pretty (IONumber n) = int n
@@ -204,7 +221,7 @@
     | InAnd       -- ^ @\<&@
     | OutAnd      -- ^ @\>&@
     | InOut       -- ^ @\<\>@
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Operator RedirOp where
     operatorTable = zip [minBound .. maxBound]
@@ -217,7 +234,7 @@
 data HeredocOp
     = Here       -- ^ @\<\<@
     | HereStrip  -- ^ @\<\<-@
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Operator HeredocOp where
     operatorTable = zip [Here, HereStrip] ["<<", "<<-"]
@@ -227,14 +244,14 @@
 
 -- | A compound list of statements.
 newtype List = List [Statement]
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty List where
     pretty (List as) = pretty as
 
 -- | A single statement in a list.
 data Statement = Statement AndOr ListTerm
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty Statement where
     pretty (Statement l Sequential)   = pretty l <> ";"
@@ -249,7 +266,7 @@
 data ListTerm
     = Sequential    -- ^ @;@
     | Asynchronous  -- ^ @&@
-    deriving (Eq, Ord, Read, Show, Bounded, Enum)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Bounded, Enum)
 
 instance Operator ListTerm where
     operatorTable =
@@ -269,7 +286,7 @@
     | And Pipeline AndOr
       -- | A @||@ construct.
     | Or Pipeline AndOr
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty AndOr where
     pretty (Last p)  = pretty p
@@ -288,7 +305,7 @@
       -- @command1 |& command2@ is treated as a shorthand for
       -- @command1 2>&1 | command2@.
     , commands   :: [Command]
-    } deriving (Eq, Read, Show)
+    } deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty Pipeline where
     pretty Pipeline{..} =
@@ -299,7 +316,7 @@
 
 -- | An assignment.
 data Assign = Assign Parameter AssignOp RValue
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty Assign where
     pretty (Assign lhs op rhs) = pretty lhs <> pretty op <> pretty rhs
@@ -308,7 +325,7 @@
 data AssignOp
     = Equals      -- ^ @=@
     | PlusEquals  -- ^ @+=@
-    deriving (Eq, Ord, Read, Show, Bounded, Enum)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Bounded, Enum)
 
 instance Operator AssignOp where
     operatorTable = zip [Equals, PlusEquals] ["=", "+="]
@@ -322,7 +339,7 @@
     = RValue Word
       -- | An array assignment, as @(subscript, word)@ pairs.
     | RArray [(Maybe Word, Word)]
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty RValue where
     pretty (RValue w)  = pretty w
diff --git a/src/Language/Bash/Word.hs b/src/Language/Bash/Word.hs
--- a/src/Language/Bash/Word.hs
+++ b/src/Language/Bash/Word.hs
@@ -1,5 +1,7 @@
 {-# LANGUAGE
-    FlexibleInstances
+    DeriveDataTypeable
+  , FlexibleInstances
+  , CPP
   , OverloadedStrings
   , RecordWildCards
   , TypeSynonymInstances
@@ -23,7 +25,13 @@
     , unquote
     ) where
 
+#if __GLASGOW_HASKELL__ >= 710
+import Prelude hiding (Word)
+#endif
+
+import           Data.Data        (Data)
 import qualified Data.String
+import           Data.Typeable    (Typeable)
 import           Text.PrettyPrint
 
 import           Language.Bash.Operator
@@ -60,7 +68,7 @@
     | CommandSubst String
       -- | A process substitution.
     | ProcessSubst ProcessSubstOp String
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty Span where
     pretty (Char c)           = char c
@@ -79,7 +87,7 @@
 
 -- | A parameter name an optional subscript.
 data Parameter = Parameter String (Maybe Word)
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 instance Pretty Parameter where
     pretty (Parameter s sub) = text s <> subscript sub
@@ -158,7 +166,7 @@
         , convertAll        :: Bool
         , pattern           :: Word
         }
-    deriving (Eq, Read, Show)
+    deriving (Data, Eq, Read, Show, Typeable)
 
 prettyParameter :: Bool -> Parameter -> Doc -> Doc
 prettyParameter bang param suffix =
@@ -201,7 +209,7 @@
     | AltAssign   -- ^ '=', ':='
     | AltError    -- ^ '?', ':?'
     | AltReplace  -- ^ '+', ':+'
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Operator AltOp where
     operatorTable = zip [minBound .. maxBound] ["-", "=", "?", "+"]
@@ -213,7 +221,7 @@
 data LetterCaseOp
     = ToLower
     | ToUpper
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Operator LetterCaseOp where
     operatorTable = zip [ToLower, ToUpper] [",", "^"]
@@ -225,7 +233,7 @@
 data Direction
     = Front
     | Back
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Pretty Direction where
     pretty Front = "#"
@@ -235,7 +243,7 @@
 data ProcessSubstOp
     = ProcessIn   -- ^ @\<@
     | ProcessOut  -- ^ @\>@
-    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+    deriving (Data, Eq, Ord, Read, Show, Typeable, Enum, Bounded)
 
 instance Operator ProcessSubstOp where
     operatorTable = zip [ProcessIn, ProcessOut] ["<", ">"]
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,46 @@
+module Main (main) where
+
+import Control.Applicative
+import System.Process           (readProcess)
+import Test.QuickCheck
+import Test.QuickCheck.Monadic
+import Test.Tasty
+import Test.Tasty.QuickCheck
+import Text.Parsec              (parse)
+
+import Language.Bash.Expand     (braceExpand)
+import Language.Bash.Parse.Word (word)
+import Language.Bash.Word       (unquote)
+
+-- TODO sequence
+braceExpr :: Gen String
+braceExpr = concat <$> listOf charset
+  where
+    charset = oneof $
+        [ elements ["{", ",", "}"]
+        , elements ["\\ ", "\\{", "\\,", "\\}"]
+        , (:[]) <$> elements ['a'..'z']
+        ]
+
+expandWithBash :: String -> IO String
+expandWithBash str = do
+    expn <- readProcess "/usr/bin/env" ["bash", "-c", "echo " ++ str] ""
+    return $ filter (`notElem` "\r\n") expn
+
+testExpand :: String -> [String]
+testExpand s = case parse word "" s of
+    Left  e -> error (show e)
+    Right w -> map unquote (braceExpand w)
+
+-- Tests brace expansion.
+prop_expandsLikeBash :: Property
+prop_expandsLikeBash = monadicIO $ forAllM braceExpr $ \str -> do
+    bash <- run $ expandWithBash str
+    let check = unwords . filter (not . null) $ testExpand str
+    assert (bash == check)
+
+tests :: TestTree
+tests = testProperty "brace expansion" prop_expandsLikeBash
+
+main :: IO ()
+main = defaultMain tests
