diff --git a/language-bash.cabal b/language-bash.cabal
--- a/language-bash.cabal
+++ b/language-bash.cabal
@@ -1,5 +1,5 @@
 name:               language-bash
-version:            0.2.0
+version:            0.3.0
 category:           Language
 license:            BSD3
 license-file:       LICENSE
@@ -26,12 +26,14 @@
   hs-source-dirs: src
 
   exposed-modules:
+    Language.Bash.Cond
     Language.Bash.Parse
     Language.Bash.Parse.Builder
     Language.Bash.Pretty
     Language.Bash.Syntax
 
   other-modules:
+    Language.Bash.Operator
     Language.Bash.Parse.Internal
     Language.Bash.Parse.Packrat
 
diff --git a/src/Language/Bash/Cond.hs b/src/Language/Bash/Cond.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Bash/Cond.hs
@@ -0,0 +1,101 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Bash conditional commands.
+module Language.Bash.Cond
+    ( CondExpr(..)
+    , UnaryOp(..)
+    , BinaryOp(..)
+    ) where
+
+import Text.PrettyPrint
+
+import Language.Bash.Operator
+import Language.Bash.Pretty
+
+-- | Bash conditional expressions.
+data CondExpr
+    = Unary UnaryOp String
+    | Binary String BinaryOp String
+    | Not CondExpr
+    | And CondExpr CondExpr
+    | Or CondExpr CondExpr
+    deriving (Eq, Read, Show)
+
+instance Pretty CondExpr where
+    pretty = go (0 :: Int)
+      where
+        go _ (Unary op a)    = pretty op <+> text a
+        go _ (Binary a op b) = text a <+> pretty op <+> text b
+        go _ (Not e)         = "!" <+> go 2 e
+        go p (And e1 e2)     = paren (p > 1) $ go 1 e1 <+> "&&" <+> go 1 e2
+        go p (Or e1 e2)      = paren (p > 0) $ go 0 e1 <+> "||" <+> go 0 e2
+
+        paren False d = d
+        paren True d  = "(" <+> d <+> ")"
+
+-- | Unary conditional operators.
+data UnaryOp
+    = BlockFile      -- ^ @-b@
+    | CharacterFile  -- ^ @-c@
+    | Directory      -- ^ @-d@
+    | FileExists     -- ^ @-e@, @-a@
+    | RegularFile    -- ^ @-f@
+    | SetGID         -- ^ @-g@
+    | Sticky         -- ^ @-k@
+    | NamedPipe      -- ^ @-p@
+    | Readable       -- ^ @-r@
+    | FileSize       -- ^ @-s@
+    | Terminal       -- ^ @-t@
+    | SetUID         -- ^ @-u@
+    | Writable       -- ^ @-w@
+    | Executable     -- ^ @-x@
+    | GroupOwned     -- ^ @-G@
+    | SymbolicLink   -- ^ @-L@, @-h@
+    | Modified       -- ^ @-N@
+    | UserOwned      -- ^ @-O@
+    | Socket         -- ^ @-S@
+    | Optname        -- ^ @-o@
+    | Varname        -- ^ @-v@
+    | ZeroString     -- ^ @-z@
+    | NonzeroString  -- ^ @-n /string/@ or @/string/@
+    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+
+instance Operator UnaryOp where
+    operatorTable =
+        zip [minBound .. maxBound]
+            (map (\c -> ['-', c]) "bcdefgkprstuwxGLNOSovzn") ++
+        [ (FileExists  , "-a")
+        , (SymbolicLink, "-h")
+        ]
+
+instance Pretty UnaryOp where
+    pretty = prettyOperator
+
+-- | Binary conditional operators.
+data BinaryOp
+    = SameFile   -- ^ @-ef@
+    | NewerThan  -- ^ @-nt@
+    | OlderThan  -- ^ @-ot@
+    | StrMatch   -- ^ @=~@
+    | StrEQ      -- ^ @==@, @=@
+    | StrNE      -- ^ @!=@
+    | StrLT      -- ^ @<@
+    | StrGT      -- ^ @>@
+    | ArithEQ    -- ^ @-eq@
+    | ArithNE    -- ^ @-ne@
+    | ArithLT    -- ^ @-lt@
+    | ArithLE    -- ^ @-le@
+    | ArithGT    -- ^ @-gt@
+    | ArithGE    -- ^ @-ge@
+    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+
+instance Operator BinaryOp where
+    operatorTable =
+        zip [minBound .. maxBound]
+            [ "-ef", "-nt", "-ot"
+            , "=~", "==", "!=", "<", ">"
+            , "-eq", "-ne", "-lt", "-le", "-gt", "-ge"
+            ] ++
+        [ (StrEQ, "=") ]
+
+instance Pretty BinaryOp where
+    pretty = prettyOperator
diff --git a/src/Language/Bash/Operator.hs b/src/Language/Bash/Operator.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Bash/Operator.hs
@@ -0,0 +1,28 @@
+-- | Bash has a lot of operators.
+module Language.Bash.Operator
+    ( Operator(..)
+    , select
+    , selectOperator
+    , prettyOperator
+    ) where
+
+import Control.Applicative
+import Text.PrettyPrint     hiding (empty)
+
+import Language.Bash.Pretty
+
+-- | String operators.
+class Eq a => Operator a where
+    operatorTable :: [(a, String)]
+
+-- | Select the first element that succeeds from a table.
+select :: Alternative f => (b -> f c) -> [(a, b)] -> f a
+select p = foldr (<|>) empty . map (\(a, b) -> a <$ p b)
+
+-- | Select an operator from the 'String' it represents.
+selectOperator :: (Alternative f, Operator a) => (String -> f c) -> f a
+selectOperator p = select p operatorTable
+
+-- | Render an operator.
+prettyOperator :: Operator a => a -> Doc
+prettyOperator = pretty . flip lookup operatorTable
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
@@ -10,9 +10,12 @@
 import           Text.Parsec.Char             hiding (newline)
 import           Text.Parsec.Combinator       hiding (optional)
 import           Text.Parsec.Error            (ParseError)
+import           Text.Parsec.Expr
 import           Text.Parsec.Pos
 import           Text.Parsec.Prim             hiding (parse, (<|>))
 
+import qualified Language.Bash.Cond           as Cond
+import           Language.Bash.Operator
 import qualified Language.Bash.Parse.Internal as I
 import           Language.Bash.Parse.Packrat
 import           Language.Bash.Syntax
@@ -112,20 +115,24 @@
             }
 
     heredocRedir = do
-        (strip, op) <- heredocOperator
+        strip <- heredocOperator
         w <- anyWord
         let delim = I.unquote w
         h <- heredoc strip delim
         return Heredoc
-            { redirOp            = op
+            { heredocStrip       = strip
             , heredocDelim       = delim
             , heredocDelimQuoted = delim /= w
-            , document           = h
+            , hereDocument       = h
             }
 
-    heredocOperator = (,) False <$> operator "<<"
-                  <|> (,) True  <$> operator "<<-"
+    redirOperator = selectOperator word
+                <?> "redirection operator"
 
+    heredocOperator = False <$ operator "<<"
+                  <|> True  <$ operator "<<-"
+                  <?> "here document operator"
+
 -- | Skip a list of redirections.
 redirList :: Parser [Redir]
 redirList = many redir
@@ -214,7 +221,7 @@
 
     addRedir (Command c rs) = Command c (stderrRedir : rs)
 
-    stderrRedir = Redir (Just (IONumber 2)) ">&" "1"
+    stderrRedir = Redir (Just (IONumber 2)) OutAnd "1"
 
 -- | Parse a compound list of commands.
 compoundList :: Parser List
@@ -337,7 +344,29 @@
 
 -- | Parse a conditional command.
 condCommand :: Parser ShellCommand
-condCommand = Cond <$ word "[[" <*> many1 condWord <* word "]]"
+condCommand = Cond <$ word "[[" <*> expr <* word "]]"
+  where
+    expr = buildExpressionParser opTable term
+
+    term = word "(" *> expr <* word ")"
+       <|> Cond.Unary <$> unaryOp <*> condWord
+       <|> (condWord >>= wordTerm)
+
+    wordTerm w = Cond.Binary w <$> binaryOp <*> condWord
+             <|> pure (Cond.Unary Cond.NonzeroString w)
+
+    opTable =
+        [ [Prefix (Cond.Not <$ word "!")]
+        , [Infix  (Cond.And <$ operator "&&") AssocLeft]
+        , [Infix  (Cond.Or  <$ operator "||") AssocLeft]
+        ]
+
+    condWord = (anyWord <|> anyOperator) `satisfying` (/= "]]") <?> "word"
+
+    condOperator op = condWord `satisfying` (== op) <?> op
+
+    unaryOp  = selectOperator condOperator <?> "unary operator"
+    binaryOp = selectOperator condOperator <?> "binary operator"
 
 -------------------------------------------------------------------------------
 -- Coprocesses
diff --git a/src/Language/Bash/Parse/Packrat.hs b/src/Language/Bash/Parse/Packrat.hs
--- a/src/Language/Bash/Parse/Packrat.hs
+++ b/src/Language/Bash/Parse/Packrat.hs
@@ -12,6 +12,8 @@
     ( -- * Packrat parsing
       D
     , pack
+      -- * Tokens
+    , satisfying
       -- * Whitespace
     , I.skipSpace
       -- * Words
@@ -19,14 +21,12 @@
     , word
     , reservedWord
     , unreservedWord
-    , condWord
     , assignBuiltin
     , ioDesc
     , name
       -- * Operators
     , anyOperator
     , operator
-    , redirOperator
       -- * Assignments
     , assign
       -- * Arithmetic expressions
@@ -99,7 +99,7 @@
         _ioDesc      = result $ token >>= \case
             TIODesc desc -> return desc
             _            -> empty
-        _anyOperator = result $ I.operator normalOps <* I.skipSpace
+        _anyOperator = result $ I.operator operators <* I.skipSpace
         _assign      = result $ I.assign <* I.skipSpace
         _uncons      = case s of
             []     -> Nothing
@@ -116,6 +116,32 @@
     t <- a
     if p t then return t else unexpected (show t)
 
+-- | Shell reserved words.
+reservedWords :: [Word]
+reservedWords =
+    [ "!", "[[", "]]", "{", "}"
+    , "if", "then", "else", "elif", "fi"
+    , "case", "esac", "for", "select", "while", "until"
+    , "in", "do", "done", "time", "function"
+    ]
+
+-- | Shell assignment builtins. These builtins can take assignments as
+-- arguments.
+assignBuiltins :: [Word]
+assignBuiltins =
+    [ "alias", "declare", "export", "eval"
+    , "let", "local", "readonly", "typeset"
+    ]
+
+-- | All Bash operators.
+operators :: [String]
+operators =
+    [ "(", ")", ";;", ";&", ";;&"
+    , "|", "|&", "||", "&&", ";", "&", "\n"
+    , "<", ">", ">|", ">>", "&>", "&>>", "<<<", "<&", ">&", "<>"
+    , "<<", "<<-"
+    ]
+
 -- | Parse a descriptor.
 descriptor :: Stream s m Char => ParsecT s u m IODesc
 descriptor = IONumber . read <$> many1 digit
@@ -142,10 +168,6 @@
 unreservedWord = anyWord `satisfying` (`notElem` reservedWords)
     <?> "unreserved word"
 
--- | Parse a word in a @[[...]]@ command.
-condWord :: Monad m => ParsecT D u m Word
-condWord = (anyWord <|> anyOperator) `satisfying` (/= "]]")
-
 -- | Parse an assignment builtin.
 assignBuiltin :: Monad m => ParsecT D u m Word
 assignBuiltin = anyWord `satisfying` (`elem` assignBuiltins)
@@ -170,11 +192,6 @@
 -- | Parse a given operator.
 operator :: Monad m => String -> ParsecT D u m String
 operator op = anyOperator `satisfying` (== op) <?> op
-
--- | Parse a non-heredoc redirection operator.
-redirOperator :: Monad m => ParsecT D u m String
-redirOperator = anyOperator `satisfying` (`elem` redirOps)
-    <?> "redirection operator"
 
 -- | Parse an assignment.
 assign :: Monad m => ParsecT D u m Assign
diff --git a/src/Language/Bash/Pretty.hs b/src/Language/Bash/Pretty.hs
--- a/src/Language/Bash/Pretty.hs
+++ b/src/Language/Bash/Pretty.hs
@@ -1,15 +1,12 @@
-{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
 -- | Pretty-printing of Bash scripts. This tries to stay close to the format
 -- used by the Bash builtin @declare -f@.
 module Language.Bash.Pretty
     ( Pretty(..)
-    , render
+    , prettyText
     ) where
 
 import Text.PrettyPrint
 
-import Language.Bash.Syntax
-
 -- | A class of types which may be pretty-printed.
 class Pretty a where
     -- | Pretty-print to a 'Doc'.
@@ -36,120 +33,6 @@
 instance (Pretty a, Pretty b) => Pretty (Either a b) where
     pretty = either pretty pretty
 
-instance Pretty Command where
-    pretty (Command c rs) = pretty c <+> pretty rs
-
-instance Pretty Redir where
-    pretty Redir{..} =
-        pretty redirDesc <> text redirOp <> text redirTarget
-    pretty Heredoc{..} =
-        text redirOp <>
-        text (if heredocDelimQuoted
-              then "'" ++ heredocDelim ++ "'"
-              else heredocDelim) <> "\n" <>
-        text document <> text heredocDelim <> "\n"
-
-    prettyList = foldr f empty
-      where
-        f a@(Redir{})   b = pretty a <+> b
-        f a@(Heredoc{}) b = pretty a <> b
-
-instance Pretty IODesc where
-    pretty (IONumber n) = int n
-    pretty (IOVar n)    = "{" <> text n <> "}"
-
-instance Pretty ShellCommand where
-    pretty (SimpleCommand as ws)  = pretty as <+> pretty ws
-    pretty (AssignBuiltin w args) = text w <+> pretty args
-    pretty (FunctionDef name l) =
-        text name <+> "()" $+$ pretty (Group l)
-    pretty (Coproc name c) =
-        "coproc" <+> text name <+> pretty c
-    pretty (Subshell l) =
-        "(" <+> pretty l <+> ")"
-    pretty (Group l) =
-        "{" $+$ indent l $+$ "}"
-    pretty (Arith s) =
-        "((" <> text s <> "))"
-    pretty (Cond ws) =
-        "[[" <+> pretty ws <+> "]]"
-    pretty (For w ws l) =
-        "for" <+> text w <+> "in" <+> pretty ws <> ";" $+$ doDone l
-    pretty (ArithFor s l) =
-        "for" <+> "((" <> text s <> "))" $+$ doDone l
-    pretty (Select w ws l) =
-        "select" <+> text w <+> "in" <+> pretty ws <> ";" $+$ doDone l
-    pretty (Case w cs) =
-        "case" <+> text w <+> "in" $+$ indent cs $+$ "esac"
-    pretty (If p t f) =
-        "if" <+> pretty p <+> "then" $+$ indent t $+$
-        pretty (fmap (\l -> "else" $+$ indent l) f) $+$
-        "fi"
-    pretty (Until p l) =
-        "until" <+> pretty p <+> doDone l
-    pretty (While p l) =
-        "while" <+> pretty p <+> doDone l
-
-instance Pretty CaseClause where
-    pretty (CaseClause ps l term) =
-        hcat (punctuate " | " (map text ps)) <> ")" $+$
-        indent l $+$
-        pretty term
-
-instance Pretty CaseTerm where
-    pretty Break       = ";;"
-    pretty FallThrough = ";&"
-    pretty Continue    = ";;&"
-
-instance Pretty List where
-    pretty (List as) = pretty as
-
-instance Pretty Statement where
-    pretty (Statement l Sequential)   = pretty l <> ";"
-    pretty (Statement l Asynchronous) = pretty l <+> "&"
-
-    prettyList = foldr f empty
-      where
-        f a@(Statement _ Sequential)   b = pretty a $+$ b
-        f a@(Statement _ Asynchronous) b = pretty a <+> b
-
-instance Pretty ListTerm where
-    pretty Sequential   = ";"
-    pretty Asynchronous = "&"
-
-instance Pretty AndOr where
-    pretty (Last p)  = pretty p
-    pretty (And p a) = pretty p <+> "&&" <+> pretty a
-    pretty (Or p a)  = pretty p <+> "||" <+> pretty a
-
-instance Pretty Pipeline where
-    pretty Pipeline{..} =
-        (if timed      then "time" else empty) <+>
-        (if timedPosix then "-p"   else empty) <+>
-        (if inverted   then "!"    else empty) <+>
-        pretty commands
-
-instance Pretty Assign where
-    pretty (Assign lhs op rhs) = pretty lhs <> pretty op <> pretty rhs
-
-instance Pretty LValue where
-    pretty (LValue name sub) =
-        text name <> pretty (fmap (\s -> "[" ++ s ++ "]") sub)
-
-instance Pretty AssignOp where
-    pretty Equals     = "="
-    pretty PlusEquals = "+="
-
-instance Pretty RValue where
-    pretty (RValue w)  = text w
-    pretty (RArray rs) = "(" <> hsep (map f rs) <> ")"
-      where
-        f (sub, w) = pretty (fmap (\s -> "[" ++ s ++ "]=") sub) <> text w
-
--- | Indent by 4 columns.
-indent :: Pretty a => a -> Doc
-indent = nest 4 . pretty
-
--- | Render a @do...done@ block.
-doDone :: Pretty a => a -> Doc
-doDone a = "do" $+$ indent a $+$ "done"
+-- | Pretty-print to a 'String'.
+prettyText :: Pretty a => a -> String
+prettyText = render . pretty
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,3 +1,4 @@
+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
 -- | Shell script types.
 module Language.Bash.Syntax
     ( -- * Syntax
@@ -8,6 +9,7 @@
     , Command(..)
     , Redir(..)
     , IODesc(..)
+    , RedirOp(..)
     , ShellCommand(..)
     , CaseClause(..)
     , CaseTerm(..)
@@ -22,15 +24,22 @@
     , LValue(..)
     , AssignOp(..)
     , RValue(..)
-      -- * Syntax elements
-    , reservedWords
-    , assignBuiltins
-    , redirOps
-    , heredocOps
-    , controlOps
-    , normalOps
     ) where
 
+import Text.PrettyPrint
+
+import Language.Bash.Cond     (CondExpr)
+import Language.Bash.Operator
+import Language.Bash.Pretty
+
+-- | Indent by 4 columns.
+indent :: Pretty a => a -> Doc
+indent = nest 4 . pretty
+
+-- | Render a @do...done@ block.
+doDone :: Pretty a => a -> Doc
+doDone a = "do" $+$ indent a $+$ "done"
+
 -- | A Bash word.
 type Word = String
 
@@ -41,6 +50,9 @@
 data Command = Command ShellCommand [Redir]
     deriving (Eq, Read, Show)
 
+instance Pretty Command where
+    pretty (Command c rs) = pretty c <+> pretty rs
+
 -- | A redirection.
 data Redir
     -- | A redirection.
@@ -48,22 +60,39 @@
         { -- | An optional file descriptor.
           redirDesc   :: Maybe IODesc
           -- | The redirection operator.
-        , redirOp     :: String
+        , redirOp     :: RedirOp
           -- | The redirection target.
         , redirTarget :: Word
         }
     -- | A here document.
     | Heredoc
-        { redirOp            :: String
+        { -- | 'True' if the here document was stripped of leading tabs using
+          -- the @\<\<-@ operator.
+          heredocStrip       :: Bool
           -- | The here document delimiter.
         , heredocDelim       :: String
           -- | 'True' if the delimiter was quoted.
         , heredocDelimQuoted :: Bool
           -- | The document itself.
-        , document           :: String
+        , hereDocument       :: String
         }
     deriving (Eq, Read, Show)
 
+instance Pretty Redir where
+    pretty Redir{..} =
+        pretty redirDesc <> pretty redirOp <> text redirTarget
+    pretty Heredoc{..} =
+        text (if heredocStrip then "<<-" else "<<") <>
+        text (if heredocDelimQuoted
+              then "'" ++ heredocDelim ++ "'"
+              else heredocDelim) <> "\n" <>
+        text hereDocument <> text heredocDelim <> "\n"
+
+    prettyList = foldr f empty
+      where
+        f a@Redir{}   b = pretty a <+> b
+        f a@Heredoc{} b = pretty a <> b
+
 -- | A redirection file descriptor.
 data IODesc
     -- | A file descriptor number.
@@ -72,6 +101,32 @@
     | IOVar String
     deriving (Eq, Read, Show)
 
+instance Pretty IODesc where
+    pretty (IONumber n) = int n
+    pretty (IOVar n)    = "{" <> text n <> "}"
+
+-- | A redirection operator.
+data RedirOp
+    = In          -- ^ @\<@
+    | Out         -- ^ @\>@
+    | OutOr       -- ^ @\>|@
+    | Append      -- ^ @\>\>@
+    | AndOut      -- ^ @&\>@
+    | AndAppend   -- ^ @&\>\>@
+    | HereString  -- ^ @\<\<\<@
+    | InAnd       -- ^ @\<&@
+    | OutAnd      -- ^ @\>&@
+    | InOut       -- ^ @\<\>@
+    deriving (Eq, Ord, Read, Show, Enum, Bounded)
+
+instance Operator RedirOp where
+    operatorTable = zip [minBound .. maxBound]
+        ["<", ">", ">|", ">>", "&>", "&>>", "<<<", "<&", ">&", "<>"]
+
+-- | A redirection operator.
+instance Pretty RedirOp where
+    pretty = prettyOperator
+
 -- | A Bash command.
 data ShellCommand
     -- | A simple command consisting of assignments followed by words.
@@ -90,7 +145,7 @@
     -- | An arithmetic expression.
     | Arith String
     -- | A Bash @[[...]]@ conditional expression.
-    | Cond [Word]
+    | Cond CondExpr
     -- | A @for /word/ in /words/@ command. If @in /words/@ is absent,
     -- the word list defaults to @\"$\@\"@.
     | For Word [Word] List
@@ -110,28 +165,80 @@
     | While List List
     deriving (Eq, Read, Show)
 
+instance Pretty ShellCommand where
+    pretty (SimpleCommand as ws)  = pretty as <+> pretty ws
+    pretty (AssignBuiltin w args) = text w <+> pretty args
+    pretty (FunctionDef name l) =
+        text name <+> "()" $+$ pretty (Group l)
+    pretty (Coproc name c) =
+        "coproc" <+> text name <+> pretty c
+    pretty (Subshell l) =
+        "(" <+> pretty l <+> ")"
+    pretty (Group l) =
+        "{" $+$ indent l $+$ "}"
+    pretty (Arith s) =
+        "((" <> text s <> "))"
+    pretty (Cond e) =
+        "[[" <+> pretty e <+> "]]"
+    pretty (For w ws l) =
+        "for" <+> text w <+> "in" <+> pretty ws <> ";" $+$ doDone l
+    pretty (ArithFor s l) =
+        "for" <+> "((" <> text s <> "))" $+$ doDone l
+    pretty (Select w ws l) =
+        "select" <+> text w <+> "in" <+> pretty ws <> ";" $+$ doDone l
+    pretty (Case w cs) =
+        "case" <+> text w <+> "in" $+$ indent cs $+$ "esac"
+    pretty (If p t f) =
+        "if" <+> pretty p <+> "then" $+$ indent t $+$
+        pretty (fmap (\l -> "else" $+$ indent l) f) $+$
+        "fi"
+    pretty (Until p l) =
+        "until" <+> pretty p <+> doDone l
+    pretty (While p l) =
+        "while" <+> pretty p <+> doDone l
+
 -- | A single case clause.
 data CaseClause = CaseClause [Word] List CaseTerm
     deriving (Eq, Read, Show)
 
+instance Pretty CaseClause where
+    pretty (CaseClause ps l term) =
+        hcat (punctuate " | " (map text ps)) <> ")" $+$
+        indent l $+$
+        pretty term
+
 -- | A case clause terminator.
 data CaseTerm
-    -- | The @;;@ operator.
-    = Break
-    -- | The @;&@ operator.
-    | FallThrough
-    -- | The @;;&@ operator.
-    | Continue
+    = Break        -- ^ @;;@
+    | FallThrough  -- ^ @;&@
+    | Continue     -- ^ @;;&@
     deriving (Eq, Ord, Read, Show, Bounded, Enum)
 
+instance Pretty CaseTerm where
+    pretty Break       = ";;"
+    pretty FallThrough = ";&"
+    pretty Continue    = ";;&"
+
 -- | A compound list of statements.
 newtype List = List [Statement]
     deriving (Eq, Read, Show)
 
+instance Pretty List where
+    pretty (List as) = pretty as
+
 -- | A single statement in a list.
 data Statement = Statement AndOr ListTerm
     deriving (Eq, Read, Show)
 
+instance Pretty Statement where
+    pretty (Statement l Sequential)   = pretty l <> ";"
+    pretty (Statement l Asynchronous) = pretty l <+> "&"
+
+    prettyList = foldr f empty
+      where
+        f a@(Statement _ Sequential)   b = pretty a $+$ b
+        f a@(Statement _ Asynchronous) b = pretty a <+> b
+
 -- | A statement terminator.
 data ListTerm
     -- | The @;@ operator.
@@ -140,6 +247,10 @@
     | Asynchronous
     deriving (Eq, Ord, Read, Show, Bounded, Enum)
 
+instance Pretty ListTerm where
+    pretty Sequential   = ";"
+    pretty Asynchronous = "&"
+
 -- | A right-associative list of pipelines.
 data AndOr
     -- | The last pipeline of a list.
@@ -150,6 +261,11 @@
     | Or Pipeline AndOr
     deriving (Eq, Read, Show)
 
+instance Pretty AndOr where
+    pretty (Last p)  = pretty p
+    pretty (And p a) = pretty p <+> "&&" <+> pretty a
+    pretty (Or p a)  = pretty p <+> "||" <+> pretty a
+
 -- | A (possibly timed or inverted) pipeline, linked with @|@ or @|&@.
 data Pipeline = Pipeline
     { -- | 'True' if the pipeline is timed with @time@.
@@ -164,22 +280,38 @@
     , commands   :: [Command]
     } deriving (Eq, Read, Show)
 
+instance Pretty Pipeline where
+    pretty Pipeline{..} =
+        (if timed      then "time" else empty) <+>
+        (if timedPosix then "-p"   else empty) <+>
+        (if inverted   then "!"    else empty) <+>
+        pretty commands
+
 -- | An assignment.
 data Assign = Assign LValue AssignOp RValue
     deriving (Eq, Read, Show)
 
+instance Pretty Assign where
+    pretty (Assign lhs op rhs) = pretty lhs <> pretty op <> pretty rhs
+
 -- | The left side of an assignment.
 data LValue = LValue String (Maybe Subscript)
     deriving (Eq, Read, Show)
 
+instance Pretty LValue where
+    pretty (LValue name sub) =
+        text name <> pretty (fmap (\s -> "[" ++ s ++ "]") sub)
+
 -- | An assignment operator.
 data AssignOp
-    -- | The @=@ operator.
-    = Equals
-    -- | The @+=@ operator.
-    | PlusEquals
+    = Equals      -- ^ @=@
+    | PlusEquals  -- ^ @+=@
     deriving (Eq, Ord, Read, Show, Bounded, Enum)
 
+instance Pretty AssignOp where
+    pretty Equals     = "="
+    pretty PlusEquals = "+="
+
 -- | The right side of an assignment.
 data RValue
     -- | A simple word.
@@ -188,38 +320,8 @@
     | RArray [(Maybe Subscript, Word)]
     deriving (Eq, Read, Show)
 
--- | Shell reserved words.
-reservedWords :: [Word]
-reservedWords =
-    [ "!", "[[", "]]", "{", "}"
-    , "if", "then", "else", "elif", "fi"
-    , "case", "esac", "for", "select", "while", "until"
-    , "in", "do", "done", "time", "function"
-    ]
-
--- | Shell assignment builtins. These builtins can take assignments as
--- arguments.
-assignBuiltins :: [Word]
-assignBuiltins =
-    [ "alias", "declare", "export", "eval"
-    , "let", "local", "readonly", "typeset"
-    ]
-
--- | Redirection operators, not including here document operators.
-redirOps :: [String]
-redirOps = [">", "<", ">>", ">|", "<>", "<<<", "<&", ">&", "&>", "&>>"]
-
--- | Here document operators.
-heredocOps :: [String]
-heredocOps = ["<<", "<<-"]
-
--- | Shell control operators.
-controlOps :: [String]
-controlOps =
-    [ "(", ")", ";;", ";&", ";;&"
-    , "|", "|&", "||", "&&", ";", "&", "\n"
-    ]
-
--- | All normal operators.
-normalOps :: [String]
-normalOps = redirOps ++ heredocOps ++ controlOps
+instance Pretty RValue where
+    pretty (RValue w)  = text w
+    pretty (RArray rs) = "(" <> hsep (map f rs) <> ")"
+      where
+        f (sub, w) = pretty (fmap (\s -> "[" ++ s ++ "]=") sub) <> text w
