diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,88 @@
+# ShellCheck - A shell script static analysis tool
+
+http://www.shellcheck.net
+
+Copyright 2012-2014, Vidar 'koala_man' Holen
+Licensed under the GNU Affero General Public License, v3
+
+The goals of ShellCheck are:
+
+  - To point out and clarify typical beginner's syntax issues,
+    that causes a shell to give cryptic error messages.
+
+  - To point out and clarify typical intermediate level semantic problems,
+    that causes a shell to behave strangely and counter-intuitively.
+
+  - To point out subtle caveats, corner cases and pitfalls, that may cause an
+    advanced user's otherwise working script to fail under future circumstances.
+
+ShellCheck is written in Haskell, and requires at least 1 GB of RAM to compile.
+
+## Installing
+
+On systems with Cabal:
+
+    cabal update
+    cabal install shellcheck
+
+On Arch Linux with community packages enabled:
+
+    pacman -S shellcheck
+
+On OS X with homebrew:
+
+    brew install shellcheck
+
+ShellCheck is also available as an online service:
+
+    http://www.shellcheck.net
+
+## Building with Cabal
+
+This sections describes how to build ShellCheck from a source directory.
+
+First, make sure cabal is installed. On Debian based distros:
+
+    apt-get install cabal-install
+
+On Fedora:
+
+    yum install cabal-install
+
+On Mac OS X with homebrew (http://brew.sh/):
+
+    brew install cabal-install
+
+On Mac OS X with MacPorts (http://www.macports.org/):
+
+    port install hs-cabal-install
+
+Let cabal update itself, in case your distro version is outdated:
+
+    $ cabal update
+    $ cabal install cabal-install
+
+With cabal installed, cd to the ShellCheck source directory and:
+
+    $ cabal install
+
+This will install ShellCheck to your ~/.cabal/bin directory.
+
+Add the directory to your PATH (for bash, add this to your ~/.bashrc file):
+
+    export PATH=$HOME/.cabal/bin:$PATH
+
+Verify that your PATH is set up correctly:
+
+    $ which shellcheck
+    ~/.cabal/bin/shellcheck
+
+## Running tests
+
+To run the unit test suite:
+
+    cabal configure --enable-tests
+    cabal build
+    cabal test
+
+Happy ShellChecking!
diff --git a/ShellCheck.cabal b/ShellCheck.cabal
--- a/ShellCheck.cabal
+++ b/ShellCheck.cabal
@@ -1,6 +1,5 @@
 Name:             ShellCheck
--- Must also be updated in ShellCheck/Data.hs :
-Version:          0.3.3
+Version:          0.3.4
 Synopsis:         Shell script analysis tool
 License:          OtherLicense
 License-file:     LICENSE
@@ -23,6 +22,13 @@
   * To point out subtle caveats, corner cases and pitfalls, that may cause an
     advanced user's otherwise working script to fail under future circumstances.
 
+Extra-Source-Files:
+    -- documentation
+    README.md
+    shellcheck.1.md
+    -- tests
+    test/shellcheck.hs
+
 source-repository head
     type: git
     location: git://github.com/koalaman/shellcheck.git
@@ -35,13 +41,16 @@
       json,
       mtl,
       parsec,
-      regex-compat
+      regex-compat,
+      QuickCheck >= 2.2
     exposed-modules:
       ShellCheck.Analytics
       ShellCheck.AST
       ShellCheck.Data
       ShellCheck.Parser
       ShellCheck.Simple
+    other-modules:
+      Paths_ShellCheck
 
 executable shellcheck
     build-depends:
@@ -52,5 +61,21 @@
       json,
       mtl,
       parsec,
-      regex-compat
+      regex-compat,
+      QuickCheck >= 2.2
     main-is: shellcheck.hs
+
+test-suite test-shellcheck
+    type: exitcode-stdio-1.0
+    build-depends:
+      ShellCheck,
+      base >= 4 && < 5,
+      containers,
+      directory,
+      json,
+      mtl,
+      parsec,
+      regex-compat,
+      QuickCheck >= 2.2
+    main-is: test/shellcheck.hs
+
diff --git a/ShellCheck/AST.hs b/ShellCheck/AST.hs
--- a/ShellCheck/AST.hs
+++ b/ShellCheck/AST.hs
@@ -29,16 +29,14 @@
 data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
 data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
 data ForInType = NormalForIn | ShortForIn deriving (Show, Eq)
+data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
 
 data Token =
-    TA_Base Id String Token
-    | TA_Binary Id String Token Token
-    | TA_Expansion Id Token
-    | TA_Literal Id String
+    TA_Binary Id String Token Token
+    | TA_Expansion Id [Token]
     | TA_Sequence Id [Token]
     | TA_Trinary Id Token Token Token
     | TA_Unary Id String Token
-    | TA_Variable Id String
     | TC_And Id ConditionType String Token Token
     | TC_Binary Id ConditionType String Token Token
     | TC_Group Id ConditionType Token
@@ -49,6 +47,8 @@
     | T_AndIf Id (Token) (Token)
     | T_Arithmetic Id Token
     | T_Array Id [Token]
+    | T_IndexedElement Id Token Token
+    | T_ Id [Token]
     | T_Assignment Id AssignmentMode String (Maybe Token) Token
     | T_Backgrounded Id Token
     | T_Backticked Id [Token]
@@ -58,7 +58,7 @@
     | T_BraceGroup Id [Token]
     | T_CLOBBER Id
     | T_Case Id
-    | T_CaseExpression Id Token [([Token],[Token])]
+    | T_CaseExpression Id Token [(CaseType, [Token], [Token])]
     | T_Condition Id ConditionType Token
     | T_DGREAT Id
     | T_DLESS Id
@@ -179,6 +179,7 @@
         b <- round value
         return $ T_Assignment id mode var a b
     delve (T_Array id t) = dl t $ T_Array id
+    delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id
     delve (T_Redirecting id redirs cmd) = do
         a <- roundAll redirs
         b <- round cmd
@@ -207,10 +208,10 @@
     delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
     delve (T_CaseExpression id word cases) = do
         newWord <- round word
-        newCases <- mapM (\(c, t) -> do
+        newCases <- mapM (\(o, c, t) -> do
                             x <- mapM round c
                             y <- mapM round t
-                            return (x,y)
+                            return (o, x,y)
                         ) cases
         return $ T_CaseExpression id newWord newCases
 
@@ -243,8 +244,7 @@
         b <- round t2
         c <- round t3
         return $ TA_Trinary id a b c
-    delve (TA_Expansion id t) = d1 t $ TA_Expansion id
-    delve (TA_Base id b t) = d1 t $ TA_Base id b
+    delve (TA_Expansion id t) = dl t $ TA_Expansion id
     delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
     delve t = return t
 
@@ -297,6 +297,7 @@
         T_FdRedirect id _ _  -> id
         T_Assignment id _ _ _ _  -> id
         T_Array id _  -> id
+        T_IndexedElement id _ _  -> id
         T_Redirecting id _ _  -> id
         T_SimpleCommand id _ _  -> id
         T_Pipeline id _ _  -> id
@@ -327,11 +328,8 @@
         TA_Binary id _ _ _  -> id
         TA_Unary id _ _  -> id
         TA_Sequence id _  -> id
-        TA_Variable id _  -> id
         TA_Trinary id _ _ _  -> id
         TA_Expansion id _  -> id
-        TA_Literal id _ -> id
-        TA_Base id _ _ -> id
         T_ProcSub id _ _ -> id
         T_Glob id _ -> id
         T_ForArithmetic id _ _ _ _ -> id
diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs
--- a/ShellCheck/Analytics.hs
+++ b/ShellCheck/Analytics.hs
@@ -15,21 +15,25 @@
     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -}
-module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics, shellForExecutable) where
+{-# LANGUAGE TemplateHaskell #-}
+module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics, shellForExecutable, runTests) where
 
+import Control.Arrow (first)
 import Control.Monad
 import Control.Monad.State
 import Control.Monad.Writer
 import Data.Char
 import Data.Functor
+import Data.Function (on)
 import Data.List
 import Data.Maybe
 import Debug.Trace
 import ShellCheck.AST
 import ShellCheck.Data
-import ShellCheck.Parser
+import ShellCheck.Parser hiding (runTests)
 import Text.Regex
 import qualified Data.Map as Map
+import Test.QuickCheck.All (quickCheckAll)
 
 data Shell = Ksh | Zsh | Sh | Bash
     deriving (Show, Eq)
@@ -46,8 +50,8 @@
 treeChecks :: [Parameters -> Token -> [Note]]
 treeChecks = [
     runNodeAnalysis
-        (\p t -> mapM_ (\f -> f t) $
-            map (\f -> f p) (nodeChecks ++ checksFor (shellType p)))
+        (\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p))
+            (nodeChecks ++ checksFor (shellType p))))
     ,subshellAssignmentCheck
     ,checkSpacefulness
     ,checkQuotesInLiterals
@@ -201,6 +205,8 @@
     ,checkAliasesUsesArgs
     ,checkShouldUseGrepQ
     ,checkTestGlobs
+    ,checkConcatenatedDollarAt
+    ,checkFindActionPrecedence
     ]
 
 
@@ -242,7 +248,7 @@
   where
     f str = do
         (_, match, rest, _) <- matchRegexAll re str
-        return $ (match, rest)
+        return (match, rest)
 
 willSplit x =
   case x of
@@ -267,7 +273,7 @@
 isConfusedGlobRegex _ = False
 
 getSuspiciousRegexWildcard str =
-    if (not $ str `matches` contra)
+    if not $ str `matches` contra
     then do
         match <- matchRegex suspicious str
         str <- match !!! 0
@@ -306,7 +312,7 @@
 simplify = doTransform makeSimple
 
 deadSimple (T_NormalWord _ l) = [concat (concatMap deadSimple l)]
-deadSimple (T_DoubleQuoted _ l) = [(concat (concatMap deadSimple l))]
+deadSimple (T_DoubleQuoted _ l) = [concat (concatMap deadSimple l)]
 deadSimple (T_SingleQuoted _ s) = [s]
 deadSimple (T_DollarBraced _ _) = ["${VAR}"]
 deadSimple (T_DollarArithmetic _ _) = ["${VAR}"]
@@ -423,7 +429,7 @@
 
 prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1"
 prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2"
-checkWrongArithmeticAssignment params (T_SimpleCommand id ((T_Assignment _ _ _ _ val):[]) []) =
+checkWrongArithmeticAssignment params (T_SimpleCommand id (T_Assignment _ _ _ _ val:[]) []) =
   fromMaybe (return ()) $ do
     str <- getNormalString val
     match <- matchRegex regex str
@@ -454,7 +460,7 @@
 prop_checkUuoc2 = verifyNot checkUuoc "cat * | grep bar"
 prop_checkUuoc3 = verify checkUuoc "cat $var | grep bar"
 prop_checkUuoc4 = verifyNot checkUuoc "cat $var"
-checkUuoc _ (T_Pipeline _ _ ((T_Redirecting _ _ cmd):_:_)) =
+checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) =
     checkCommand "cat" (const f) cmd
   where
     f [word] = when (isSimple word) $
@@ -470,7 +476,7 @@
 prop_checkNeedlessCommands3 = verifyNot checkNeedlessCommands "foo=$(expr foo : regex)"
 prop_checkNeedlessCommands4 = verifyNot checkNeedlessCommands "foo=$(expr foo \\< regex)"
 checkNeedlessCommands _ cmd@(T_SimpleCommand id _ args) |
-        cmd `isCommand` "expr" && (not $ any (`elem` words) exceptions) =
+        cmd `isCommand` "expr" && not (any (`elem` words) exceptions) =
     style id 2003 "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
   where
     -- These operators are hard to replicate in POSIX
@@ -482,11 +488,19 @@
 prop_checkPipePitfalls4 = verifyNot checkPipePitfalls "find . -print0 | xargs -0 foo"
 prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo"
 prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo"
+prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo"
 checkPipePitfalls _ (T_Pipeline id _ commands) = do
     for ["find", "xargs"] $
-        \(find:xargs:_) -> let args = deadSimple xargs in
-            unless (hasShortParameter args '0') $
-                warn (getId find) 2038 "Use either 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow for non-alphanumeric filenames."
+        \(find:xargs:_) ->
+          let args = deadSimple xargs ++ deadSimple find
+          in
+            unless (or $ map ($ args) [
+                hasShortParameter '0',
+                hasParameter "null",
+                hasParameter "print0",
+                hasParameter "printf"
+              ]) $ warn (getId find) 2038
+                      "Use -print0/-0 or -exec + to allow for non-alphanumeric filenames."
 
     for ["?", "echo"] $
         \(_:echo:_) -> info (getId echo) 2008 "echo doesn't read from stdin, are you sure you should be piping to it?"
@@ -505,22 +519,24 @@
         ]
     unless didLs $ do
         for ["ls", "?"] $
-            \(ls:_) -> unless (hasShortParameter (deadSimple ls) 'N') $
+            \(ls:_) -> unless (hasShortParameter 'N' (deadSimple ls)) $
                 info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames."
         return ()
   where
     for l f =
         let indices = indexOfSublists l (map (headOrDefault "" . deadSimple) commands)
         in do
-            mapM_ f (map (\n -> take (length l) $ drop n $ commands) indices)
+            mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices
             return . not . null $ indices
     for' l f = for l (first f)
     first func (x:_) = func (getId x)
     first _ _ = return ()
-    hasShortParameter list char = any (\x -> "-" `isPrefixOf` x && char `elem` x) list
+    hasShortParameter char list = any (\x -> "-" `isPrefixOf` x && char `elem` x) list
+    hasParameter string list =
+        any (isPrefixOf string . dropWhile (== '-')) list
 checkPipePitfalls _ _ = return ()
 
-indexOfSublists sub all = f 0 all
+indexOfSublists sub = f 0
   where
     f _ [] = []
     f n a@(r:rest) =
@@ -570,9 +586,7 @@
 prop_checkShebang1 = verifyTree checkShebang "#!/usr/bin/env bash -x\necho cow"
 prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh  -l "
 checkShebang _ (T_Script id sb _) =
-    if (length $ words sb) > 2 then
-        [Note id ErrorC 2096 $ "On most OS, shebangs can only specify a single parameter."]
-    else []
+    [Note id ErrorC 2096 "On most OS, shebangs can only specify a single parameter." | length (words sb) > 2]
 
 prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
 prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
@@ -612,11 +626,15 @@
             warnMsg id $ op ++ " is"
     bashism (TA_Unary id op _)
         | op `elem` [ "|++", "|--", "++|", "--|"] =
-            warnMsg id $ (filter (/= '|') op) ++ " is"
+            warnMsg id $ filter (/= '|') op ++ " is"
     bashism t@(T_SimpleCommand id _ _)
         | t `isCommand` "source" =
             warnMsg id "'source' in place of '.' is"
     bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
+    bashism t@(TA_Expansion id _) | getLiteralString t == Just "RANDOM" =
+        warnMsg id "RANDOM is"
+    bashism t@(T_DollarBraced id _) | getBracedReference (bracedString t) == "RANDOM" =
+        warnMsg id "$RANDOM is"
     bashism (T_DollarBraced id token) =
         mapM_ check expansion
       where
@@ -628,20 +646,18 @@
         | t `isCommand` "echo" && "-" `isPrefixOf` argString =
             unless ("--" `isPrefixOf` argString) $ -- echo "-------"
                 warnMsg (getId arg) "echo flags are"
-      where argString = (concat $ deadSimple arg)
+      where argString = concat $ deadSimple arg
     bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
-        | t `isCommand` "exec" && "-" `isPrefixOf` (concat $ deadSimple arg) =
+        | t `isCommand` "exec" && "-" `isPrefixOf` concat (deadSimple arg) =
             warnMsg (getId arg) "exec flags are"
     bashism t@(T_SimpleCommand id _ _)
         | t `isCommand` "let" = warnMsg id "'let' is"
-    bashism t@(TA_Variable id "RANDOM") =
-        warnMsg id "RANDOM is"
     bashism t@(T_Pipe id "|&") =
         warnMsg id "|& in place of 2>&1 | is"
     bashism (T_Array id _) =
         warnMsg id "arrays are"
 
-    bashism _ = return()
+    bashism _ = return ()
 
     varChars="_0-9a-zA-Z"
     expansion = let re = mkRegex in [
@@ -650,7 +666,7 @@
         (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
         (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
         (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is"),
-        (re $ "^RANDOM$", "$RANDOM is")
+        (re "^RANDOM$", "$RANDOM is")
         ]
 
 prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
@@ -665,14 +681,14 @@
 checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =
     when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list
             || (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $
-        err id 2066 $ "Since you double quoted this, it will not word split, and the loop will only run once."
+        err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once."
 checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [T_SingleQuoted id s]] _) =
     warn id 2041 $ "This is a literal string. To run as a command, use $(" ++ s ++ ")."
 checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [T_Literal id s]] _) =
     if ',' `elem` s
       then unless ('{' `elem` s) $
-            warn id 2042 $ "Use spaces, not commas, to separate loop elements."
-      else warn id 2043 $ "This loop will only run once, with " ++ (head f) ++ "='" ++ s ++ "'."
+            warn id 2042 "Use spaces, not commas, to separate loop elements."
+      else warn id 2043 $ "This loop will only run once, with " ++ head f ++ "='" ++ s ++ "'."
 checkForInQuoted _ _ = return ()
 
 prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"
@@ -694,7 +710,7 @@
 prop_checkForInLs = verify checkForInLs "for f in $(ls *.mp3); do mplayer \"$f\"; done"
 prop_checkForInLs2 = verify checkForInLs "for f in `ls *.mp3`; do mplayer \"$f\"; done"
 prop_checkForInLs3 = verify checkForInLs "for f in `find / -name '*.mp3'`; do mplayer \"$f\"; done"
-checkForInLs _ t = try t
+checkForInLs _ = try
   where
    try (T_ForIn _ _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) =
         check id f x
@@ -718,14 +734,14 @@
 prop_checkFindExec6 = verify checkFindExec "find / -type d -execdir rm *.jpg \\;"
 checkFindExec _ cmd@(T_SimpleCommand _ _ t@(h:r)) | cmd `isCommand` "find" = do
     c <- broken r False
-    when c $ do
+    when c $
         let wordId = getId $ last t in
             err wordId 2067 "Missing ';' or + terminating -exec. You can't use |/||/&&, and ';' has to be a separate, quoted argument."
 
   where
     broken [] v = return v
     broken (w:r) v = do
-        when v $ (mapM_ warnFor $ fromWord w)
+        when v (mapM_ warnFor $ fromWord w)
         case getLiteralString w of
             Just "-exec" -> broken r True
             Just "-execdir" -> broken r True
@@ -738,7 +754,7 @@
         T_DollarExpansion _ _ -> True
         T_Backticked _ _ -> True
         T_Glob _ _ -> True
-        T_Extglob _ _ _ -> True
+        T_Extglob {} -> True
         _ -> False
 
     warnFor x =
@@ -759,8 +775,8 @@
 prop_checkUnquotedExpansions5 = verifyNot checkUnquotedExpansions "for f in $(cmd); do echo $f; done"
 prop_checkUnquotedExpansions6 = verifyNot checkUnquotedExpansions "$(cmd)"
 prop_checkUnquotedExpansions7 = verifyNot checkUnquotedExpansions "cat << foo\n$(ls)\nfoo"
-checkUnquotedExpansions params t =
-    check t
+checkUnquotedExpansions params =
+    check
   where
     check t@(T_DollarExpansion _ _) = examine t
     check t@(T_Backticked _ _) = examine t
@@ -779,7 +795,7 @@
 checkRedirectToSame params s@(T_Pipeline _ _ list) =
     mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurences x) l) (getAllRedirs list))) list
   where
-    note x = Note x InfoC 2094 $
+    note x = Note x InfoC 2094
                 "Make sure not to read and write the same file in the same pipeline."
     checkOccurences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) =
         when (exceptId /= newId
@@ -789,17 +805,17 @@
             addNote $ note newId
             addNote $ note exceptId
     checkOccurences _ _ = return ()
-    getAllRedirs l = concatMap (\(T_Redirecting _ ls _) -> concatMap getRedirs ls) l
+    getAllRedirs = concatMap (\(T_Redirecting _ ls _) -> concatMap getRedirs ls)
     getRedirs (T_FdRedirect _ _ (T_IoFile _ op file)) =
             case op of T_Greater _ -> [file]
                        T_Less _    -> [file]
                        T_DGREAT _  -> [file]
                        _ -> []
     getRedirs _ = []
-    special x = "/dev/" `isPrefixOf` (concat $ deadSimple x)
+    special x = "/dev/" `isPrefixOf` concat (deadSimple x)
     isOutput t =
         case drop 1 $ getPath (parentMap params) t of
-            (T_IoFile _ op _):_ ->
+            T_IoFile _ op _:_ ->
                 case op of
                     T_Greater _  -> True
                     T_DGREAT _ -> True
@@ -816,7 +832,7 @@
         | not $ isOk t =
     info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true."
   where
-    isOk [t] = isAssignment t || (fromMaybe False $ do
+    isOk [t] = isAssignment t || fromMaybe False (do
         name <- getCommandBasename t
         return $ name `elem` ["echo", "exit", "return"])
     isOk _ = False
@@ -825,10 +841,10 @@
 
 prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done"
 prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"
-checkDollarStar p t@(T_NormalWord _ [(T_DollarBraced id l)])
-      | (bracedString l) == "*"  =
+checkDollarStar p t@(T_NormalWord _ [T_DollarBraced id l])
+      | bracedString l == "*"  =
     unless isAssigned $
-        warn id 2048 $ "Use \"$@\" (with quotes) to prevent whitespace problems."
+        warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."
   where
     path = getPath (parentMap p) t
     isAssigned = any isAssignment . take 2 $ path
@@ -843,14 +859,28 @@
 prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }"
 prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@"
 checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not isAssigned =
-    flip mapM_ (take 1 $ filter isArrayExpansion parts) $ \x -> do
-        err (getId x) 2068 $
+    forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->
+        err (getId x) 2068
             "Double quote array expansions, otherwise they're like $* and break on spaces."
   where
     path = getPath (parentMap p) word
     isAssigned = any isAssignment . take 2 $ path
 checkUnquotedDollarAt _ _ = return ()
 
+prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\""
+prop_checkConcatenatedDollarAt2 = verify checkConcatenatedDollarAt "echo ${arr[@]}lol"
+prop_checkConcatenatedDollarAt3 = verify checkConcatenatedDollarAt "echo $a$@"
+prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@"
+prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\""
+checkConcatenatedDollarAt p word@(T_NormalWord {})
+    | not $ isQuoteFree (parentMap p) word =
+        unless (null $ drop 1 parts) $
+            mapM_ for array
+  where
+    parts = getWordParts word
+    array = take 1 $ filter isArrayExpansion parts
+    for t = err (getId t) 2145 "Argument mixes string and array. Use * or separate argument."
+checkConcatenatedDollarAt _ _ = return ()
 
 prop_checkArrayAsString1 = verify checkArrayAsString "a=$@"
 prop_checkArrayAsString2 = verify checkArrayAsString "a=\"${arr[@]}\""
@@ -880,8 +910,8 @@
         return . maybeToList $ do
             name <- getLiteralString token
             assignment <- Map.lookup name map
-            return [(Note id WarningC 2128
-                "Expanding an array without an index only gives the first element.")]
+            return [Note id WarningC 2128
+                    "Expanding an array without an index only gives the first element."]
     readF _ _ _ = return []
 
     writeF _ t name (DataFrom [T_Array {}]) = do
@@ -900,11 +930,11 @@
             T_Greater _ -> error
             T_DGREAT _ -> error
             _ -> return ()
-         where error = err id 2069 $ "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last."
+         where error = err id 2069 "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last."
 checkStderrRedirect _ _ = return ()
 
-lt x = trace ("FAILURE " ++ (show x)) x
-ltt t x = trace ("FAILURE " ++ (show t)) x
+lt x = trace ("FAILURE " ++ show x) x
+ltt t = trace ("FAILURE " ++ show t)
 
 
 prop_checkSingleQuotedVariables  = verify checkSingleQuotedVariables "echo '$foo'"
@@ -925,15 +955,14 @@
         else unless isProbablyOk showMessage
   where
     parents = parentMap params
-    showMessage = info id 2016 $
+    showMessage = info id 2016
         "Expressions don't expand in single quotes, use double quotes for that."
     commandName = fromMaybe "" $ do
         cmd <- getClosestCommand parents t
-        name <- getCommandBasename cmd
-        return name
+        getCommandBasename cmd
 
     isProbablyOk =
-            (any isOkAssignment $ take 3 $ getPath parents t)
+            any isOkAssignment (take 3 $ getPath parents t)
             || commandName `elem` [
                 "trap"
                 ,"sh"
@@ -978,22 +1007,22 @@
 prop_checkNumberComparisons11= verify checkNumberComparisons "[[ $foo -eq 'N' ]]"
 prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]"
 checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
-    if (isNum lhs && (not $ isNonNum rhs)
-            || isNum rhs && (not $ isNonNum lhs))
+    if isNum lhs && not (isNonNum rhs)
+       || isNum rhs && not (isNonNum lhs)
       then do
         when (isLtGt op) $
           err id 2071 $
-            op ++ " is for string comparisons. Use " ++ (eqv op) ++ " instead."
+            op ++ " is for string comparisons. Use " ++ eqv op ++ " instead."
         when (isLeGe op) $
             err id 2071 $ op ++ " is not a valid operator. " ++
-              "Use " ++ (eqv op) ++ " ."
+              "Use " ++ eqv op ++ " ."
       else do
         when (isLeGe op || isLtGt op) $
             mapM_ checkDecimals [lhs, rhs]
 
         when (isLeGe op) $
             err id 2122 $ op ++ " is not a valid operator. " ++
-                "Use '! a " ++ (invert op) ++ " b' instead."
+                "Use '! a " ++ invert op ++ " b' instead."
 
     when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do
         mapM_ checkDecimals [lhs, rhs]
@@ -1021,7 +1050,7 @@
       numChar x = isDigit x || x `elem` "+-. "
 
       stringError t = err (getId t) 2130 $
-        op ++ " is for integer comparisons. Use " ++ (seqv op) ++ " instead."
+        op ++ " is for integer comparisons. Use " ++ seqv op ++ " instead."
 
       isNum t =
         case deadSimple t of
@@ -1096,7 +1125,7 @@
         T_NormalWord id [T_SingleQuoted _ _] -> error id
         _ -> return ()
   where
-    error id = err id 2076 $ "Don't quote rhs of =~, it'll match literally rather than as a regex."
+    error id = err id 2076 "Don't quote rhs of =~, it'll match literally rather than as a regex."
 checkQuotedCondRegex _ _ = return ()
 
 prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
@@ -1106,9 +1135,8 @@
 prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
 checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
     let s = concat $ deadSimple rhs in
-        if isConfusedGlobRegex s
-            then warn (getId rhs) 2049 $ "=~ is for regex. Use == for globs."
-            else return ()
+        when (isConfusedGlobRegex s) $
+            warn (getId rhs) 2049 "=~ is for regex. Use == for globs."
 checkGlobbedRegex _ _ = return ()
 
 
@@ -1118,8 +1146,8 @@
 prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"
 prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"
 checkConstantIfs _ (TC_Binary id typ op lhs rhs)
-    | op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] = do
-        when (isJust lLit && isJust rLit) $ warn id 2050 $ "This expression is constant. Did you forget the $ on a variable?"
+    | op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] =
+        when (isJust lLit && isJust rLit) $ warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
     where
         lLit = getLiteralString lhs
         rLit = getLiteralString rhs
@@ -1130,32 +1158,35 @@
 prop_checkNoaryWasBinary3 = verify checkNoaryWasBinary "[ $foo!=3 ]"
 checkNoaryWasBinary _ (TC_Noary _ _ t@(T_NormalWord id l)) | not $ isConstant t = do
     let str = concat $ deadSimple t
-    when ('=' `elem` str) $ err id 2077 $ "You need spaces around the comparison operator."
+    when ('=' `elem` str) $ err id 2077 "You need spaces around the comparison operator."
 checkNoaryWasBinary _ _ = return ()
 
 prop_checkConstantNoary = verify checkConstantNoary "[[ '$(foo)' ]]"
 prop_checkConstantNoary2 = verify checkConstantNoary "[ \"-f lol\" ]"
 prop_checkConstantNoary3 = verify checkConstantNoary "[[ cmd ]]"
 prop_checkConstantNoary4 = verify checkConstantNoary "[[ ! cmd ]]"
-checkConstantNoary _ (TC_Noary _ _ t@(T_NormalWord id _)) | isConstant t = do
-    err id 2078 $ "This expression is constant. Did you forget a $ somewhere?"
+checkConstantNoary _ (TC_Noary _ _ t@(T_NormalWord id _)) | isConstant t =
+    err id 2078 "This expression is constant. Did you forget a $ somewhere?"
 checkConstantNoary _ _ = return ()
 
 prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
 prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
 checkBraceExpansionVars _ (T_BraceExpansion id s) | "..$" `isInfixOf` s =
-    warn id 2051 $ "Bash doesn't support variables in brace range expansions."
+    warn id 2051 "Bash doesn't support variables in brace range expansions."
 checkBraceExpansionVars _ _ = return ()
 
 prop_checkForDecimals = verify checkForDecimals "((3.14*c))"
-checkForDecimals _ (TA_Literal id s) | any (== '.') s = do
-    err id 2079 $ "(( )) doesn't support decimals. Use bc or awk."
+checkForDecimals _ t@(TA_Expansion id _) = potentially $ do
+    str <- getLiteralString t
+    first <- str !!! 0
+    guard $ isDigit first && '.' `elem` str
+    return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
 checkForDecimals _ _ = return ()
 
 prop_checkDivBeforeMult = verify checkDivBeforeMult "echo $((c/n*100))"
 prop_checkDivBeforeMult2 = verifyNot checkDivBeforeMult "echo $((c*100/n))"
-checkDivBeforeMult _ (TA_Binary _ "*" (TA_Binary id "/" _ _) _) = do
-    info id 2017 $ "Increase precision by replacing a/b*c with a*c/b."
+checkDivBeforeMult _ (TA_Binary _ "*" (TA_Binary id "/" _ _) _) =
+    info id 2017 "Increase precision by replacing a/b*c with a*c/b."
 checkDivBeforeMult _ _ = return ()
 
 prop_checkArithmeticDeref = verify checkArithmeticDeref "echo $((3+$foo))"
@@ -1163,24 +1194,33 @@
 prop_checkArithmeticDeref3 = verifyNot checkArithmeticDeref "cow=1/40; (( s+= ${cow%%/*} ))"
 prop_checkArithmeticDeref4 = verifyNot checkArithmeticDeref "(( ! $? ))"
 prop_checkArithmeticDeref5 = verifyNot checkArithmeticDeref "(($1))"
-prop_checkArithmeticDeref6 = verifyNot checkArithmeticDeref "(( ${a[$i]} ))"
+prop_checkArithmeticDeref6 = verify checkArithmeticDeref "(( a[$i] ))"
 prop_checkArithmeticDeref7 = verifyNot checkArithmeticDeref "(( 10#$n ))"
-checkArithmeticDeref params t@(TA_Expansion _ (T_DollarBraced id l)) =
-    when (not $ (excepting $ bracedString l) || inBaseExpression) $
-        style id 2004 $ "$ on variables in (( )) is unnecessary."
+prop_checkArithmeticDeref8 = verifyNot checkArithmeticDeref "let i=$i+1"
+checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id l]) =
+    unless ((isException $ bracedString l) || (not isNormal)) $
+        style id 2004 "$ on variables in (( )) is unnecessary."
   where
-    inBaseExpression = any isBase $ parents params t
-    isBase (TA_Base {}) = True
-    isBase _ = False
-    excepting [] = True
-    excepting s = (any (`elem` "/.:#%?*@[]") s) || (isDigit $ head s)
+    isException [] = True
+    isException s = any (`elem` "/.:#%?*@") s || isDigit (head s)
+    isNormal = fromMaybe True $ msum $ map isNormalContext $ (parents params t)
+    isNormalContext t =
+        case t of
+            T_Arithmetic {} -> return True
+            T_DollarArithmetic {} -> return True
+            T_SimpleCommand {} -> return False
+            _ -> fail "Irrelevant"
 checkArithmeticDeref _ _ = return ()
 
 prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))"
 prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))"
 prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))"
-checkArithmeticBadOctal _ (TA_Base id "0" (TA_Literal _ str)) | '9' `elem` str || '8' `elem` str =
-    err id 2080 $ "Numbers with leading 0 are considered octal."
+checkArithmeticBadOctal _ t@(TA_Expansion id _) = potentially $ do
+    str <- getLiteralString t
+    guard $ str `matches` octalRE
+    return $ err id 2080 "Numbers with leading 0 are considered octal."
+  where
+    octalRE = mkRegex "^0[0-7]*[8-9]"
 checkArithmeticBadOctal _ _ = return ()
 
 prop_checkComparisonAgainstGlob = verify checkComparisonAgainstGlob "[[ $cow == $bar ]]"
@@ -1188,25 +1228,28 @@
 prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
 prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
 checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _])) | op == "=" || op == "==" =
-    warn id 2053 $ "Quote the rhs of = in [[ ]] to prevent glob interpretation."
+    warn id 2053 "Quote the rhs of = in [[ ]] to prevent glob interpretation."
 checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
         | (op == "=" || op == "==") && isGlob word =
-    err (getId word) 2081 $ "[ .. ] can't match globs. Use [[ .. ]] or grep."
+    err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or grep."
 checkComparisonAgainstGlob _ _ = return ()
 
 prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)"
 prop_checkCommarrays2 = verify checkCommarrays "a+=(1,2,3)"
 prop_checkCommarrays3 = verifyNot checkCommarrays "cow=(1 \"foo,bar\" 3)"
 prop_checkCommarrays4 = verifyNot checkCommarrays "cow=('one,' 'two')"
+prop_checkCommarrays5 = verify checkCommarrays "a=([a]=b, [c]=d)"
+prop_checkCommarrays6 = verify checkCommarrays "a=([a]=b,[c]=d,[e]=f)"
 checkCommarrays _ (T_Array id l) =
     when (any (isCommaSeparated . literal) l) $
         warn id 2054 "Use spaces, not commas, to separate array elements."
   where
+    literal (T_IndexedElement _ _ l) = literal l
     literal (T_NormalWord _ l) = concatMap literal l
     literal (T_Literal _ str) = str
     literal _ = "str"
 
-    isCommaSeparated str = "," `isSuffixOf` str || (length $ filter (== ',') str) > 1
+    isCommaSeparated str = "," `isSuffixOf` str || length (filter (== ',') str) > 1
 checkCommarrays _ _ = return ()
 
 prop_checkOrNeq1 = verify checkOrNeq "if [[ $lol -ne cow || $lol -ne foo ]]; then echo foo; fi"
@@ -1229,10 +1272,10 @@
 prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]"
 prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]"
 checkValidCondOps _ (TC_Binary id _ s _ _)
-    | not (s `elem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="]) =
+    | s `notElem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="] =
         warn id 2057 "Unknown binary operator."
 checkValidCondOps _ (TC_Unary id _ s _)
-    | not (s `elem`  [ "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-o", "-v", "-R"]) =
+    | s `notElem`  [ "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-o", "-v", "-R"] =
         warn id 2058 "Unknown unary operator."
 checkValidCondOps _ _ = return ()
 
@@ -1241,14 +1284,14 @@
 getParentTree t =
     snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
   where
-    pre t = modify (\(l, m) -> (t:l, m))
+    pre t = modify (first ((:) t))
     post t = do
-        ((_:rest), map) <- get
+        (_:rest, map) <- get
         case rest of [] -> put (rest, map)
                      (x:_) -> put (rest, Map.insert (getId t) x map)
 
 getTokenMap t =
-    snd $ runState (doAnalysis f t) (Map.empty)
+    execState (doAnalysis f t) Map.empty
   where
     f t = modify (Map.insert (getId t) t)
 
@@ -1256,7 +1299,7 @@
 -- Is this node self quoting?
 isQuoteFree tree t =
     (isQuoteFreeElement t == Just True) ||
-        (head $ (mapMaybe isQuoteFreeContext $ drop 1 $ getPath tree t) ++ [False])
+        head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
   where
     -- Is this node self-quoting in itself?
     isQuoteFreeElement t =
@@ -1270,24 +1313,22 @@
             TC_Noary _ DoubleBracket _ -> return True
             TC_Unary _ DoubleBracket _ _ -> return True
             TC_Binary _ DoubleBracket _ _ _ -> return True
-            TA_Unary _ _ _ -> return True
-            TA_Binary _ _ _ _ -> return True
-            TA_Trinary _ _ _ _ -> return True
-            TA_Expansion _ _ -> return True
+            TA_Sequence {} -> return True
+            T_Arithmetic {} -> return True
             T_Assignment {} -> return True
-            T_Redirecting _ _ _ -> return $
+            T_Redirecting {} -> return $
                 any (isCommand t) ["local", "declare", "typeset", "export"]
             T_DoubleQuoted _ _ -> return True
-            T_CaseExpression _ _ _ -> return True
-            T_HereDoc _ _ _ _ _ -> return True
+            T_CaseExpression {} -> return True
+            T_HereDoc {} -> return True
             T_DollarBraced {} -> return True
             -- Pragmatically assume it's desirable to split here
             T_ForIn {} -> return True
             T_SelectIn {} -> return True
             _ -> Nothing
 
-isParamTo tree cmd t =
-    go t
+isParamTo tree cmd =
+    go
   where
     go x = case Map.lookup (getId x) tree of
                 Nothing -> False
@@ -1297,24 +1338,24 @@
             T_SingleQuoted _ _ -> go t
             T_DoubleQuoted _ _ -> go t
             T_NormalWord _ _ -> go t
-            T_SimpleCommand _ _ _ -> isCommand t cmd
-            T_Redirecting _ _ _ -> isCommand t cmd
+            T_SimpleCommand {} -> isCommand t cmd
+            T_Redirecting {} -> isCommand t cmd
             _ -> False
 
 getClosestCommand tree t =
     msum . map getCommand $ getPath tree t
   where
-    getCommand t@(T_Redirecting _ _ _) = return t
+    getCommand t@(T_Redirecting {}) = return t
     getCommand _ = Nothing
 
 usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
   where
-    go currentId ((T_NormalWord id [word]):rest)
-        | currentId == (getId word) = go id rest
-    go currentId ((T_DoubleQuoted id [word]):rest)
-        | currentId == (getId word) = go id rest
-    go currentId ((T_SimpleCommand _ _ (word:_)):_)
-        | currentId == (getId word) = True
+    go currentId (T_NormalWord id [word]:rest)
+        | currentId == getId word = go id rest
+    go currentId (T_DoubleQuoted id [word]:rest)
+        | currentId == getId word = go id rest
+    go currentId (T_SimpleCommand _ _ (word:_):_)
+        | currentId == getId word = True
     go _ _ = False
 
 -- A list of the element and all its parents
@@ -1323,16 +1364,16 @@
         Nothing -> []
         Just parent -> getPath tree parent
 
-parents params t = getPath (parentMap params) t
+parents params = getPath (parentMap params)
 
 --- Command specific checks
 
 checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
-    if t `isCommand` str then f cmd rest else return ()
+    when (t `isCommand` str) $ f cmd rest
 checkCommand _ _ _ = return ()
 
 checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
-    if t `isUnqualifiedCommand` str then f cmd rest else return ()
+    when (t `isUnqualifiedCommand` str) $ f cmd rest
 checkUnqualifiedCommand _ _ _ = return ()
 
 getLiteralString = getLiteralStringExt (const Nothing)
@@ -1342,12 +1383,13 @@
     f (T_Glob _ str) = return str
     f _ = Nothing
 
-getLiteralStringExt more t = g t
+getLiteralStringExt more = g
   where
-    allInList l = let foo = map g l in if all isJust foo then return $ concat (catMaybes foo) else Nothing
-    g s@(T_DoubleQuoted _ l) = allInList l
-    g s@(T_DollarDoubleQuoted _ l) = allInList l
-    g s@(T_NormalWord _ l) = allInList l
+    allInList = liftM concat . sequence . map g
+    g (T_DoubleQuoted _ l) = allInList l
+    g (T_DollarDoubleQuoted _ l) = allInList l
+    g (T_NormalWord _ l) = allInList l
+    g (TA_Expansion _ l) = allInList l
     g (T_SingleQuoted _ s) = return s
     g (T_Literal _ s) = return s
     g x = more x
@@ -1355,14 +1397,12 @@
 isLiteral t = isJust $ getLiteralString t
 
 -- turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
-getWordParts t = g t
-  where
-    g (T_NormalWord _ l) = concatMap g l
-    g (T_DoubleQuoted _ l) = l
-    g other = [other]
+getWordParts (T_NormalWord _ l)   = concatMap getWordParts l
+getWordParts (T_DoubleQuoted _ l) = l
+getWordParts other                = [other]
 
 isCommand token str = isCommandMatch token (\cmd -> cmd  == str || ("/" ++ str) `isSuffixOf` cmd)
-isUnqualifiedCommand token str = isCommandMatch token (\cmd -> cmd  == str)
+isUnqualifiedCommand token str = isCommandMatch token (== str)
 
 isCommandMatch token matcher = fromMaybe False $ do
     cmd <- getCommandName token
@@ -1376,7 +1416,7 @@
 getCommandName _ = Nothing
 
 getCommandBasename = liftM basename . getCommandName
-basename = reverse . (takeWhile (/= '/')) . reverse
+basename = reverse . takeWhile (/= '/') . reverse
 
 isAssignment (T_Annotation _ _ w) = isAssignment w
 isAssignment (T_Redirecting _ _ w) = isAssignment w
@@ -1389,14 +1429,13 @@
 prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
 prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
 checkPrintfVar _ = checkUnqualifiedCommand "printf" (const f) where
-    f (dashv:var:rest) | getLiteralString dashv == (Just "-v") = f rest
+    f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
     f (format:params) = check format
     f _ = return ()
     check format =
-        if '%' `elem` (concat $ deadSimple format) || isLiteral format
-          then return ()
-          else warn (getId format) 2059 $
-                "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
+        unless ('%' `elem` concat (deadSimple format) || isLiteral format) $
+          warn (getId format) 2059
+              "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
 
 prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
 prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
@@ -1405,10 +1444,10 @@
 prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
 checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where
     msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
-    f [T_NormalWord id [(T_DollarExpansion _ _)]] = msg id
-    f [T_NormalWord id [T_DoubleQuoted _ [(T_DollarExpansion _ _)]]] = msg id
-    f [T_NormalWord id [(T_Backticked _ _)]] = msg id
-    f [T_NormalWord id [T_DoubleQuoted _ [(T_Backticked _ _)]]] = msg id
+    f [T_NormalWord id [T_DollarExpansion _ _]] = msg id
+    f [T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ _]]] = msg id
+    f [T_NormalWord id [T_Backticked _ _]] = msg id
+    f [T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ _]]] = msg id
     f _ = return ()
 
 prop_checkUuoeVar1 = verify checkUuoeVar "for f in $(echo $tmp); do echo lol; done"
@@ -1434,7 +1473,7 @@
     check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c
     check _ _ = return ()
     warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars ->
-        unless ("-" `isPrefixOf` (concat $ concatMap deadSimple vars)) $
+        unless ("-" `isPrefixOf` concat (concatMap deadSimple vars)) $
             when (all couldBeOptimized vars) $ style id 2116
                 "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."
 
@@ -1453,23 +1492,23 @@
 prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
 checkTr _ = checkCommand "tr" (const $ mapM_ f)
   where
-    f w | isGlob w = do -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
-        warn (getId w) 2060 $ "Quote parameters to tr to prevent glob expansion."
+    f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
+        warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion."
     f word =
       case getLiteralString word of
         Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."
         Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."
         Just s -> do  -- Eliminate false positives by only looking for dupes in SET2?
-          when ((not $ "-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
+          when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
             info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
           unless ("[:" `isPrefixOf` s) $
-            when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && (not $ '*' `elem` s)) $
+            when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
               info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets."
         Nothing -> return ()
 
     duplicated s =
         let relevant = filter isAlpha s
-        in not $ relevant == nub relevant
+        in relevant /= nub relevant
 
 
 prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
@@ -1506,21 +1545,21 @@
     f [] = return ()
     f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
     f (re:_) = do
-        when (isGlob re) $ do
-            warn (getId re) 2062 $ "Quote the grep pattern so the shell won't interpret it."
+        when (isGlob re) $
+            warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
         let string = concat $ deadSimple re
         if isConfusedGlobRegex string then
-            warn (getId re) 2063 $ "Grep uses regex, but this looks like a glob."
+            warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
           else potentially $ do
             char <- getSuspiciousRegexWildcard string
             return $ info (getId re) 2022 $
-                "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ (wordStartingWith char) ++ "'."
+                "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
 
 wordStartingWith c =
     head . filter ([c] `isPrefixOf`) $ candidates
   where
     candidates =
-        sampleWords ++ (map (\(x:r) -> (toUpper x) : r) sampleWords) ++ [c:"test"]
+        sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
 
 prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
 prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
@@ -1531,7 +1570,7 @@
     f _ = return ()
     checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs
     checkTrap _ = return ()
-    warning id = warn id 2064 $ "Use single quotes, otherwise this expands now rather than when signalled."
+    warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
     checkExpansions (T_DollarExpansion id _) = warning id
     checkExpansions (T_Backticked id _) = warning id
     checkExpansions (T_DollarBraced id _) = warning id
@@ -1543,16 +1582,15 @@
 prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
 checkTimeParameters _ = checkUnqualifiedCommand "time" f where
     f cmd (x:_) = let s = concat $ deadSimple x in
-                if "-" `isPrefixOf` s && s /= "-p" then
+                when ("-" `isPrefixOf` s && s /= "-p") $
                     info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."
-                  else return ()
     f _ _ = return ()
 
 prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"
 prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"
 prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo"
 checkTestRedirects _ (T_Redirecting id redirs@(redir:_) cmd) | cmd `isCommand` "test" =
-    warn (getId redir) 2065 $ "This is interpretted as a shell file redirection, not a comparison."
+    warn (getId redir) 2065 "This is interpretted as a shell file redirection, not a comparison."
 checkTestRedirects _ _ = return ()
 
 prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
@@ -1566,20 +1604,20 @@
     mapM_ warnAbout redirs
   where
     warnAbout (T_FdRedirect _ s (T_IoFile id op file))
-        | (s == "" || s == "&") && (not $ special file) =
+        | (s == "" || s == "&") && not (special file) =
         case op of
             T_Less _ ->
-              info (getId op) 2024 $
+              info (getId op) 2024
                 "sudo doesn't affect redirects. Use sudo cat file | .."
             T_Greater _ ->
-              warn (getId op) 2024 $
+              warn (getId op) 2024
                 "sudo doesn't affect redirects. Use ..| sudo tee file"
             T_DGREAT _ ->
-              warn (getId op) 2024 $
+              warn (getId op) 2024
                 "sudo doesn't affect redirects. Use .. | sudo tee -a file"
             _ -> return ()
     warnAbout _ = return ()
-    special file = (concat $ deadSimple file) == "/dev/null"
+    special file = concat (deadSimple file) == "/dev/null"
 checkSudoRedirect _ _ = return ()
 
 prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
@@ -1621,8 +1659,8 @@
         err i 2082 "To expand via indirection, use name=\"foo$n\"; echo \"${!name}\"."
   where
     isIndirection vars =
-        let list = catMaybes (map isIndirectionPart vars) in
-            not (null list) && all id list
+        let list = mapMaybe isIndirectionPart vars in
+            not (null list) && and list
     isIndirectionPart t =
         case t of T_DollarExpansion _ _ ->  Just True
                   T_Backticked _ _ ->       Just True
@@ -1642,11 +1680,11 @@
 prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
 checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
   where
-    check ((T_SingleQuoted _ _):(T_Literal id str):_)
+    check (T_SingleQuoted _ _:T_Literal id str:_)
         | all isAlphaNum str =
-        info id 2026 $ "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? "
+        info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? "
 
-    check ((T_DoubleQuoted _ _):trapped:(T_DoubleQuoted _ _):_) =
+    check (T_DoubleQuoted _ _:trapped:T_DoubleQuoted _ _:_) =
         case trapped of
             T_DollarExpansion id _ -> warnAboutExpansion id
             T_DollarBraced id _ -> warnAboutExpansion id
@@ -1655,9 +1693,9 @@
 
     check _ = return ()
     warnAboutExpansion id =
-        warn id 2027 $ "The surrounding quotes actually unquote this. Remove or escape them."
+        warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them."
     warnAboutLiteral id =
-        warn id 2140 $ "The double quotes around this do nothing. Remove or escape them."
+        warn id 2140 "The double quotes around this do nothing. Remove or escape them."
 checkInexplicablyUnquoted _ _ = return ()
 
 prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""
@@ -1669,9 +1707,9 @@
   where
     verify id ('~':_) = warn id 2088 "Note that ~ does not expand in quotes."
     verify _ _ = return ()
-    check (T_NormalWord _ ((T_SingleQuoted id str):_)) =
+    check (T_NormalWord _ (T_SingleQuoted id str:_)) =
         verify id str
-    check (T_NormalWord _ ((T_DoubleQuoted _ ((T_Literal id str):_)):_)) =
+    check (T_NormalWord _ (T_DoubleQuoted _ (T_Literal id str:_):_)) =
         verify id str
     check _ = return ()
 
@@ -1719,7 +1757,7 @@
     commentIfExec (T_Redirecting _ _ f@(
       T_SimpleCommand id _ (cmd:arg:_))) =
         when (f `isUnqualifiedCommand` "exec") $
-          warn (id) 2093 $
+          warn id 2093
             "Remove \"exec \" if script should continue after this command."
     commentIfExec _ = return ()
 
@@ -1751,7 +1789,7 @@
   where
     isDashE = mkRegex "^-.*e"
     hasEscapes = mkRegex "\\\\[rnt]"
-    f args | (concat $ concatMap deadSimple allButLast) `matches` isDashE =
+    f args | concat (concatMap deadSimple allButLast) `matches` isDashE =
         return ()
       where allButLast = reverse . drop 1 . reverse $ args
     f args = mapM_ checkEscapes args
@@ -1794,8 +1832,8 @@
 prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
 checkSshCommandString _ = checkCommand "ssh" (const f)
   where
-    nonOptions args =
-        filter (\x -> not $ "-" `isPrefixOf` (concat $ deadSimple x)) args
+    nonOptions =
+        filter (\x -> not $ "-" `isPrefixOf` concat (deadSimple x))
     f args =
         case nonOptions args of
             (hostport:r@(_:_)) -> checkArg $ last r
@@ -1803,7 +1841,7 @@
     checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
         case filter (not . isConstant) parts of
             [] -> return ()
-            (x:_) -> info (getId x) 2029 $
+            (x:_) -> info (getId x) 2029
                 "Note that, unescaped, this expands on the client side."
     checkArg _ = return ()
 
@@ -1850,7 +1888,7 @@
         T_Backticked _ _  -> SubshellScope "`..` expansion"
         T_Backgrounded _ _  -> SubshellScope "backgrounding &"
         T_Subshell _ _  -> SubshellScope "(..) group"
-        T_Redirecting _ _ _  ->
+        T_Redirecting {}  ->
             if fromMaybe False causesSubshell
             then SubshellScope "pipeline"
             else NoneScope
@@ -1859,7 +1897,7 @@
     parentPipeline = do
         parent <- Map.lookup (getId t) parents
         case parent of
-            T_Pipeline _ _ _ -> return parent
+            T_Pipeline {} -> return parent
             _ -> Nothing
 
     causesSubshell = do
@@ -1868,7 +1906,7 @@
             then return False
             else if lastCreatesSubshell
                 then return True
-                else return . not $ (getId . head $ reverse list) == (getId t)
+                else return . not $ (getId . head $ reverse list) == getId t
 
     lastCreatesSubshell =
         case shell of
@@ -1885,15 +1923,19 @@
                                     [(x, x, name, DataFrom [w])]
                                 _ -> []
                       ) vars
-        c@(T_SimpleCommand _ _ _) ->
+        c@(T_SimpleCommand {}) ->
             getModifiedVariableCommand c
 
-        TA_Unary _ "++|" (TA_Variable id name) -> [(t, t, name, DataFrom [t])]
-        TA_Unary _ "|++" (TA_Variable id name) -> [(t, t, name, DataFrom [t])]
-        TA_Binary _ op (TA_Variable id name) rhs ->
-            if any (==op) ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
-                then [(t, t, name, DataFrom [rhs])]
-                else []
+        TA_Unary _ "++|" var -> maybeToList $ do
+            name <- getLiteralString var
+            return (t, t, name, DataFrom [t])
+        TA_Unary _ "|++" var -> maybeToList $ do
+            name <- getLiteralString var
+            return (t, t, name, DataFrom [t])
+        TA_Binary _ op lhs rhs -> maybeToList $ do
+            guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
+            name <- getLiteralString lhs
+            return (t, t, name, DataFrom [rhs])
 
         --Points to 'for' rather than variable
         T_ForIn id _ strs words _ -> map (\str -> (t, t, str, DataFrom words)) strs
@@ -1901,7 +1943,7 @@
         _ -> []
 
 -- Consider 'export/declare -x' a reference, since it makes the var available
-getReferencedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) =
+getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
     case x of
         "export" -> concatMap getReference rest
         "declare" -> if "x" `elem` getFlags base
@@ -1915,7 +1957,7 @@
 
 getReferencedVariableCommand _ = []
 
-getModifiedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) =
+getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
    filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
     case x of
         "read" ->
@@ -1932,10 +1974,10 @@
   where
     stripEquals s = let rest = dropWhile (/= '=') s in
         if rest == "" then "" else tail rest
-    stripEqualsFrom (T_NormalWord id1 ((T_Literal id2 s):rs)) =
-        (T_NormalWord id1 ((T_Literal id2 (stripEquals s)):rs))
+    stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
+        T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
     stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
-        (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]])
+        T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
     stripEqualsFrom t = t
 
     getLiteral t = do
@@ -1951,11 +1993,11 @@
           if var == ""
             then []
             else [(base, token, var, DataFrom [stripEqualsFrom token])]
-        where var = takeWhile (isVariableChar) $ dropWhile (\x -> x `elem` "+-") $ concat $ deadSimple token
+        where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ deadSimple token
 getModifiedVariableCommand _ = []
 
 -- TODO:
-getBracedReference s = takeWhile (\x -> not $ x `elem` ":[#%/^,") $ dropWhile (`elem` "#!") s
+getBracedReference s = takeWhile (`notElem` ":[#%/^,") $ dropWhile (`elem` "#!") s
 getIndexReferences s = fromMaybe [] $ do
     (_, index, _, _) <- matchRegexAll re s
     return $ matchAll variableNameRegex index
@@ -1966,9 +2008,10 @@
     case t of
         T_DollarBraced id l -> let str = bracedString l in
             (t, t, getBracedReference str) :
-                (map (\x -> (l, l, x)) $ getIndexReferences str)
-        TA_Variable id str ->
-            map (\x -> (t, t, x)) $ (getBracedReference str):(getIndexReferences str)
+                map (\x -> (l, l, x)) (getIndexReferences str)
+        TA_Expansion id _ -> maybeToList $ do
+            str <- getLiteralStringExt (const $ return "#") t
+            return (t, t, getBracedReference str)
         T_Assignment id Append str _ _ -> [(t, t, str)]
         x -> getReferencedVariableCommand x
 
@@ -1979,15 +2022,15 @@
     startScope t =
         let scopeType = leadType shell parents t
         in do
-            when (scopeType /= NoneScope) $ modify ((StackScope scopeType):)
-            if assignFirst t then setWritten t else return ()
+            when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
+            when (assignFirst t) $ setWritten t
 
     endScope t =
         let scopeType = leadType shell parents t
         in do
             setRead t
-            if assignFirst t then return () else setWritten t
-            when (scopeType /= NoneScope) $ modify ((StackScopeEnd):)
+            unless (assignFirst t) $ setWritten t
+            when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
 
     assignFirst (T_ForIn {}) = True
     assignFirst (T_SelectIn {}) = True
@@ -1995,16 +2038,16 @@
 
     setRead t =
         let read    = getReferencedVariables t
-        in mapM_ (\v -> modify ((Reference v):)) read
+        in mapM_ (\v -> modify (Reference v:)) read
 
     setWritten t =
         let written = getModifiedVariables t
-        in mapM_ (\v -> modify ((Assignment v):)) written
+        in mapM_ (\v -> modify (Assignment v:)) written
 
 findSubshelled [] _ _ = return ()
-findSubshelled ((Assignment x@(_, _, str, _)):rest) ((reason,scope):lol) deadVars =
+findSubshelled (Assignment x@(_, _, str, _):rest) ((reason,scope):lol) deadVars =
     findSubshelled rest ((reason, x:scope):lol) $ Map.insert str Alive deadVars
-findSubshelled ((Reference (_, readToken, str)):rest) scopes deadVars = do
+findSubshelled (Reference (_, readToken, str):rest) scopes deadVars = do
     case Map.findWithDefault Alive str deadVars of
         Alive -> return ()
         Dead writeToken reason -> do
@@ -2012,15 +2055,15 @@
                     info (getId readToken) 2031 $ str ++ " was modified in a subshell. That change might be lost."
     findSubshelled rest scopes deadVars
 
-findSubshelled ((StackScope (SubshellScope reason)):rest) scopes deadVars =
+findSubshelled (StackScope (SubshellScope reason):rest) scopes deadVars =
     findSubshelled rest ((reason,[]):scopes) deadVars
 
-findSubshelled ((StackScopeEnd):rest) ((reason, scope):oldScopes) deadVars =
+findSubshelled (StackScopeEnd:rest) ((reason, scope):oldScopes) deadVars =
     findSubshelled rest oldScopes $
         foldl (\m (_, token, var, _) ->
             Map.insert var (Dead token reason) m) deadVars scope
 
-doVariableFlowAnalysis readFunc writeFunc empty flow = fst $ runState (
+doVariableFlowAnalysis readFunc writeFunc empty flow = evalState (
     foldM (\list x -> do { l <- doFlow x;  return $ l ++ list; }) [] flow
     ) empty
   where
@@ -2062,18 +2105,17 @@
         map <- get
         return $ Map.findWithDefault True name map
 
-    setSpaces name bool = do
+    setSpaces name bool =
         modify $ Map.insert name bool
 
     readF _ token name = do
         spaced <- hasSpaces name
-        if spaced
-          && (not $ "@" `isPrefixOf` name) -- There's another warning for this
-          && (not $ isCounting token)
-          && (not $ isQuoteFree parents token)
-          && (not $ usedAsCommandName parents token)
-            then return [(Note (getId token) InfoC 2086 warning)]
-            else return []
+        return [Note (getId token) InfoC 2086 warning |
+                  spaced
+                  && not ("@" `isPrefixOf` name) -- There's another warning for this
+                  && not (isCounting token)
+                  && not (isQuoteFree parents token)
+                  && not (usedAsCommandName parents token)]
       where
         warning = "Double quote to prevent globbing and word splitting."
 
@@ -2096,14 +2138,14 @@
     isCounting _ = False
 
     isSpacefulWord :: (String -> Bool) -> [Token] -> Bool
-    isSpacefulWord f words = any (isSpaceful f) words
+    isSpacefulWord f = any (isSpaceful f)
     isSpaceful :: (String -> Bool) -> Token -> Bool
     isSpaceful spacefulF x =
         case x of
           T_DollarExpansion _ _ -> True
           T_Backticked _ _ -> True
           T_Glob _ _         -> True
-          T_Extglob _ _ _    -> True
+          T_Extglob {}       -> True
           T_Literal _ s      -> s `containsAny` globspace
           T_SingleQuoted _ s -> s `containsAny` globspace
           T_DollarBraced _ l -> spacefulF $ getBracedReference $ bracedString l
@@ -2112,7 +2154,7 @@
           _ -> False
       where
         globspace = "*? \t\n"
-        containsAny s chars = any (\c -> c `elem` s) chars
+        containsAny s = any (`elem` s)
 
 
 prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
@@ -2157,16 +2199,17 @@
 
     readF _ expr name = do
         assignment <- getQuotes name
-        if isJust assignment
-            && not (isParamTo parents "eval" expr)
-            && not (isQuoteFree parents expr)
-            then return [
-                Note (fromJust assignment)WarningC 2089 $
-                    "Quotes/backslashes will be treated literally. Use an array.",
-                Note (getId expr) WarningC 2090 $
-                    "Quotes/backslashes in this variable will not be respected."
-              ]
-            else return []
+        return
+          (if isJust assignment
+              && not (isParamTo parents "eval" expr)
+              && not (isQuoteFree parents expr)
+              then [
+                  Note (fromJust assignment)WarningC 2089
+                      "Quotes/backslashes will be treated literally. Use an array.",
+                  Note (getId expr) WarningC 2090
+                      "Quotes/backslashes in this variable will not be respected."
+                ]
+              else [])
 
 
 prop_checkFunctionsUsedExternally1 =
@@ -2193,7 +2236,7 @@
             mapM_ (checkArg name) args
     checkCommand _ _ = return ()
 
-    analyse f t = snd $ runState (doAnalysis f t) []
+    analyse f t = execState (doAnalysis f t) []
     functions = Map.fromList $ analyse findFunctions t
     findFunctions (T_Function id _ _ name _) = modify ((name, id):)
     findFunctions t@(T_SimpleCommand id _ (_:args))
@@ -2207,7 +2250,7 @@
         case Map.lookup (concat $ deadSimple arg) functions of
             Nothing -> return ()
             Just id -> do
-              warn (getId arg) 2033 $
+              warn (getId arg) 2033
                 "Shell functions can't be passed to external commands."
               info id 2032 $
                 "Use own script or sh -c '..' to run this from " ++ cmd ++ "."
@@ -2230,6 +2273,8 @@
 prop_checkUnused15= verifyNotTree checkUnusedAssignments "x=(1); n=0; (( x[n] ))"
 prop_checkUnused16= verifyNotTree checkUnusedAssignments "foo=5; declare -x foo"
 prop_checkUnused17= verifyNotTree checkUnusedAssignments "read -i 'foo' -e -p 'Input: ' bar; $bar;"
+prop_checkUnused18= verifyNotTree checkUnusedAssignments "a=1; arr=( [$a]=42 ); echo \"${arr[@]}\""
+prop_checkUnused19= verifyNotTree checkUnusedAssignments "a=1; let b=a+1; echo $b"
 checkUnusedAssignments params t = snd $ runWriter (mapM_ checkAssignment flow)
   where
     flow = variableFlow params
@@ -2246,7 +2291,7 @@
                     name ++ " appears unused. Verify it or export it."
     checkAssignment _ = return ()
 
-    stripSuffix str = takeWhile isVariableChar str
+    stripSuffix = takeWhile isVariableChar
     defaultMap = Map.fromList $ zip internalVariables $ repeat ()
 
 prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"
@@ -2255,9 +2300,9 @@
 checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =
     mapM_ check $ takeWhile (not . isEndOfArgs) args
   where
-    check v@(T_NormalWord _ ((T_Glob id s):_)) | s == "*" || s == "?" =
+    check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" =
         info id 2035 $
-            "Use ./" ++ (concat $ deadSimple v)
+            "Use ./" ++ concat (deadSimple v)
                 ++ " so names with dashes won't become options."
     check _ = return ()
 
@@ -2280,7 +2325,7 @@
 prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file"
 
 checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)
-        | isStdinReadCommand command = do
+        | isStdinReadCommand command =
     mapM_ checkMuncher contents
   where
     munchers = [ "ssh", "ffmpeg", "mplayer" ]
@@ -2292,10 +2337,10 @@
             && all (not . stdinRedirect) redirs
     isStdinReadCommand _ = False
 
-    checkMuncher (T_Pipeline _ _ ((T_Redirecting _ redirs cmd):_)) | not $ any stdinRedirect redirs = do
+    checkMuncher (T_Pipeline _ _ (T_Redirecting _ redirs cmd:_)) | not $ any stdinRedirect redirs =
         case cmd of
             (T_IfExpression _ thens elses) ->
-              mapM_ checkMuncher . concat $ (map fst thens) ++ (map snd thens) ++ [elses]
+              mapM_ checkMuncher . concat $ map fst thens ++ map snd thens ++ [elses]
 
             _ -> potentially $ do
                 name <- getCommandBasename cmd
@@ -2347,12 +2392,11 @@
         && contents /= ":"
     then warn id 2101 "Named class needs outer [], e.g. [[:digit:]]."
     else
-        if ('[' `notElem` contents) && hasDupes
-        then info id 2102 "Ranges can only match single chars (mentioned due to duplicates)."
-        else return ()
+        when ('[' `notElem` contents && hasDupes) $
+            info id 2102 "Ranges can only match single chars (mentioned due to duplicates)."
   where
     isCharClass str = "[" `isPrefixOf` str && "]" `isSuffixOf` str
-    contents = drop 1 . take ((length str) - 1) $ str
+    contents = drop 1 . take (length str - 1) $ str
     hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents
 checkCharRangeGlob _ _ = return ()
 
@@ -2404,10 +2448,10 @@
     if not $ any isLoop path
     then if any isFunction $ take 1 path
         -- breaking at a source/function invocation is an abomination. Let's ignore it.
-        then err (getId t) 2104 $ "In functions, use return instead of " ++ (fromJust name) ++ "."
-        else err (getId t) 2105 $ (fromJust name) ++ " is only valid in loops."
+        then err (getId t) 2104 $ "In functions, use return instead of " ++ fromJust name ++ "."
+        else err (getId t) 2105 $ fromJust name ++ " is only valid in loops."
     else case map subshellType $ filter (not . isFunction) path of
-        (Just str):_ -> warn (getId t) 2106 $
+        Just str:_ -> warn (getId t) 2106 $
             "This only exits the subshell caused by the " ++ str ++ "."
         _ -> return ()
   where
@@ -2416,7 +2460,7 @@
     subshellType t = case leadType (shellType params) (parentMap params) t of
         NoneScope -> Nothing
         SubshellScope str -> return str
-    isFunction t = case t of T_Function _ _ _ _ _ -> True; _ -> False
+    isFunction t = case t of T_Function {} -> True; _ -> False
     relevant t = isLoop t || isFunction t || isJust (subshellType t)
 checkLoopKeywordScope _ _ = return ()
 
@@ -2426,10 +2470,10 @@
 prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }"
 checkFunctionDeclarations params
         (T_Function id (FunctionKeyword hasKeyword) (FunctionParentheses hasParens) _ _) =
-    case (shellType params) of
+    case shellType params of
         Bash -> return ()
         Zsh -> return ()
-        Ksh -> do
+        Ksh ->
             when (hasKeyword && hasParens) $
                 err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."
         Sh -> do
@@ -2451,7 +2495,7 @@
 prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
 prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"
 checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =
-    when (any isRecursiveFlag $ simpleArgs) $
+    when (any isRecursiveFlag simpleArgs) $
         mapM_ checkWord tokens
   where
     -- This ugly hack is based on the fact that ids generally increase
@@ -2463,8 +2507,8 @@
     checkWord token =
         case getLiteralString token of
             Just str ->
-                when (all (/= "--") simpleArgs && (fixPath str `elem` importantPaths)) $
-                    info (getId token) 2114 $ "Obligatory typo warning. Use 'rm --' to disable this message."
+                when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $
+                    info (getId token) 2114 "Obligatory typo warning. Use 'rm --' to disable this message."
             Nothing ->
                 checkWord' token
 
@@ -2472,12 +2516,12 @@
         m <- relevantMap id
         filename <- combine m token
         let path = fixPath filename
-        return . when (path `elem` importantPaths) $ do
+        return . when (path `elem` importantPaths) $
             warn (getId token) 2115 $ "Make sure this never accidentally expands to '" ++ path ++ "'."
 
     fixPath filename =
         let normalized = skipRepeating '/' . skipRepeating '*' $ filename in
-            if normalized == "/" then normalized else stripTrailing '/' $ normalized
+            if normalized == "/" then normalized else stripTrailing '/' normalized
 
     unnullable = all isVariableChar . concat . deadSimple
     isRecursiveFlag "--recursive" = True
@@ -2487,7 +2531,7 @@
 
     stripTrailing c = reverse . dropWhile (== c) . reverse
     skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
-    skipRepeating c (a:r) = a:(skipRepeating c r)
+    skipRepeating c (a:r) = a:skipRepeating c r
     skipRepeating _ [] = []
 
     addNulls map (Reference (_, token, name)) =
@@ -2498,13 +2542,10 @@
         if mightBeGuarded token
         then Map.insert name Nothing map
         else Map.insert name (Just "") map
-    addNulls m (Assignment (_, token, name, DataFrom [word])) =
-        if mightBeGuarded token
-        then Map.insert name Nothing m
-        else
-            if couldFail word
-            then m
-            else Map.insert name ((combine m) word) m
+    addNulls m (Assignment (_, token, name, DataFrom [word]))
+        | mightBeGuarded token = Map.insert name Nothing m
+        | couldFail word       = m
+        | otherwise            = Map.insert name (combine m word) m
     addNulls m (Assignment (_, token, name, DataFrom _)) =
         Map.insert name Nothing m
     addNulls map _ = map
@@ -2515,7 +2556,7 @@
 
     joinMaybes :: [Maybe String] -> Maybe String
     joinMaybes = foldl (liftM2 (++)) (Just "")
-    combine m token = c token
+    combine m = c
       where
         c (T_DollarBraced _ t) | unnullable t =
             Map.findWithDefault (Just "") (concat $ deadSimple t) m
@@ -2532,9 +2573,9 @@
 
     mightBeGuarded token = any t (getPath (parentMap params) token)
       where
-        t (T_Condition _ _ _) = True
-        t (T_OrIf _ _ _) = True
-        t (T_AndIf _ _ _) = True
+        t (T_Condition {}) = True
+        t (T_OrIf {}) = True
+        t (T_AndIf {}) = True
         t _ = False
 
     paths = [
@@ -2673,8 +2714,10 @@
 
 prop_checkUnsupported1 = verifyNot checkUnsupported "#!/bin/zsh\nfunction { echo cow; }"
 prop_checkUnsupported2 = verify checkUnsupported "#!/bin/sh\nfunction { echo cow; }"
+prop_checkUnsupported3 = verify checkUnsupported "#!/bin/sh\ncase foo in bar) baz ;& esac"
+prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac"
 checkUnsupported params t =
-    when  (shellType params `notElem` support) $
+    when ((not $ null support) && (shellType params `notElem` support)) $
         report name
  where
     (name, support) = shellSupport t
@@ -2689,23 +2732,26 @@
     T_ForIn _ _ (_:_:_) _ _ -> ("multi-index for loops", [Zsh])
     T_ForIn _ ShortForIn _ _ _ -> ("short form for loops", [Zsh])
     T_ProcSub _ "=" _ -> ("=(..) process substitution", [Zsh])
-    otherwise -> ("", [Bash, Ksh, Sh, Zsh])
-
-getCommandSequences t =
-    f t
+    T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list)
+    otherwise -> ("", [])
   where
-    f (T_Script _ _ cmds) = [cmds]
-    f (T_BraceGroup _ cmds) = [cmds]
-    f (T_Subshell _ cmds) = [cmds]
-    f (T_WhileExpression _ _ cmds) = [cmds]
-    f (T_UntilExpression _ _ cmds) = [cmds]
-    f (T_ForIn _ _ _ _ cmds) = [cmds]
-    f (T_ForArithmetic _ _ _ _ cmds) = [cmds]
-    f (T_IfExpression _ thens elses) = (map snd thens) ++ [elses]
-    f _ = []
+    forCase seps | any (== CaseContinue) seps = ("cases with ;;&", [Bash])
+    forCase seps | any (== CaseFallThrough) seps = ("cases with ;&", [Bash, Ksh, Zsh])
+    forCase _ = ("", [])
 
-groupWith f l = groupBy (\x y -> f x == f y) l
 
+getCommandSequences (T_Script _ _ cmds) = [cmds]
+getCommandSequences (T_BraceGroup _ cmds) = [cmds]
+getCommandSequences (T_Subshell _ cmds) = [cmds]
+getCommandSequences (T_WhileExpression _ _ cmds) = [cmds]
+getCommandSequences (T_UntilExpression _ _ cmds) = [cmds]
+getCommandSequences (T_ForIn _ _ _ _ cmds) = [cmds]
+getCommandSequences (T_ForArithmetic _ _ _ _ cmds) = [cmds]
+getCommandSequences (T_IfExpression _ thens elses) = map snd thens ++ [elses]
+getCommandSequences _ = []
+
+groupWith f = groupBy ((==) `on` f)
+
 prop_checkMultipleAppends1 = verify checkMultipleAppends "foo >> file; bar >> file; baz >> file;"
 prop_checkMultipleAppends2 = verify checkMultipleAppends "foo >> file; bar | grep f >> file; baz >> file;"
 prop_checkMultipleAppends3 = verifyNot checkMultipleAppends "foo < file; bar < file; baz < file;"
@@ -2722,7 +2768,7 @@
     checkGroup _ = return ()
     getTarget (T_Pipeline _ _ args@(_:_)) = getTarget (last args)
     getTarget (T_Redirecting id list _) = do
-        file <- (mapMaybe getAppend list) !!! 0
+        file <- mapMaybe getAppend list !!! 0
         return (file, id)
     getTarget _ = Nothing
     getAppend (T_FdRedirect _ _ (T_IoFile _ (T_DGREAT {}) f)) = return f
@@ -2736,8 +2782,8 @@
     checkUnqualifiedCommand "alias" (const f)
   where
     f = mapM_ checkArg
-    checkArg arg | '=' `elem` (concat $ deadSimple arg) =
-        flip mapM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
+    checkArg arg | '=' `elem` concat (deadSimple arg) =
+        forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
             \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
     checkArg _ = return ()
 
@@ -2748,8 +2794,8 @@
         str <- getLiteralString value
         return $ check str
   where
-    n = if (shellType params == Sh) then "'<literal linefeed here>'" else "$'\\n'"
-    t = if (shellType params == Sh) then "\"$(printf '\\t')\"" else "$'\\t'"
+    n = if shellType params == Sh then "'<literal linefeed here>'" else "$'\\n'"
+    t = if shellType params == Sh then "\"$(printf '\\t')\"" else "$'\\t'"
     check value =
         case value of
             "\\n" -> suggest n
@@ -2815,3 +2861,25 @@
     err (getId token) 2144 $
        op ++ " doesn't work with globs. Use a for loop."
 checkTestGlobs _ _ = return ()
+
+prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
+prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
+prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
+checkFindActionPrecedence params = checkCommand "find" (const f)
+  where
+    pattern = [isMatch, const True, isParam ["-o"], isMatch, const True, isAction]
+    f list | length list < length pattern = return ()
+    f list@(_:rest) =
+        if all id (zipWith ($) pattern list)
+        then warnFor (list !! ((length pattern)-1))
+        else f rest
+    isMatch = isParam [ "-name", "-regex", "-iname", "-iregex" ]
+    isAction = isParam [ "-exec", "-execdir", "-delete", "-print" ]
+    isParam strs t = fromMaybe False $ do
+        param <- getLiteralString t
+        return $ param `elem` strs
+    warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
+
+return []
+runTests = $quickCheckAll
+
diff --git a/ShellCheck/Data.hs b/ShellCheck/Data.hs
--- a/ShellCheck/Data.hs
+++ b/ShellCheck/Data.hs
@@ -1,6 +1,9 @@
 module ShellCheck.Data where
 
-shellcheckVersion = "0.3.3"  -- Must also be updated in ShellCheck.cabal
+import Data.Version (showVersion)
+import Paths_ShellCheck (version)
+
+shellcheckVersion = showVersion version
 
 internalVariables = [
     -- Generic
diff --git a/ShellCheck/Parser.hs b/ShellCheck/Parser.hs
--- a/ShellCheck/Parser.hs
+++ b/ShellCheck/Parser.hs
@@ -15,15 +15,15 @@
     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -}
-{-# LANGUAGE NoMonomorphismRestriction #-}
-
-module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote) where
+{-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell #-}
+module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote, runTests) where
 
 import ShellCheck.AST
 import ShellCheck.Data
 import Text.Parsec
 import Debug.Trace
 import Control.Monad
+import Control.Arrow (first)
 import Data.Char
 import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub)
 import qualified Data.Map as Map
@@ -33,9 +33,10 @@
 import System.IO
 import Text.Parsec.Error
 import GHC.Exts (sortWith)
+import Test.QuickCheck.All (quickCheckAll)
 
 backslash = char '\\'
-linefeed = (optional carriageReturn) >> char '\n'
+linefeed = optional carriageReturn >> char '\n'
 singleQuote = char '\'' <|> unicodeSingleQuote
 doubleQuote = char '"' <|> unicodeDoubleQuote
 variableStart = upper <|> lower <|> oneOf "_"
@@ -60,7 +61,7 @@
 
 prop_spacing = isOk spacing "  \\\n # Comment"
 spacing = do
-    x <- many (many1 linewhitespace <|> (try $ string "\\\n"))
+    x <- many (many1 linewhitespace <|> try (string "\\\n"))
     optional readComment
     return $ concat x
 
@@ -110,7 +111,7 @@
 data Note = Note Id Severity Code String deriving (Show, Eq)
 data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq)
 data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
-data Context = ContextName SourcePos String | ContextAnnotation [Annotation]
+data Context = ContextName SourcePos String | ContextAnnotation [Annotation] deriving (Show)
 type Code = Integer
 
 codeForParseNote (ParseNote _ _ code _) = code
@@ -131,7 +132,7 @@
     let newMap = Map.insert newId sourcepos map
     putState (newId, newMap, notes)
     return newId
-  where incId (Id n) = (Id $ n+1)
+  where incId (Id n) = Id $ n+1
 
 getNextId = do
     pos <- getPosition
@@ -151,7 +152,7 @@
 
 addParseNote n = do
     irrelevant <- shouldIgnoreCode (codeForParseNote n)
-    when (not irrelevant) $ do
+    unless irrelevant $ do
         (a, b, notes) <- getState
         putState (a, b, n:notes)
 
@@ -169,7 +170,7 @@
     pos <- getPosition
     parseProblemAt pos level code msg
 
-setCurrentContexts c = do
+setCurrentContexts c =
     Ms.modify (\(list, _) -> (list, c))
 
 getCurrentContexts = do
@@ -192,8 +193,8 @@
 
 parseProblemAt pos level code msg = do
     irrelevant <- shouldIgnoreCode code
-    when (not irrelevant) $
-        Ms.modify (\(list, current) -> ((ParseNote pos level code msg):list, current))
+    unless irrelevant $
+        Ms.modify (first ((:) (ParseNote pos level code msg)))
 
 -- Store non-parse problems inside
 
@@ -209,15 +210,15 @@
     optional follow
     return r
 
-unexpecting s p = try $ do
+unexpecting s p = try $
     (try p >> unexpected s) <|> return ()
 
 notFollowedBy2 = unexpecting "keyword/token"
 
-disregard x = x >> return ()
+disregard = void
 
-reluctantlyTill p end = do
-    (lookAhead ((disregard $ try end) <|> eof) >> return []) <|> do
+reluctantlyTill p end =
+    (lookAhead (disregard (try end) <|> eof) >> return []) <|> do
         x <- p
         more <- reluctantlyTill p end
         return $ x:more
@@ -229,15 +230,15 @@
     more <- reluctantlyTill p end
     return $ x:more
 
-attempting rest branch = do
-    ((try branch) >> rest) <|> rest
+attempting rest branch =
+    (try branch >> rest) <|> rest
 
-orFail parser stuff = do
+orFail parser stuff =
     try (disregard parser) <|> (disregard stuff >> fail "nope")
 
 wasIncluded p = option False (p >> return True)
 
-acceptButWarn parser level code note = do
+acceptButWarn parser level code note =
     optional $ try (do
         pos <- getPosition
         parser
@@ -252,17 +253,17 @@
         return v
      <|> do -- p failed without consuming input, abort context
         popContext
-        fail $ ""
+        fail ""
 
 called s p = do
     pos <- getPosition
     withContext (ContextName pos s) p
 
-withAnnotations anns p =
-    withContext (ContextAnnotation anns) p
+withAnnotations anns =
+    withContext (ContextAnnotation anns)
 
-readConditionContents single = do
-    readCondContents `attempting` (lookAhead $ do
+readConditionContents single =
+    readCondContents `attempting` lookAhead (do
                                 pos <- getPosition
                                 s <- many1 letter
                                 when (s `elem` commonCommands) $
@@ -273,7 +274,7 @@
     readCondBinaryOp = try $ do
         optional guardArithmetic
         id <- getNextId
-        op <- (choice $ (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"])) <|> otherOp
+        op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp
         hardCondSpacing
         return op
       where
@@ -301,7 +302,7 @@
           arg <- readCondWord
           return $ op arg)
         <|> (do
-              parseProblemAt pos ErrorC 1019 $ "Expected this to be an argument to the unary condition."
+              parseProblemAt pos ErrorC 1019 "Expected this to be an argument to the unary condition."
               fail "oops")
 
     readCondUnaryOp = try $ do
@@ -316,7 +317,7 @@
         return ('-':s)
 
     readCondWord = do
-        notFollowedBy2 (try (spacing >> (string "]")))
+        notFollowedBy2 (try (spacing >> string "]"))
         x <- readNormalWord
         pos <- getPosition
         when (endedWith "]" x) $ do
@@ -324,14 +325,14 @@
                 "You need a space before the " ++ (if single then "]" else "]]") ++ "."
             fail "Missing space before ]"
         when (single && endedWith ")" x) $ do
-            parseProblemAt pos ErrorC 1021 $
+            parseProblemAt pos ErrorC 1021
                 "You need a space before the \\)"
             fail "Missing space before )"
         disregard spacing
         return x
       where endedWith str (T_NormalWord id s@(_:_)) =
-                case (last s) of T_Literal id s -> str `isSuffixOf` s
-                                 _ -> False
+                case last s of T_Literal id s -> str `isSuffixOf` s
+                               _ -> False
             endedWith _ _ = False
 
     readCondAndOp = do
@@ -364,9 +365,9 @@
             op <- readCondBinaryOp
             y <- if isRegex
                     then readRegex
-                    else  readCondWord <|> ( (parseProblemAt pos ErrorC 1027 $ "Expected another argument for this operator.") >> mzero)
+                    else  readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero)
             return (x `op` y)
-          ) <|> (return $ TC_Noary id typ x)
+          ) <|> return (TC_Noary id typ x)
 
     readCondGroup = do
           id <- getNextId
@@ -389,7 +390,7 @@
         xor x y = x && not y || not x && y
 
     -- Currently a bit of a hack since parsing rules are obscure
-    regexOperatorAhead = (lookAhead $ do
+    regexOperatorAhead = lookAhead (do
         try (string "=~") <|> try (string "~=")
         return True)
           <|> return False
@@ -467,6 +468,8 @@
 prop_aD = isOk readArithmeticContents "foo[9*y+x]++"
 prop_aE = isOk readArithmeticContents "1+`echo 2`"
 prop_aF = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
+prop_a10= isOk readArithmeticContents "$foo$bar"
+prop_a11= isOk readArithmeticContents "i<(0+(1+1))"
 readArithmeticContents =
     readSequence
   where
@@ -484,25 +487,34 @@
         spacing
         return $ token id op
 
-    readVar = do
+    readArrayIndex = do
         id <- getNextId
-        x <- readVariableName
-        y <- readArrayIndex <|> return ""
-        optional spacing
-        return $ TA_Variable id (x ++ y)
+        start <- literal "["
+        middle <- readArithmeticContents
+        end <- literal "]"
+        return $ T_NormalWord id [start, middle, end]
 
-    -- Doesn't help with foo[foo]
-    readArrayIndex = do
-        char '['
-        x <- many1 $ noneOf "]"
-        char ']'
-        return $ "[" ++ x ++ "]"
+    literal s = do
+        id <- getNextId
+        string s
+        return $ T_Literal id s
 
+    readArithmeticLiteral =
+        readArrayIndex <|> literal "#"
+
     readExpansion = do
         id <- getNextId
-        x <- readNormalDollar <|> readBackTicked
+        pieces <- many1 $ choice [
+            readArithmeticLiteral,
+            readSingleQuoted,
+            readDoubleQuoted,
+            readNormalDollar,
+            readBraced,
+            readBackTicked,
+            readNormalLiteral "+-*/=%^,]"
+            ]
         spacing
-        return $ TA_Expansion id x
+        return $ TA_Expansion id pieces
 
     readGroup = do
         char '('
@@ -511,40 +523,7 @@
         spacing
         return s
 
-    readNumber = do
-        id <- getNextId
-        num <- many1 $ oneOf "0123456789."
-        return $ TA_Literal id (num)
-
-    readBased = getArbitrary <|> getHex <|> getOct
-      where
-        getThing prefix litchars = try $ do
-            id <- getNextId
-            x <- prefix
-            t <- readExpansion <|> (do
-                i <- getNextId
-                stuff <- many1 litchars
-                return $ TA_Literal i stuff)
-            return $ TA_Base id x t
-
-        getArbitrary = getThing arbitrary variableChars
-        getHex = getThing hex hexDigit
-        getOct = getThing oct digit
-
-        arbitrary = try $ do
-            b <- many1 digit
-            s <- char '#'
-            return (b ++ [s])
-        hex = try $ do
-            z <- char '0'
-            x <- oneOf "xX"
-            return (z:x:[])
-        oct = string "0"
-
-    readArithTerm = readBased <|> readArithTermUnit
-    readArithTermUnit = readGroup <|> readExpansion <|> readQuoted <|> readNumber <|> readVar
-
-    readQuoted = readDoubleQuoted <|> readSingleQuoted
+    readArithTerm = readGroup <|> readExpansion
 
     readSequence = do
         spacing
@@ -641,7 +620,7 @@
 readCondition = called "test expression" $ do
     opos <- getPosition
     id <- getNextId
-    open <- (try $ string "[[") <|> (string "[")
+    open <- try (string "[[") <|> string "["
     let single = open == "["
     condSpacingMsg False $ if single
           then "You need spaces after the opening [ and before the closing ]."
@@ -649,7 +628,7 @@
     condition <- readConditionContents single
 
     cpos <- getPosition
-    close <- (try $ string "]]") <|> (string "]")
+    close <- try (string "]]") <|> string "]"
     when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
     when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
     spacing
@@ -674,12 +653,12 @@
 readAnnotation = called "shellcheck annotation" $ do
     try readAnnotationPrefix
     many1 linewhitespace
-    values <- many1 (readDisable)
+    values <- many1 readDisable
     linefeed
     many linewhitespace
     return $ concat values
   where
-    readDisable = forKey "disable" $ do
+    readDisable = forKey "disable" $
         readCode `sepBy` char ','
       where
         readCode = do
@@ -718,12 +697,12 @@
     return $ T_NormalWord id x
 
 checkPossibleTermination pos [T_Literal _ x] =
-    if x `elem` ["do", "done", "then", "fi", "esac"]
-        then parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
-        else return ()
+    when (x `elem` ["do", "done", "then", "fi", "esac"]) $
+        parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
 checkPossibleTermination _ _ = return ()
 
 readNormalWordPart end = do
+    notFollowedBy2 $ oneOf end
     checkForParenthesis
     choice [
         readSingleQuoted,
@@ -737,7 +716,7 @@
         readLiteralCurlyBraces
       ]
   where
-    checkForParenthesis = do
+    checkForParenthesis =
         return () `attempting` do
             pos <- getPosition
             lookAhead $ char '('
@@ -806,9 +785,9 @@
 
     optional $ do
         c <- try . lookAhead $ suspectCharAfterQuotes <|> oneOf "'"
-        if (not (null string) && isAlpha c && isAlpha (last string))
+        if not (null string) && isAlpha c && isAlpha (last string)
           then
-            parseProblemAt endPos WarningC 1011 $
+            parseProblemAt endPos WarningC 1011
                 "This apostrophe terminated the single quoted string!"
           else
             when ('\n' `elem` string && not ("\n" `isPrefixOf` string)) $
@@ -824,7 +803,7 @@
 
 readSingleQuotedPart =
     readSingleEscaped
-    <|> (many1 $ noneOf "'\\\x2018\x2019")
+    <|> many1 (noneOf "'\\\x2018\x2019")
 
 prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"
 prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"
@@ -843,7 +822,7 @@
 
     optional $ do
         c <- try . lookAhead $ suspectCharAfterQuotes
-        when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $ do
+        when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $
             suggestForgotClosingQuote startPos endPos "backtick expansion"
 
     -- Result positions may be off due to escapes
@@ -858,7 +837,7 @@
       disregard (char '`') <|> do
          pos <- getPosition
          char '´'
-         parseProblemAt pos ErrorC 1077 $
+         parseProblemAt pos ErrorC 1077
             "For command expansion, the tick should slant left (` vs ´)."
 
 subParse pos parser input = do
@@ -889,7 +868,7 @@
             suggestForgotClosingQuote startPos endPos "double quoted string"
     return $ T_DoubleQuoted id x
   where
-    startsWithLineFeed ((T_Literal _ ('\n':_)):_) = True
+    startsWithLineFeed (T_Literal _ ('\n':_):_) = True
     startsWithLineFeed _ = False
     hasLineFeed (T_Literal _ str) | '\n' `elem` str = True
     hasLineFeed _ = False
@@ -897,7 +876,7 @@
 suggestForgotClosingQuote startPos endPos name = do
     parseProblemAt startPos WarningC 1078 $
         "Did you forget to close this " ++ name ++ "?"
-    parseProblemAt endPos InfoC 1079 $
+    parseProblemAt endPos InfoC 1079
         "This is actually an end quote, but due to next char it looks suspect."
 
 doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked
@@ -914,7 +893,7 @@
     return $ T_Literal id (concat s)
 
 readDoubleLiteralPart = do
-    x <- many1 $ (readDoubleEscaped <|> (many1 $ noneOf ('\\':doubleQuotableChars)))
+    x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars)))
     return $ concat x
 
 readNormalLiteral end = do
@@ -937,9 +916,9 @@
         readClass = try $ do
             id <- getNextId
             char '['
-            s <- many1 (predefined <|> (liftM return $ letter <|> digit <|> oneOf globchars))
+            s <- many1 (predefined <|> liftM return (letter <|> digit <|> oneOf globchars))
             char ']'
-            return $ T_Glob id $ "[" ++ (concat s) ++ "]"
+            return $ T_Glob id $ "[" ++ concat s ++ "]"
           where
            globchars = "^-_:?*.,!~@#$%=+{}/~"
            predefined = do
@@ -953,20 +932,20 @@
             c <- extglobStart <|> char '['
             return $ T_Literal id [c]
 
-readNormalLiteralPart end = do
-    readNormalEscaped <|> (many1 $ noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}"))
+readNormalLiteralPart end =
+    readNormalEscaped <|> many1 (noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}"))
 
 readNormalEscaped = called "escaped char" $ do
     pos <- getPosition
     backslash
     do
-        next <- (quotable <|> oneOf "?*@!+[]{}.,")
+        next <- quotable <|> oneOf "?*@!+[]{}.,"
         return $ if next == '\n' then "" else [next]
       <|>
         do
             next <- anyChar
             case escapedChar next of
-                Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use " ++ (alternative next) ++ " instead."
+                Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use " ++ alternative next ++ " instead."
                 Nothing -> parseNoteAt pos InfoC 1001 $ "This \\" ++ [next] ++ " will be a regular '" ++ [next] ++ "' in this context."
             return [next]
   where
@@ -991,7 +970,7 @@
             f <- extglobStart
             char '('
             return f
-    contents <- readExtglobPart `sepBy` (char '|')
+    contents <- readExtglobPart `sepBy` char '|'
     char ')'
     return $ T_Extglob id [c] contents
 
@@ -1003,7 +982,7 @@
     readExtglobGroup = do
         id <- getNextId
         char '('
-        contents <- readExtglobPart `sepBy` (char '|')
+        contents <- readExtglobPart `sepBy` char '|'
         char ')'
         return $ T_Extglob id "" contents
     readExtglobLiteral = do
@@ -1030,18 +1009,18 @@
 readDoubleEscaped = do
     bs <- backslash
     (linefeed >> return "")
-        <|> (doubleQuotable >>= return . return)
-        <|> (anyChar >>= (return . \x -> [bs, x]))
+        <|> liftM return doubleQuotable
+        <|> liftM (\ x -> [bs, x]) anyChar
 
 readBraceEscaped = do
     bs <- backslash
     (linefeed >> return "")
-        <|> (bracedQuotable >>= return . return)
-        <|> (anyChar >>= (return . \x -> [bs, x]))
+        <|> liftM return bracedQuotable
+        <|> liftM (\ x -> [bs, x]) anyChar
 
 
 readGenericLiteral endChars = do
-    strings <- many (readGenericEscaped <|> (many1 $ noneOf ('\\':endChars)))
+    strings <- many (readGenericEscaped <|> many1 (noneOf ('\\':endChars)))
     return $ concat strings
 
 readGenericLiteral1 endExp = do
@@ -1059,12 +1038,12 @@
     let strip (T_Literal _ s) = return ("\"" ++ s ++ "\"")
     id <- getNextId
     char '{'
-    str <- many1 ((readDoubleQuotedLiteral >>= (strip)) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace))
+    str <- many1 ((readDoubleQuotedLiteral >>= strip) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace))
     char '}'
     let result = concat str
     unless (',' `elem` result || ".." `isInfixOf` result) $
         fail "Not a brace expression"
-    return $ T_BraceExpansion id $ result
+    return $ T_BraceExpansion id result
 
 readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
 readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
@@ -1129,7 +1108,7 @@
     try (string "$(")
     cmds <- readCompoundList
     char ')' <?> "end of $(..) expression"
-    return $ (T_DollarExpansion id cmds)
+    return $ T_DollarExpansion id cmds
 
 prop_readDollarVariable = isOk readDollarVariable "$@"
 readDollarVariable = do
@@ -1176,6 +1155,7 @@
 prop_readHereDoc4 = isOk readHereDoc "<< foo\n`\nfoo"
 prop_readHereDoc5 = isOk readHereDoc "<<- !foo\nbar\n!foo"
 prop_readHereDoc6 = isOk readHereDoc "<< foo\\ bar\ncow\nfoo bar"
+prop_readHereDoc7 = isOk readHereDoc "<< foo\n\\$(f ())\nfoo"
 readHereDoc = called "here document" $ do
     fid <- getNextId
     pos <- getPosition
@@ -1189,8 +1169,8 @@
         parseProblemAt pos ErrorC 1038 message
     hid <- getNextId
     (quoted, endToken) <-
-            (readDoubleQuotedLiteral >>= return . (\x -> (Quoted, stripLiteral x)))
-            <|> (readSingleQuotedLiteral >>= return . (\x -> (Quoted, x)))
+            liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
+            <|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
             <|> (readToken >>= (\x -> return (Unquoted, x)))
     spacing
 
@@ -1214,7 +1194,7 @@
     stripLiteral (T_Literal _ x) = x
     stripLiteral (T_SingleQuoted _ x) = x
 
-    readToken = do
+    readToken =
         liftM concat $ many1 (escaped <|> quoted <|> normal)
       where
         quoted = liftM stripLiteral readDoubleQuotedLiteral <|> readSingleQuotedLiteral
@@ -1226,16 +1206,16 @@
 
     parseHereData Quoted startPos hereData = do
         id <- getNextIdAt startPos
-        return $ [T_Literal id hereData]
+        return [T_Literal id hereData]
 
-    parseHereData Unquoted startPos hereData = do
+    parseHereData Unquoted startPos hereData =
         subParse startPos readHereData hereData
 
-    readHereData = many $ try readNormalDollar <|> try readBackTicked <|> readHereLiteral
+    readHereData = many $ try doubleQuotedPart <|> readHereLiteral
 
     readHereLiteral = do
         id <- getNextId
-        chars <- many1 $ noneOf "`$"
+        chars <- many1 $ noneOf "`$\\"
         return $ T_Literal id chars
 
     verifyHereDoc dashed quoted spacing hereInfo = do
@@ -1245,17 +1225,17 @@
             parseNote ErrorC 1040 "When using <<-, you can only indent with tabs."
         return ()
 
-    debugHereDoc pos endToken doc =
-        if endToken `isInfixOf` doc
-            then
-                  let lookAt line = when (endToken `isInfixOf` line) $
-                            parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")
-                  in do
-                        parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.")
-                        mapM_ lookAt (lines doc)
-            else if (map toLower endToken) `isInfixOf` (map toLower doc)
-                then parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
-                else parseProblemAt pos ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")
+    debugHereDoc pos endToken doc
+        | endToken `isInfixOf` doc =
+            let lookAt line = when (endToken `isInfixOf` line) $
+                      parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")
+            in do
+                  parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.")
+                  mapM_ lookAt (lines doc)
+        | map toLower endToken `isInfixOf` map toLower doc =
+            parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
+        | otherwise =
+            parseProblemAt pos ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")
 
 
 readFilename = readNormalWord
@@ -1305,9 +1285,9 @@
 prop_readSeparator1 = isWarning readScript "a &; b"
 prop_readSeparator2 = isOk readScript "a & b"
 readSeparatorOp = do
-    notFollowedBy2 (g_AND_IF <|> g_DSEMI)
+    notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator)
     notFollowedBy2 (string "&>")
-    f <- (try $ do
+    f <- try (do
                     char '&'
                     spacing
                     pos <- getPosition
@@ -1320,7 +1300,7 @@
     spacing
     return f
 
-readSequentialSep = (disregard $ g_Semi >> readLineBreak) <|> (disregard readNewlineList)
+readSequentialSep = disregard (g_Semi >> readLineBreak) <|> disregard readNewlineList
 readSeparator =
     do
         separator <- readSeparatorOp
@@ -1343,9 +1323,9 @@
     in
         T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args
   where
-    assignment (T_Assignment _ _ _ _ _) = True
+    assignment (T_Assignment {}) = True
     assignment _ = False
-    redirection (T_FdRedirect _ _ _) = True
+    redirection (T_FdRedirect {}) = True
     redirection _ = False
 
 
@@ -1365,20 +1345,20 @@
     case cmd of
       Nothing -> return $ makeSimpleCommand id1 id2 prefix [] []
       Just cmd -> do
-            suffix <- option [] $
-                        if isModifierCommand cmd
-                        then readModifierSuffix
-                        else if isTimeCommand cmd
-                             then readTimeSuffix
-                             else readCmdSuffix
+            suffix <- option [] $ getParser readCmdSuffix cmd [
+                        (["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
+                        (["time"], readTimeSuffix),
+                        (["let"], readLetSuffix)
+                    ]
             return $ makeSimpleCommand id1 id2 prefix [cmd] suffix
   where
-    isModifierCommand (T_NormalWord _ [T_Literal _ s]) =
-        s `elem` ["declare", "export", "local", "readonly", "typeset"]
-    isModifierCommand _ = False
-    -- Might not belong in T_SimpleCommand. Fixme?
-    isTimeCommand (T_NormalWord _ [T_Literal _ "time"]) = True
-    isTimeCommand _ = False
+    isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
+    isCommand _ _ = False
+    getParser def cmd [] = def
+    getParser def cmd ((list, action):rest) =
+        if isCommand list cmd
+        then action
+        else getParser def cmd rest
 
 prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
 prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
@@ -1389,7 +1369,7 @@
         (T_Bang id) <- g_Bang
         pipe <- readPipeSequence
         return $ T_Banged id pipe
-      <|> do
+      <|>
         readPipeSequence
 
 prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
@@ -1399,7 +1379,7 @@
     aid <- getNextId
     annotations <- readAnnotations
 
-    andOr <- withAnnotations annotations $ do
+    andOr <- withAnnotations annotations $
         chainr1 readPipeline $ do
             op <- g_AND_IF <|> g_OR_IF
             readLineBreak
@@ -1419,11 +1399,11 @@
     do
         id <- getNextId
         sep <- readSeparator
-        more <- (option (T_EOF id) readAndOr)
+        more <- option (T_EOF id) readAndOr
         case more of (T_EOF _) -> return [transformWithSeparator id sep current]
                      _         -> do
                                 list <- readTerm' more
-                                return $ (transformWithSeparator id sep current : list)
+                                return (transformWithSeparator id sep current : list)
       <|>
         return [current]
 
@@ -1453,7 +1433,7 @@
     spacing
     return $ T_Pipe id ('|':qualifier)
 
-readCommand = (readCompoundCommand <|> readSimpleCommand)
+readCommand = readCompoundCommand <|> readSimpleCommand
 
 readCmdName = do
     f <- readNormalWord
@@ -1512,7 +1492,7 @@
 readElifPart = called "elif clause" $ do
     pos <- getPosition
     correctElif <- elif
-    when (not correctElif) $
+    unless correctElif $
         parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'."
     allspacing
     condition <- readTerm
@@ -1524,7 +1504,7 @@
     return (condition, action)
   where
     elif = (g_Elif >> return True) <|>
-        (try $ g_Else >> g_If >> return False)
+        try (g_Else >> g_If >> return False)
 
 readElsePart = called "else clause" $ do
     pos <- getPosition
@@ -1671,14 +1651,14 @@
 
 readInClause = do
     g_In
-    things <- (readCmdWord) `reluctantlyTill`
-                (disregard (g_Semi) <|> disregard linefeed <|> disregard g_Do)
+    things <- readCmdWord `reluctantlyTill`
+                (disregard g_Semi <|> disregard linefeed <|> disregard g_Do)
 
     do {
-        lookAhead (g_Do);
+        lookAhead g_Do;
         parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'.";
     } <|> do {
-        optional $ g_Semi;
+        optional g_Semi;
         disregard allspacing;
     }
 
@@ -1687,6 +1667,8 @@
 prop_readCaseClause = isOk readCaseClause "case foo in a ) lol; cow;; b|d) fooo; esac"
 prop_readCaseClause2 = isOk readCaseClause "case foo\n in * ) echo bar;; esac"
 prop_readCaseClause3 = isOk readCaseClause "case foo\n in * ) echo bar & ;; esac"
+prop_readCaseClause4 = isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac"
+prop_readCaseClause5 = isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac"
 readCaseClause = called "case expression" $ do
     id <- getNextId
     g_Case
@@ -1707,15 +1689,22 @@
     pattern <- readPattern
     g_Rparen
     readLineBreak
-    list <- ((lookAhead g_DSEMI >> return []) <|> readCompoundList)
-    (g_DSEMI <|> lookAhead (readLineBreak >> g_Esac)) `attempting` do
+    list <- (lookAhead readCaseSeparator >> return []) <|> readCompoundList
+    separator <- readCaseSeparator `attempting` do
         pos <- getPosition
         lookAhead g_Rparen
         parseProblemAt pos ErrorC 1074
             "Did you forget the ;; after the previous case item?"
     readLineBreak
-    return (pattern, list)
+    return (separator, pattern, list)
 
+readCaseSeparator = choice [
+    tryToken ";;&" (const ()) >> return CaseContinue,
+    tryToken ";&" (const ()) >> return CaseFallThrough,
+    g_DSEMI >> return CaseBreak,
+    lookAhead (readLineBreak >> g_Esac) >> return CaseBreak
+    ]
+
 prop_readFunctionDefinition = isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }"
 prop_readFunctionDefinition1 = isOk readFunctionDefinition "foo   (){ command foo --lol \"$@\"; }"
 prop_readFunctionDefinition4 = isWarning readFunctionDefinition "foo(a, b) { true; }"
@@ -1726,11 +1715,11 @@
 readFunctionDefinition = called "function" $ do
     functionSignature <- try readFunctionSignature
     allspacing
-    (disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition.")
+    disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition."
     group <- readBraceGroup <|> readSubshell
     return $ functionSignature group
   where
-    readFunctionSignature = do
+    readFunctionSignature =
         readWithFunction <|> readWithoutFunction
       where
         readWithFunction = do
@@ -1770,10 +1759,10 @@
     cmd <- choice [ readBraceGroup, readArithmeticExpression, readSubshell, readCondition, readWhileClause, readUntilClause, readIfClause, readForClause, readSelectClause, readCaseClause, readFunctionDefinition]
     optional spacing
     redirs <- many readIoRedirect
-    when (not . null $ redirs) $ optional $ do
+    unless (null redirs) $ optional $ do
         lookAhead $ try (spacing >> needsSeparator)
         parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."
-    return $ T_Redirecting id redirs $ cmd
+    return $ T_Redirecting id redirs cmd
   where
     needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]
 
@@ -1793,6 +1782,22 @@
         lookAhead $ char '-'
         readCmdWord
 
+-- Fixme: this is a hack that doesn't handle let '++c' or let a\>b
+readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
+  where
+    readLetExpression = do
+        startPos <- getPosition
+        expression <- readStringForParser readCmdWord
+        subParse startPos readArithmeticContents expression
+
+-- Get whatever a parser would parse as a string
+readStringForParser parser = do
+    pos <- lookAhead (parser >> getPosition)
+    s <- readUntil pos
+    return s
+  where
+    readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))
+
 prop_readAssignmentWord = isOk readAssignmentWord "a=42"
 prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"
 prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"
@@ -1802,7 +1807,8 @@
 prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
 prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
 prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
-prop_readAssignmentWord0 = isWarning readAssignmentWord "foo$n=42"
+prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
+prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
 readAssignmentWord = try $ do
     id <- getNextId
     pos <- getPosition
@@ -1853,10 +1859,25 @@
     id <- getNextId
     char '('
     allspacing
-    words <- (readNormalWord `thenSkip` allspacing) `reluctantlyTill` (char ')')
+    words <- readElement `reluctantlyTill` char ')'
     char ')'
     return $ T_Array id words
+  where
+    readElement = (readIndexed <|> readRegular) `thenSkip` allspacing
+    readIndexed = do
+        id <- getNextId
+        index <- try $ do
+            x <- readArrayIndex
+            char '='
+            return x
+        value <- readNormalWord <|> nothing
+        return $ T_IndexedElement id index value
+    readRegular = readNormalWord
 
+    nothing = do
+        id <- getNextId
+        return $ T_Literal id ""
+
 tryToken s t = try $ do
     id <- getNextId
     string s
@@ -1876,14 +1897,14 @@
     optional (do
         try . lookAhead $ char '['
         parseProblem ErrorC 1069 "You need a space before the [.")
-    try $ lookAhead (keywordSeparator)
+    try $ lookAhead keywordSeparator
     when (str /= keyword) $
         parseProblem ErrorC 1081 $
             "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."
     return $ t id
 
-anycaseString str =
-    mapM anycaseChar str
+anycaseString =
+    mapM anycaseChar
   where
     anycaseChar c = char (toLower c) <|> char (toUpper c)
 
@@ -1930,11 +1951,11 @@
     tryToken ";" T_Semi
 
 keywordSeparator =
-    eof <|> disregard whitespace <|> (disregard $ oneOf ";()[<>&|")
+    eof <|> disregard whitespace <|> disregard (oneOf ";()[<>&|")
 
 readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ]
 
-ifParse p t f = do
+ifParse p t f =
     (lookAhead (try p) >> t) <|> f
 
 readShebang = do
@@ -1953,24 +1974,24 @@
     pos <- getPosition
     optional $ do
         readUtf8Bom
-        parseProblem ErrorC 1082 $
+        parseProblem ErrorC 1082
             "This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
     sb <- option "" readShebang
     verifyShell pos (getShell sb)
-    if (isValidShell $ getShell sb) /= Just False
+    if isValidShell (getShell sb) /= Just False
       then
         do {
             allspacing;
             commands <- readTerm;
-            eof <|> (parseProblem ErrorC 1070 "Parsing stopped here because of parsing errors.");
+            eof <|> parseProblem ErrorC 1070 "Parsing stopped here because of parsing errors.";
             return $ T_Script id sb commands;
         } <|> do {
             parseProblem WarningC 1014 "Couldn't read any commands.";
-            return $ T_Script id sb $ [T_EOF id];
+            return $ T_Script id sb [T_EOF id];
         }
       else do
         many anyChar
-        return $ T_Script id sb $ [T_EOF id];
+        return $ T_Script id sb [T_EOF id];
 
   where
     basename s = reverse . takeWhile (/= '/') . reverse $ s
@@ -2018,8 +2039,8 @@
 
 rp p filename contents = Ms.runState (runParserT p initialState filename contents) ([], [])
 
-isWarning p s = (fst cs) && (not . null . snd $ cs) where cs = checkString p s
-isOk p s = (fst cs) && (null . snd $ cs) where cs = checkString p s
+isWarning p s = fst cs && (not . null . snd $ cs) where cs = checkString p s
+isOk p s = fst cs && (null . snd $ cs) where cs = checkString p s
 
 checkString parser string =
     case rp (parser >> eof >> getState) "-" string of
@@ -2043,7 +2064,7 @@
 
 getStringFromParsec errors =
         case map snd $ sortWith fst $ map f errors of
-            r -> (intercalate " " $ take 1 $ nub r)  ++ " Fix any mentioned problems and try again."
+            r -> unwords (take 1 $ nub r)  ++ " Fix any mentioned problems and try again."
     where f err =
             case err of
                 UnExpect s    -> (1, unexpected s)
@@ -2052,15 +2073,15 @@
                 Message s     -> (4, s ++ ".")
           wut "" = "eof"
           wut x = x
-          unexpected s = "Unexpected " ++ (wut s) ++ "."
+          unexpected s = "Unexpected " ++ wut s ++ "."
 
-parseShell filename contents = do
+parseShell filename contents =
     case rp (parseWithNotes readScript) filename contents of
         (Right (script, map, notes), (parsenotes, _)) ->
             ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes)
         (Left err, (p, context)) ->
             ParseResult Nothing
-                (nub $ sortNotes $ p ++ (notesForContext context) ++ ([makeErrorFor err]))
+                (nub $ sortNotes $ p ++ notesForContext context ++ [makeErrorFor err])
   where
     isName (ContextName _ _) = True
     isName _ = False
@@ -2071,4 +2092,8 @@
         "The mentioned parser error was in this " ++ str ++ "."
 
 lt x = trace (show x) x
-ltt t x = trace (show t) x
+ltt t = trace (show t)
+
+return []
+runTests = $quickCheckAll
+
diff --git a/ShellCheck/Simple.hs b/ShellCheck/Simple.hs
--- a/ShellCheck/Simple.hs
+++ b/ShellCheck/Simple.hs
@@ -15,35 +15,20 @@
     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -}
-module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage) where
+{-# LANGUAGE TemplateHaskell #-}
+module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage, runTests) where
 
-import ShellCheck.Parser
-import ShellCheck.Analytics
+import ShellCheck.Parser hiding (runTests)
+import ShellCheck.Analytics hiding (runTests)
 import Data.Maybe
 import Text.Parsec.Pos
 import Data.List
-
-
-prop_findsParseIssue =
-    let comments = shellCheck "echo \"$12\"" [] in
-        (length comments) == 1 && (scCode $ head comments) == 1037
-prop_commentDisablesParseIssue1 =
-    null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []
-prop_commentDisablesParseIssue2 =
-    null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []
-
-prop_findsAnalysisIssue =
-    let comments = shellCheck "echo $1" [] in
-        (length comments) == 1 && (scCode $ head comments) == 2086
-prop_commentDisablesAnalysisIssue1 =
-    null $ shellCheck "#shellcheck disable=SC2086\necho $1" []
-prop_commentDisablesAnalysisIssue2 =
-    null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []
+import Test.QuickCheck.All (quickCheckAll)
 
 shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment]
 shellCheck script options =
     let (ParseResult result notes) = parseShell "-" script in
-        let allNotes = notes ++ (concat $ maybeToList $ do
+        let allNotes = notes ++ concat (maybeToList $ do
             (tree, posMap) <- result
             let list = runAnalytics options tree
             return $ map (noteToParseNote posMap) $ filterByAnnotation tree list
@@ -65,3 +50,23 @@
 
 formatNote (ParseNote pos severity code text) =
     ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text
+
+prop_findsParseIssue =
+    let comments = shellCheck "echo \"$12\"" [] in
+        length comments == 1 && scCode (head comments) == 1037
+prop_commentDisablesParseIssue1 =
+    null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []
+prop_commentDisablesParseIssue2 =
+    null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []
+
+prop_findsAnalysisIssue =
+    let comments = shellCheck "echo $1" [] in
+        length comments == 1 && scCode (head comments) == 2086
+prop_commentDisablesAnalysisIssue1 =
+    null $ shellCheck "#shellcheck disable=SC2086\necho $1" []
+prop_commentDisablesAnalysisIssue2 =
+    null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []
+
+return []
+runTests = $quickCheckAll
+
diff --git a/shellcheck.1.md b/shellcheck.1.md
new file mode 100644
--- /dev/null
+++ b/shellcheck.1.md
@@ -0,0 +1,121 @@
+% SHELLCHECK(1) Shell script analysis tool
+
+# NAME
+
+shellcheck - Shell script analysis tool
+
+# SYNOPSIS
+
+**shellcheck** [*OPTIONS*...] *FILES*...
+
+# DESCRIPTION
+
+ShellCheck is a static analysis and linting tool for sh/bash scripts. It's
+mainly focused on handling typical beginner and intermediate level syntax
+errors and pitfalls where the shell just gives a cryptic error message or
+strange behavior, but it also reports on a few more advanced issues where
+corner cases can cause delayed failures.
+
+# OPTIONS
+
+**-f** *FORMAT*, **--format=***FORMAT*
+
+:   Specify the output format of shellcheck, which prints its results in the
+    standard output. Subsequent **-f** options are ignored, see **FORMATS**
+    below for more information.
+
+**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
+
+:   Explicitly exclude the specified codes from the report. Subsequent **-e**
+    options are cumulative, but all the codes can be specified at once,
+    comma-separated as a single argument.
+
+**-s**\ *shell*,\ **--shell=***shell*
+
+:   Specify Bourne shell dialect. Valid values are *sh*, *bash*, *ksh* and
+    *zsh*. The default is to use the file's shebang, or *bash* if the target
+    shell can't be determined.
+
+# FORMATS
+
+**tty**
+
+:   Plain text, human readable output. This is the default.
+
+**gcc**
+
+:   GCC compatible output. Useful for editors that support compiling and
+    showing syntax errors.
+
+    For example, in Vim, `:set makeprg=shellcheck\ -f\ gcc\ %` will allow
+    using `:make` to check the script, and `:cnext` to jump to the next error.
+
+        <file>:<line>:<column>: <type>: <message>
+
+**checkstyle**
+
+:   Checkstyle compatible XML output. Supported directly or through plugins
+    by many IDEs and build monitoring systems.
+
+        <?xml version='1.0' encoding='UTF-8'?>
+        <checkstyle version='4.3'>
+          <file name='file'>
+            <error
+              line='line'
+              column='column'
+              severity='severity'
+              message='message'
+              source='ShellCheck.SC####' />
+            ...
+          </file>
+          ...
+        </checkstyle>
+
+**json**
+
+:   Json is a popular serialization format that is more suitable for web
+    applications. ShellCheck's json is compact and contains only the bare
+    minimum.
+
+        [
+          {
+            "line": line,
+            "column": column,
+            "level": level,
+            "code": ####,
+            "message": message
+          },
+          ...
+        ]
+
+# DIRECTIVES
+ShellCheck directives can be specified as comments in the shell script
+before a command or block:
+
+    # shellcheck key=value key=value
+    command-or-structure
+
+For example, to suppress SC2035 about using `./*.jpg`:
+
+    # shellcheck disable=SC2035
+    echo "Files: " *.jpg
+
+Valid keys are:
+
+**disable**
+:   Disables a comma separated list of error codes for the following command.
+    The command can be a simple command like `echo foo`, or a compound command
+    like a function definition, subshell block or loop.
+
+
+# AUTHOR
+ShellCheck is written and maintained by Vidar Holen.
+
+# REPORTING BUGS
+Bugs and issues can be reported on GitHub:
+
+https://github.com/koalaman/shellcheck/issues
+
+# SEE ALSO
+
+sh(1) bash(1)
diff --git a/test/shellcheck.hs b/test/shellcheck.hs
new file mode 100644
--- /dev/null
+++ b/test/shellcheck.hs
@@ -0,0 +1,16 @@
+module Main where
+
+import Control.Monad
+import System.Exit
+import qualified ShellCheck.Simple
+import qualified ShellCheck.Analytics
+import qualified ShellCheck.Parser
+
+main = do
+    putStrLn "Running ShellCheck tests..."
+    results <- sequence [ShellCheck.Simple.runTests,
+                         ShellCheck.Analytics.runTests,
+                         ShellCheck.Parser.runTests]
+    if and results then exitSuccess
+     else exitFailure
+
