diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -7,14 +7,14 @@
 The goals of ShellCheck are
 
   - To point out and clarify typical beginner's syntax issues
-    that causes a shell to give cryptic error messages.
+    that cause 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.
+    that cause 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.
-    
+
 See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
 
 
@@ -48,13 +48,13 @@
 
 * Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
 
-* Most other editors, through [GCC error compatibility](blob/master/shellcheck.1.md#user-content-formats).
+* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
 
 
 #### In your build or test suites
 While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
 
-Use ShellCheck's exit code, or it's [CheckStyle compatible XML output](blob/master/shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
+Use ShellCheck's exit code, or its [CheckStyle compatible XML output](shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
 
 
 ## Installing
@@ -98,7 +98,7 @@
 
 ## Compiling from source
 
-This sections describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
+This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
 
 
 #### Installing Cabal
@@ -174,7 +174,7 @@
     [[ -e *.mpg ]]                    # Existence checks of globs
     [[ $foo==0 ]]                     # Always true due to missing spaces
     [[ -n "$foo " ]]                  # Always true due to literals
-    [[ $foo =~ "fo+" ]]               # Quoted regex in =~ 
+    [[ $foo =~ "fo+" ]]               # Quoted regex in =~
     [ foo =~ re ]                     # Unsupported [ ] operators
     [ $1 -eq "shellcheck" ]           # Numerical comparison of strings
     [ $n && $m ]                      # && in [ .. ]
@@ -219,7 +219,7 @@
     [[ -z $(find /tmp | grep mpg) ]]  # Use grep -q instead
     a >> log; b >> log; c >> log      # Use a redirection block instead
     echo "The time is `date`"         # Use $() instead
-    cd dir; process *; cd ..;         # Use subshells instead 
+    cd dir; process *; cd ..;         # Use subshells instead
     echo $[1+2]                       # Use standard $((..)) instead of old $[]
     echo $(($RANDOM % 6))             # Don't use $ on variables in $((..))
     echo "$(date)"                    # Useless use of echo
@@ -265,8 +265,8 @@
     foo-bar() { ..; }                # Undefined/unsupported function name
     [ $UID = 0 ]                     # Variable undefined in dash/sh
     local var=value                  # local is undefined in sh
-    
 
+
 #### Miscellaneous
 
 ShellCheck recognizes a menagerie of other issues:
@@ -279,7 +279,7 @@
     #!/bin/bash -x -e                 # Common shebang errors
     echo $((n/180*100))               # Unnecessary loss of precision
     ls *[:digit:].txt                 # Bad character class globs
-    sed 's/foo/bar/ file > file       # Redirecting to input
+    sed 's/foo/bar/' file > file       # Redirecting to input
 
 
 ## Testimonials
@@ -292,14 +292,14 @@
 
 ## Reporting bugs
 
-Please use the Github issue tracker for any bugs or feature suggestions:
+Please use the GitHub issue tracker for any bugs or feature suggestions:
 
 https://github.com/koalaman/shellcheck/issues
 
 
 ## Contributing
 
-Please submit patches to code or documentation as Github pull requests!
+Please submit patches to code or documentation as GitHub pull requests!
 
 Contributions must be licensed under the GNU GPLv3.
 The contributor retains the copyright.
diff --git a/ShellCheck.cabal b/ShellCheck.cabal
--- a/ShellCheck.cabal
+++ b/ShellCheck.cabal
@@ -1,5 +1,5 @@
 Name:             ShellCheck
-Version:          0.4.3
+Version:          0.4.4
 Synopsis:         Shell script analysis tool
 License:          GPL-3
 License-file:     LICENSE
@@ -52,7 +52,9 @@
       ShellCheck.ASTLib
       ShellCheck.Analytics
       ShellCheck.Analyzer
+      ShellCheck.AnalyzerLib
       ShellCheck.Checker
+      ShellCheck.Checks.Commands
       ShellCheck.Data
       ShellCheck.Formatter.Format
       ShellCheck.Formatter.CheckStyle
diff --git a/ShellCheck/AST.hs b/ShellCheck/AST.hs
--- a/ShellCheck/AST.hs
+++ b/ShellCheck/AST.hs
@@ -34,6 +34,7 @@
 
 data Token =
     TA_Binary Id String Token Token
+    | TA_Assignment Id String Token Token
     | TA_Expansion Id [Token]
     | TA_Index Id Token
     | TA_Sequence Id [Token]
@@ -129,7 +130,11 @@
     | T_Include Id Token Token -- . & source: SimpleCommand T_Script
     deriving (Show)
 
-data Annotation = DisableComment Integer | SourceOverride String deriving (Show, Eq)
+data Annotation =
+    DisableComment Integer
+    | SourceOverride String
+    | ShellOverride String
+    deriving (Show, Eq)
 data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
 
 -- This is an abomination.
@@ -162,7 +167,7 @@
     dll l m v = do
         x <- roundAll l
         y <- roundAll m
-        return $ v x m
+        return $ v x y
     d1 t v = do
         x <- round t
         return $ v x
@@ -246,6 +251,7 @@
     delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
 
     delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
+    delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
     delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
     delve (TA_Sequence id l) = dl l $ TA_Sequence id
     delve (TA_Trinary id t1 t2 t3) = do
@@ -340,6 +346,7 @@
         TC_Unary id _ _ _  -> id
         TC_Noary id _ _  -> id
         TA_Binary id _ _ _  -> id
+        TA_Assignment id _ _ _  -> id
         TA_Unary id _ _  -> id
         TA_Sequence id _  -> id
         TA_Trinary id _ _ _  -> id
diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs
--- a/ShellCheck/Analytics.hs
+++ b/ShellCheck/Analytics.hs
@@ -22,3688 +22,2773 @@
 
 import ShellCheck.AST
 import ShellCheck.ASTLib
-import ShellCheck.Data
-import ShellCheck.Parser
-import ShellCheck.Interface
-import ShellCheck.Regex
-
-import Control.Arrow (first)
-import Control.Monad
-import Control.Monad.Identity
-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 Data.Ord
-import Debug.Trace
-import qualified Data.Map as Map
-import Test.QuickCheck.All (forAllProperties)
-import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
-
-data Parameters = Parameters {
-    variableFlow :: [StackData],
-    parentMap :: Map.Map Id Token,
-    shellType :: Shell,
-    shellTypeSpecified :: Bool
-    }
-
--- Checks that are run on the AST root
-treeChecks :: [Parameters -> Token -> [TokenComment]]
-treeChecks = [
-    runNodeAnalysis
-        (\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p))
-            (nodeChecks ++ checksFor (shellType p))))
-    ,subshellAssignmentCheck
-    ,checkSpacefulness
-    ,checkQuotesInLiterals
-    ,checkShebangParameters
-    ,checkFunctionsUsedExternally
-    ,checkUnusedAssignments
-    ,checkUnpassedInFunctions
-    ,checkArrayWithoutIndex
-    ,checkShebang
-    ,checkUnassignedReferences
-    ,checkUncheckedCd
-    ]
-
-checksFor Sh = [
-    checkBashisms
-    ,checkTimeParameters
-    ,checkForDecimals
-    ]
-checksFor Dash = [
-    checkBashisms
-    ,checkForDecimals
-    ,checkLocalScope
-    ]
-checksFor Ksh = [
-    checkEchoSed
-    ]
-checksFor Bash = [
-    checkTimeParameters
-    ,checkBraceExpansionVars
-    ,checkEchoSed
-    ,checkForDecimals
-    ,checkLocalScope
-    ]
-
-runAnalytics :: AnalysisSpec -> AnalysisResult
-runAnalytics options = AnalysisResult {
-        arComments =
-            nub . filterByAnnotation (asScript options) $
-                runList options treeChecks
-    }
-
-runList :: AnalysisSpec -> [Parameters -> Token -> [TokenComment]]
-    -> [TokenComment]
-runList spec list = notes
-    where
-        root = asScript spec
-        params = Parameters {
-                shellType = fromMaybe (determineShell root) $ asShellType spec,
-                shellTypeSpecified = isJust $ asShellType spec,
-                parentMap = getParentTree root,
-                variableFlow =
-                    getVariableFlow (shellType params) (parentMap params) root
-        }
-        notes = concatMap (\f -> f params root) list
-
-checkList l t = concatMap (\f -> f t) l
-
-getCode (TokenComment _ (Comment _ c _)) = c
-
-prop_determineShell0 = determineShell (T_Script (Id 0) "#!/bin/sh" []) == Sh
-prop_determineShell1 = determineShell (T_Script (Id 0) "#!/usr/bin/env ksh" []) == Ksh
-prop_determineShell2 = determineShell (T_Script (Id 0) "" []) == Bash
-prop_determineShell3 = determineShell (T_Script (Id 0) "#!/bin/sh -e" []) == Sh
-determineShell (T_Script _ shebang _) = fromMaybe Bash . shellForExecutable $ shellFor shebang
-    where shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
-          shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
-          shellFor s = reverse . takeWhile (/= '/') . reverse $ s
-
--- Checks that are run on each node in the AST
-runNodeAnalysis f p t = execWriter (doAnalysis (f p) t)
-
-nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()]
-nodeChecks = [
-    checkUuoc
-    ,checkPipePitfalls
-    ,checkForInQuoted
-    ,checkForInLs
-    ,checkShorthandIf
-    ,checkDollarStar
-    ,checkUnquotedDollarAt
-    ,checkStderrRedirect
-    ,checkUnquotedN
-    ,checkNumberComparisons
-    ,checkSingleBracketOperators
-    ,checkDoubleBracketOperators
-    ,checkLiteralBreakingTest
-    ,checkConstantNoary
-    ,checkDivBeforeMult
-    ,checkArithmeticDeref
-    ,checkArithmeticBadOctal
-    ,checkComparisonAgainstGlob
-    ,checkPrintfVar
-    ,checkCommarrays
-    ,checkOrNeq
-    ,checkEchoWc
-    ,checkConstantIfs
-    ,checkTr
-    ,checkPipedAssignment
-    ,checkAssignAteCommand
-    ,checkUuoeCmd
-    ,checkUuoeVar
-    ,checkFindNameGlob
-    ,checkGrepRe
-    ,checkNeedlessCommands
-    ,checkQuotedCondRegex
-    ,checkForInCat
-    ,checkFindExec
-    ,checkValidCondOps
-    ,checkGlobbedRegex
-    ,checkTrapQuotes
-    ,checkTestRedirects
-    ,checkIndirectExpansion
-    ,checkSudoRedirect
-    ,checkPS1Assignments
-    ,checkBackticks
-    ,checkInexplicablyUnquoted
-    ,checkTildeInQuotes
-    ,checkLonelyDotDash
-    ,checkSpuriousExec
-    ,checkSpuriousExpansion
-    ,checkUnusedEchoEscapes
-    ,checkDollarBrackets
-    ,checkSshHereDoc
-    ,checkSshCommandString
-    ,checkGlobsAsOptions
-    ,checkWhileReadPitfalls
-    ,checkArithmeticOpCommand
-    ,checkCharRangeGlob
-    ,checkUnquotedExpansions
-    ,checkSingleQuotedVariables
-    ,checkRedirectToSame
-    ,checkPrefixAssignmentReference
-    ,checkLoopKeywordScope
-    ,checkCdAndBack
-    ,checkWrongArithmeticAssignment
-    ,checkConditionalAndOrs
-    ,checkFunctionDeclarations
-    ,checkCatastrophicRm
-    ,checkInteractiveSu
-    ,checkStderrPipe
-    ,checkSetAssignment
-    ,checkOverridingPath
-    ,checkArrayAsString
-    ,checkUnsupported
-    ,checkMultipleAppends
-    ,checkAliasesExpandEarly
-    ,checkSuspiciousIFS
-    ,checkAliasesUsesArgs
-    ,checkShouldUseGrepQ
-    ,checkTestGlobs
-    ,checkConcatenatedDollarAt
-    ,checkFindActionPrecedence
-    ,checkTildeInPath
-    ,checkFindExecWithSingleArgument
-    ,checkReturn
-    ,checkMaskedReturns
-    ,checkInjectableFindSh
-    ,checkReadWithoutR
-    ,checkExportedExpansions
-    ,checkLoopVariableReassignment
-    ,checkTrailingBracket
-    ,checkNonportableSignals
-    ,checkMkdirDashPM
-    ]
-
-filterByAnnotation token =
-    filter (not . shouldIgnore)
-  where
-    idFor (TokenComment id _) = id
-    shouldIgnore note =
-        any (shouldIgnoreFor (getCode note)) $
-            getPath parents (T_Bang $ idFor note)
-    shouldIgnoreFor num (T_Annotation _ anns _) =
-        any hasNum anns
-      where
-        hasNum (DisableComment ts) = num == ts
-        hasNum _ = False
-    shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
-    shouldIgnoreFor _ _ = False
-    parents = getParentTree token
-
-makeComment :: Severity -> Id -> Code -> String -> TokenComment
-makeComment severity id code note =
-    TokenComment id $ Comment severity code note
-
-addComment note = tell [note]
-
-warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
-warn  id code str = addComment $ makeComment WarningC id code str
-err   id code str = addComment $ makeComment ErrorC id code str
-info  id code str = addComment $ makeComment InfoC id code str
-style id code str = addComment $ makeComment StyleC id code str
-
-isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
-isVariableChar x = isVariableStartChar x || isDigit x
-variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
-
-prop_isVariableName1 = isVariableName "_fo123"
-prop_isVariableName2 = not $ isVariableName "4"
-prop_isVariableName3 = not $ isVariableName "test: "
-isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
-isVariableName _ = False
-
-potentially = fromMaybe (return ())
-
-wouldHaveBeenGlob s = '*' `elem` s
-
-isConfusedGlobRegex ('*':_) = True
-isConfusedGlobRegex [x,'*'] | x /= '\\' = True
-isConfusedGlobRegex _ = False
-
-getSuspiciousRegexWildcard str =
-    if not $ str `matches` contra
-    then do
-        match <- matchRegex suspicious str
-        str <- match !!! 0
-        str !!! 0
-    else
-        fail "looks good"
-  where
-    suspicious = mkRegex "([A-Za-z1-9])\\*"
-    contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
-
-headOrDefault _ (a:_) = a
-headOrDefault def _ = def
-
-
-(!!!) list i =
-    case drop i list of
-        [] -> Nothing
-        (r:_) -> Just r
-
-verify :: (Parameters -> Token -> Writer [TokenComment] ()) -> String -> Bool
-verify f s = checkNode f s == Just True
-
-verifyNot :: (Parameters -> Token -> Writer [TokenComment] ()) -> String -> Bool
-verifyNot f s = checkNode f s == Just False
-
-verifyTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool
-verifyTree f s = producesComments f s == Just True
-
-verifyNotTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool
-verifyNotTree f s = producesComments f s == Just False
-
-
-defaultSpec root = AnalysisSpec {
-    asScript = root,
-    asShellType = Nothing,
-    asExecutionMode = Executed
-}
-
-checkNode f = producesComments (runNodeAnalysis f)
-producesComments :: (Parameters -> Token -> [TokenComment]) -> String -> Maybe Bool
-producesComments f s = do
-        root <- prRoot pResult
-        return . not . null $ runList (defaultSpec root) [f]
-  where
-    pSpec = ParseSpec {
-        psFilename = "script",
-        psScript = s
-    }
-    pResult = runIdentity $ parseScript (mockedSystemInterface []) pSpec
-
--- Copied from https://wiki.haskell.org/Edit_distance
-dist :: Eq a => [a] -> [a] -> Int
-dist a b
-    = last (if lab == 0 then mainDiag
-            else if lab > 0 then lowers !! (lab - 1)
-                 else{- < 0 -}   uppers !! (-1 - lab))
-    where mainDiag = oneDiag a b (head uppers) (-1 : head lowers)
-          uppers = eachDiag a b (mainDiag : uppers) -- upper diagonals
-          lowers = eachDiag b a (mainDiag : lowers) -- lower diagonals
-          eachDiag a [] diags = []
-          eachDiag a (bch:bs) (lastDiag:diags) = oneDiag a bs nextDiag lastDiag : eachDiag a bs diags
-              where nextDiag = head (tail diags)
-          oneDiag a b diagAbove diagBelow = thisdiag
-              where doDiag [] b nw n w = []
-                    doDiag a [] nw n w = []
-                    doDiag (ach:as) (bch:bs) nw n w = me : doDiag as bs me (tail n) (tail w)
-                        where me = if ach == bch then nw else 1 + min3 (head w) nw (head n)
-                    firstelt = 1 + head diagBelow
-                    thisdiag = firstelt : doDiag a b firstelt diagAbove (tail diagBelow)
-          lab = length a - length b
-          min3 x y z = if x < y then x else min y z
-
-hasFloatingPoint params = shellType params == Ksh
-
--- Checks whether the current parent path is part of a condition
-isCondition [] = False
-isCondition [_] = False
-isCondition (child:parent:rest) =
-    getId child `elem` map getId (getConditionChildren parent) || isCondition (parent:rest)
-  where
-    getConditionChildren t =
-        case t of
-            T_AndIf _ left right -> [left]
-            T_OrIf id left right -> [left]
-            T_IfExpression id conditions elses -> concatMap (take 1 . reverse . fst) conditions
-            T_WhileExpression id c l -> take 1 . reverse $ c
-            T_UntilExpression id c l -> take 1 . reverse $ c
-            _ -> []
-
-prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)"
-checkEchoWc _ (T_Pipeline id _ [a, b]) =
-    when (acmd == ["echo", "${VAR}"]) $
-        case bcmd of
-            ["wc", "-c"] -> countMsg
-            ["wc", "-m"] -> countMsg
-            _ -> return ()
-  where
-    acmd = oversimplify a
-    bcmd = oversimplify b
-    countMsg = style id 2000 "See if you can use ${#variable} instead."
-checkEchoWc _ _ = return ()
-
-prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
-prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
-checkEchoSed _ (T_Pipeline id _ [a, b]) =
-    when (acmd == ["echo", "${VAR}"]) $
-        case bcmd of
-            ["sed", v] -> checkIn v
-            ["sed", "-e", v] -> checkIn v
-            _ -> return ()
-  where
-    -- This should have used backreferences, but TDFA doesn't support them
-    sedRe = mkRegex "^s(.)([^\n]*)g?$"
-    isSimpleSed s = fromMaybe False $ do
-        [first,rest] <- matchRegex sedRe s
-        let delimiters = filter (== (head first)) rest
-        guard $ length delimiters == 2
-        return True
-
-    acmd = oversimplify a
-    bcmd = oversimplify b
-    checkIn s =
-        when (isSimpleSed s) $
-            style id 2001 "See if you can use ${variable//search/replace} instead."
-checkEchoSed _ _ = return ()
-
-prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo"
-prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo"
-prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo"
-checkPipedAssignment _ (T_Pipeline _ _ (T_Redirecting _ _ (T_SimpleCommand id (_:_) []):_:_)) =
-    warn id 2036 "If you wanted to assign the output of the pipeline, use a=$(b | c) ."
-checkPipedAssignment _ _ = return ()
-
-prop_checkAssignAteCommand1 = verify checkAssignAteCommand "A=ls -l"
-prop_checkAssignAteCommand2 = verify checkAssignAteCommand "A=ls --sort=$foo"
-prop_checkAssignAteCommand3 = verify checkAssignAteCommand "A=cat foo | grep bar"
-prop_checkAssignAteCommand4 = verifyNot checkAssignAteCommand "A=foo ls -l"
-prop_checkAssignAteCommand5 = verifyNot checkAssignAteCommand "PAGER=cat grep bar"
-checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) (firstWord:_)) =
-    when ("-" `isPrefixOf` concat (oversimplify firstWord) ||
-        isCommonCommand (getLiteralString assignmentTerm)
-            && not (isCommonCommand (getLiteralString firstWord))) $
-                warn id 2037 "To assign the output of a command, use var=$(cmd) ."
-  where
-    isCommonCommand (Just s) = s `elem` commonCommands
-    isCommonCommand _ = False
-checkAssignAteCommand _ _ = return ()
-
-prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1"
-prop_checkArithmeticOpCommand2 = verify checkArithmeticOpCommand "foo=bar * 2"
-prop_checkArithmeticOpCommand3 = verifyNot checkArithmeticOpCommand "foo + opts"
-checkArithmeticOpCommand _ (T_SimpleCommand id [T_Assignment {}] (firstWord:_)) =
-    fromMaybe (return ()) $ check <$> getGlobOrLiteralString firstWord
-  where
-    check op =
-        when (op `elem` ["+", "-", "*", "/"]) $
-            warn (getId firstWord) 2099 $
-                "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
-checkArithmeticOpCommand _ _ = return ()
-
-prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1"
-prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2"
-checkWrongArithmeticAssignment params (T_SimpleCommand id (T_Assignment _ _ _ _ val:[]) []) =
-  fromMaybe (return ()) $ do
-    str <- getNormalString val
-    match <- matchRegex regex str
-    var <- match !!! 0
-    op <- match !!! 1
-    Map.lookup var references
-    return . warn (getId val) 2100 $
-        "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
-  where
-    regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$"
-    references = foldl (flip ($)) Map.empty (map insertRef $ variableFlow params)
-    insertRef (Assignment (_, _, name, _)) =
-        Map.insert name ()
-    insertRef _ = Prelude.id
-
-    getNormalString (T_NormalWord _ words) = do
-        parts <- foldl (liftM2 (\x y -> x ++ [y])) (Just []) $ map getLiterals words
-        return $ concat parts
-    getNormalString _ = Nothing
-
-    getLiterals (T_Literal _ s) = return s
-    getLiterals (T_Glob _ s) = return s
-    getLiterals _ = Nothing
-checkWrongArithmeticAssignment _ _ = return ()
-
-
-prop_checkUuoc1 = verify checkUuoc "cat foo | grep bar"
-prop_checkUuoc2 = verifyNot checkUuoc "cat * | grep bar"
-prop_checkUuoc3 = verify checkUuoc "cat $var | grep bar"
-prop_checkUuoc4 = verifyNot checkUuoc "cat $var"
-prop_checkUuoc5 = verifyNot checkUuoc "cat \"$@\""
-prop_checkUuoc6 = verifyNot checkUuoc "cat -n | grep bar"
-checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) =
-    checkCommand "cat" (const f) cmd
-  where
-    f [word] = unless (mayBecomeMultipleArgs word || isOption word) $
-        style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead."
-    f _ = return ()
-    isOption word = "-" `isPrefixOf` onlyLiteralString word
-checkUuoc _ _ = return ()
-
-prop_checkNeedlessCommands = verify checkNeedlessCommands "foo=$(expr 3 + 2)"
-prop_checkNeedlessCommands2 = verify checkNeedlessCommands "foo=`echo \\`expr 3 + 2\\``"
-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) =
-    style id 2003 "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-  where
-    -- These operators are hard to replicate in POSIX
-    exceptions = [ ":", "<", ">", "<=", ">=" ]
-    words = mapMaybe getLiteralString args
-checkNeedlessCommands _ _ = return ()
-
-prop_checkPipePitfalls3 = verify checkPipePitfalls "ls | grep -v mp3"
-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 = oversimplify xargs ++ oversimplify find
-          in
-            unless (any ($ 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?"
-
-    for' ["ps", "grep"] $
-        \x -> info x 2009 "Consider using pgrep instead of grepping ps output."
-
-    for' ["grep", "wc"] $
-        \x -> style x 2126 "Consider using grep -c instead of grep|wc."
-
-    didLs <- liftM or . sequence $ [
-        for' ["ls", "grep"] $
-            \x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.",
-        for' ["ls", "xargs"] $
-            \x -> warn x 2011 "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames."
-        ]
-    unless didLs $ do
-        for ["ls", "?"] $
-            \(ls:_) -> unless (hasShortParameter 'N' (oversimplify 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 "" . oversimplify) commands)
-        in do
-            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 char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
-    hasParameter string =
-        any (isPrefixOf string . dropWhile (== '-'))
-checkPipePitfalls _ _ = return ()
-
-indexOfSublists sub = f 0
-  where
-    f _ [] = []
-    f n a@(r:rest) =
-        let others = f (n+1) rest in
-            if match sub a
-              then n:others
-              else others
-    match ("?":r1) (_:r2) = match r1 r2
-    match (x1:r1) (x2:r2) | x1 == x2 = match r1 r2
-    match [] _ = True
-    match _ _ = False
-
-
-prop_checkShebangParameters1 = verifyTree checkShebangParameters "#!/usr/bin/env bash -x\necho cow"
-prop_checkShebangParameters2 = verifyNotTree checkShebangParameters "#! /bin/sh  -l "
-checkShebangParameters _ (T_Script id sb _) =
-    [makeComment ErrorC id 2096 "On most OS, shebangs can only specify a single parameter." | length (words sb) > 2]
-
-prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho cow"
-prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh  -l "
-prop_checkShebang3 = verifyTree checkShebang "ls -l"
-checkShebang params (T_Script id sb _) =
-    [makeComment ErrorC id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
-        | not (shellTypeSpecified params) && sb == "" ]
-
-prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
-prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
-prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
-prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
-prop_checkBashisms5 = verify checkBashisms "source file"
-prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
-prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
-prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
-prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
-prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
-prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
-prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
-prop_checkBashisms13= verify checkBashisms "exec -c env"
-prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
-prop_checkBashisms15= verify checkBashisms "let n++"
-prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
-prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
-prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
-prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
-prop_checkBashisms20= verify checkBashisms "read -ra foo"
-prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
-prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
-prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
-prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
-prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
-prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
-prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
-prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
-prop_checkBashisms29= verify checkBashisms "echo ${!var}"
-prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
-prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
-prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
-prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
-prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
-prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
-prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
-prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
-prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
-prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
-prop_checkBashisms40= verify checkBashisms "echo $(<file)"
-prop_checkBashisms41= verify checkBashisms "echo `<file`"
-prop_checkBashisms42= verify checkBashisms "trap foo int"
-prop_checkBashisms43= verify checkBashisms "trap foo sigint"
-prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
-prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
-prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
-checkBashisms params = bashism
-  where
-    isDash = shellType params == Dash
-    warnMsg id s =
-        if isDash
-        then warn id 2169 $ "In dash, " ++ s ++ " not supported."
-        else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
-
-    bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
-    bashism (T_Extglob id _ _) = warnMsg id "extglob is"
-    bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
-    bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
-    bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
-    bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
-    bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
-    bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
-    bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
-    bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
-    bashism (T_HereString id _) = warnMsg id "here-strings are"
-    bashism (TC_Binary id SingleBracket op _ _)
-        | op `elem` [ "-nt", "-ef", "\\<", "\\>"] =
-            unless isDash $ warnMsg id $ op ++ " is"
-    bashism (TC_Binary id SingleBracket "==" _ _) =
-            warnMsg id "== in place of = is"
-    bashism (TC_Unary id _ "-a" _) =
-            warnMsg id "unary -a in place of -e is"
-    bashism (TA_Unary id op _)
-        | op `elem` [ "|++", "|--", "++|", "--|"] =
-            warnMsg id $ filter (/= '|') op ++ " is"
-    bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
-    bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
-    bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
-    bashism (T_IoFile id _ word) | isNetworked =
-            warnMsg id "/dev/{tcp,udp} is"
-        where
-            file = onlyLiteralString word
-            isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
-    bashism (T_Glob id str) | "[^" `isInfixOf` str =
-            warnMsg id "^ in place of ! in glob bracket expressions is"
-
-    bashism t@(TA_Expansion id _) | isBashism =
-        warnMsg id $ fromJust str ++ " is"
-      where
-        str = getLiteralString t
-        isBashism = isJust str && isBashVariable (fromJust str)
-    bashism t@(T_DollarBraced id token) = do
-        mapM_ check expansion
-        when (isBashVariable var) $
-                    warnMsg id $ var ++ " is"
-      where
-        str = bracedString t
-        var = getBracedReference str
-        check (regex, feature) =
-            when (isJust $ matchRegex regex str) $ warnMsg id feature
-
-    bashism t@(T_Pipe id "|&") =
-        warnMsg id "|& in place of 2>&1 | is"
-    bashism (T_Array id _) =
-        warnMsg id "arrays are"
-    bashism (T_IoFile id _ t) | isGlob t =
-        warnMsg id "redirecting to/from globs is"
-    bashism (T_CoProc id _ _) =
-        warnMsg id "coproc is"
-
-    bashism (T_Function id _ _ str _) | not (isVariableName str) =
-        warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
-
-    bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
-        warnMsg id "$(<file) to read files is"
-    bashism (T_Backticked id [x]) | isOnlyRedirection x =
-        warnMsg id "`<file` to read files is"
-
-    bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
-        | t `isCommand` "echo" && "-" `isPrefixOf` argString =
-            unless ("--" `isPrefixOf` argString) $ -- echo "-----"
-                if isDash
-                then
-                    when (argString /= "-n") $
-                        warnMsg (getId arg) "echo flags besides -n"
-                else
-                    warnMsg (getId arg) "echo flags are"
-      where argString = concat $ oversimplify arg
-    bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
-        | t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
-            warnMsg (getId arg) "exec flags are"
-    bashism t@(T_SimpleCommand id _ _)
-        | t `isCommand` "let" = warnMsg id "'let' is"
-
-    bashism t@(T_SimpleCommand id _ (cmd:rest)) =
-        let name = fromMaybe "" $ getCommandName t
-            flags = getLeadingFlags t
-        in do
-            when (name `elem` unsupportedCommands) $
-                warnMsg id $ "'" ++ name ++ "' is"
-            potentially $ do
-                allowed <- Map.lookup name allowedFlags
-                (word, flag) <- listToMaybe $
-                    filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
-                return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
-
-            when (name == "source") $ warnMsg id "'source' in place of '.' is"
-            when (name == "trap") $
-                let
-                    check token = potentially $ do
-                        str <- getLiteralString token
-                        let upper = map toUpper str
-                        return $ do
-                            when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
-                                warnMsg (getId token) $ "trapping " ++ str ++ " is"
-                            when ("SIG" `isPrefixOf` upper) $
-                                warnMsg (getId token)
-                                    "prefixing signal names with 'SIG' is"
-                            when (not isDash && upper /= str) $
-                                warnMsg (getId token)
-                                    "using lower/mixed case for signal names is"
-                in
-                    mapM_ check (drop 1 rest)
-
-            when (name == "printf") $ potentially $ do
-                format <- rest !!! 0  -- flags are covered by allowedFlags
-                let literal = onlyLiteralString format
-                guard $ "%q" `isInfixOf` literal
-                return $ warnMsg (getId format) "printf %q is"
-      where
-        unsupportedCommands = [
-            "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
-            "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
-            "typeset"
-            ] ++ if not isDash then ["local", "type"] else []
-        allowedFlags = Map.fromList [
-            ("read", if isDash then ["r", "p"] else ["r"]),
-            ("ulimit", ["f"]),
-            ("printf", []),
-            ("exec", [])
-            ]
-
-    bashism _ = return ()
-
-    varChars="_0-9a-zA-Z"
-    expansion = let re = mkRegex in [
-        (re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
-        (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
-        (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
-        (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
-        (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
-        (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
-        ]
-    bashVars = [
-        "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
-        "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
-        ]
-    bashDynamicVars = [ "RANDOM", "SECONDS" ]
-    isBashVariable var =
-        var `elem` bashDynamicVars
-        || var `elem` bashVars && not (isAssigned var)
-    isAssigned var = any f (variableFlow params)
-      where
-        f x = case x of
-                Assignment (_, _, name, _) -> name == var
-                _ -> False
-
-
-prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
-prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done"
-prop_checkForInQuoted2a = verifyNot checkForInQuoted "for f in *.mp3; do echo foo; done"
-prop_checkForInQuoted2b = verify checkForInQuoted "for f in \"*.mp3\"; do echo foo; done"
-prop_checkForInQuoted3 = verify checkForInQuoted "for f in 'find /'; do true; done"
-prop_checkForInQuoted4 = verify checkForInQuoted "for f in 1,2,3; do true; done"
-prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done"
-prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done"
-prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done"
-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."
-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 " ++ f ++ "='" ++ s ++ "'."
-checkForInQuoted _ _ = return ()
-
-prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"
-prop_checkForInCat1a= verify checkForInCat "for f in `cat foo`; do stuff; done"
-prop_checkForInCat2 = verify checkForInCat "for f in $(cat foo | grep lol); do stuff; done"
-prop_checkForInCat2a= verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done"
-prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done"
-checkForInCat _ (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w
-  where
-    checkF (T_DollarExpansion id [T_Pipeline _ _ r])
-        | all isLineBased r =
-            info id 2013 "To read lines rather than words, pipe/redirect to a 'while read' loop."
-    checkF (T_Backticked id cmds) = checkF (T_DollarExpansion id cmds)
-    checkF _ = return ()
-    isLineBased cmd = any (cmd `isCommand`)
-                        ["grep", "fgrep", "egrep", "sed", "cat", "awk", "cut", "sort"]
-checkForInCat _ _ = return ()
-
-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 _ = try
-  where
-   try (T_ForIn _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) =
-        check id f x
-   try (T_ForIn _ f [T_NormalWord _ [T_Backticked id [x]]] _) =
-        check id f x
-   try _ = return ()
-   check id f x =
-    case oversimplify x of
-      ("ls":n) ->
-        let warntype = if any ("-" `isPrefixOf`) n then warn else err in
-          warntype id 2045 "Iterating over ls output is fragile. Use globs."
-      ("find":_) -> warn id 2044 "For loops over find output are fragile. Use find -exec or a while read loop."
-      _ -> return ()
-
-
-prop_checkFindExec1 = verify checkFindExec "find / -name '*.php' -exec rm {};"
-prop_checkFindExec2 = verify checkFindExec "find / -exec touch {} && ls {} \\;"
-prop_checkFindExec3 = verify checkFindExec "find / -execdir cat {} | grep lol +"
-prop_checkFindExec4 = verifyNot checkFindExec "find / -name '*.php' -exec foo {} +"
-prop_checkFindExec5 = verifyNot checkFindExec "find / -execdir bash -c 'a && b' \\;"
-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 $
-        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)
-        case getLiteralString w of
-            Just "-exec" -> broken r True
-            Just "-execdir" -> broken r True
-            Just "+" -> broken r False
-            Just ";" -> broken r False
-            _ -> broken r v
-
-    shouldWarn x =
-      case x of
-        T_DollarExpansion _ _ -> True
-        T_Backticked _ _ -> True
-        T_Glob _ _ -> True
-        T_Extglob {} -> True
-        _ -> False
-
-    warnFor x =
-        when(shouldWarn x) $
-            info (getId x) 2014 "This will expand once before find runs, not per file found."
-
-    fromWord (T_NormalWord _ l) = l
-    fromWord _ = []
-checkFindExec _ _ = return ()
-
-
-prop_checkUnquotedExpansions1 = verify checkUnquotedExpansions "rm $(ls)"
-prop_checkUnquotedExpansions1a= verify checkUnquotedExpansions "rm `ls`"
-prop_checkUnquotedExpansions2 = verify checkUnquotedExpansions "rm foo$(date)"
-prop_checkUnquotedExpansions3 = verify checkUnquotedExpansions "[ $(foo) == cow ]"
-prop_checkUnquotedExpansions3a= verify checkUnquotedExpansions "[ ! $(foo) ]"
-prop_checkUnquotedExpansions4 = verifyNot checkUnquotedExpansions "[[ $(foo) == cow ]]"
-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 =
-    check
-  where
-    check t@(T_DollarExpansion _ _) = examine t
-    check t@(T_Backticked _ _) = examine t
-    check t@(T_DollarBraceCommandExpansion _ _) = examine t
-    check _ = return ()
-    tree = parentMap params
-    examine t =
-        unless (isQuoteFree tree t || usedAsCommandName tree t) $
-            warn (getId t) 2046 "Quote this to prevent word splitting."
-
-
-prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo"
-prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/g' > lol"
-prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol"
-prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null"
-prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
-prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
-prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
-checkRedirectToSame params s@(T_Pipeline _ _ list) =
-    mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
-  where
-    note x = makeComment InfoC x 2094
-                "Make sure not to read and write the same file in the same pipeline."
-    checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) =
-        when (exceptId /= newId
-                && x == y
-                && not (isOutput t && isOutput u)
-                && not (special t)
-                && not (any isHarmlessCommand [t,u])) $ do
-            addComment $ note newId
-            addComment $ note exceptId
-    checkOccurrences _ _ = return ()
-    getAllRedirs = concatMap (\t ->
-        case t of
-            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 (oversimplify x)
-    isOutput t =
-        case drop 1 $ getPath (parentMap params) t of
-            T_IoFile _ op _:_ ->
-                case op of
-                    T_Greater _  -> True
-                    T_DGREAT _ -> True
-                    _ -> False
-            _ -> False
-    isHarmlessCommand arg = fromMaybe False $ do
-        cmd <- getClosestCommand (parentMap params) arg
-        name <- getCommandBasename cmd
-        return $ name `elem` ["echo", "printf", "sponge"]
-
-checkRedirectToSame _ _ = return ()
-
-
-prop_checkShorthandIf  = verify checkShorthandIf "[[ ! -z file ]] && scp file host || rm file"
-prop_checkShorthandIf2 = verifyNot checkShorthandIf "[[ ! -z file ]] && { scp file host || echo 'Eek'; }"
-prop_checkShorthandIf3 = verifyNot checkShorthandIf "foo && bar || echo baz"
-prop_checkShorthandIf4 = verifyNot checkShorthandIf "foo && a=b || a=c"
-prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b"
-checkShorthandIf _ (T_AndIf id _ (T_OrIf _ _ (T_Pipeline _ _ t)))
-        | 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
-        name <- getCommandBasename t
-        return $ name `elem` ["echo", "exit", "return", "printf"])
-    isOk _ = False
-checkShorthandIf _ _ = return ()
-
-
-prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done"
-prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"
-checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _)])
-      | bracedString b == "*"  =
-    unless isAssigned $
-        warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."
-  where
-    path = getPath (parentMap p) t
-    isAssigned = any isAssignment . take 2 $ path
-checkDollarStar _ _ = return ()
-
-
-prop_checkUnquotedDollarAt = verify checkUnquotedDollarAt "ls $@"
-prop_checkUnquotedDollarAt1= verifyNot checkUnquotedDollarAt "ls ${#@}"
-prop_checkUnquotedDollarAt2 = verify checkUnquotedDollarAt "ls ${foo[@]}"
-prop_checkUnquotedDollarAt3 = verifyNot checkUnquotedDollarAt "ls ${#foo[@]}"
-prop_checkUnquotedDollarAt4 = verifyNot checkUnquotedDollarAt "ls \"$@\""
-prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }"
-prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@"
-prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done"
-prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\""
-prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}"
-checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word =
-    forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->
-        unless (isAlternative x) $
-            err (getId x) 2068
-                "Double quote array expansions to avoid re-splitting elements."
-  where
-    -- Fixme: should detect whether the alternative is quoted
-    isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b
-    isAlternative _ = False
-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[@]}\""
-prop_checkArrayAsString3 = verify checkArrayAsString "a=*.png"
-prop_checkArrayAsString4 = verify checkArrayAsString "a={1..10}"
-prop_checkArrayAsString5 = verifyNot checkArrayAsString "a='*.gif'"
-prop_checkArrayAsString6 = verifyNot checkArrayAsString "a=$*"
-prop_checkArrayAsString7 = verifyNot checkArrayAsString "a=( $@ )"
-checkArrayAsString _ (T_Assignment id _ _ _ word) =
-    if willConcatInAssignment word
-    then
-      warn (getId word) 2124
-        "Assigning an array to a string! Assign as array, or use * instead of @ to concatenate."
-    else
-      when (willBecomeMultipleArgs word) $
-        warn (getId word) 2125
-          "Brace expansions and globs are literal in assignments. Quote it or use an array."
-checkArrayAsString _ _ = return ()
-
-prop_checkArrayWithoutIndex1 = verifyTree checkArrayWithoutIndex "foo=(a b); echo $foo"
-prop_checkArrayWithoutIndex2 = verifyNotTree checkArrayWithoutIndex "foo='bar baz'; foo=($foo); echo ${foo[0]}"
-prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo"
-prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC"
-prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo $a"
-prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
-checkArrayWithoutIndex params _ =
-    concat $ doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
-  where
-    defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables
-    readF _ (T_DollarBraced id token) _ = do
-        map <- get
-        return . maybeToList $ do
-            name <- getLiteralString token
-            assigned <- Map.lookup name map
-            return [makeComment WarningC id 2128
-                    "Expanding an array without an index only gives the first element."]
-    readF _ _ _ = return []
-
-    writeF _ t name (DataArray _) = do
-        modify (Map.insert name ())
-        return []
-    writeF _ expr name _ = do
-        if isIndexed expr
-          then modify (Map.insert name ())
-          else modify (Map.delete name)
-        return []
-
-    isIndexed expr =
-        case expr of
-            T_Assignment _ _ _ (Just _) _ -> True
-            _ -> False
-
-prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow"
-prop_checkStderrRedirect2 = verifyNot checkStderrRedirect "test > cow 2>&1"
-prop_checkStderrRedirect3 = verifyNot checkStderrRedirect "test 2>&1 > file | grep stderr"
-prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
-prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
-prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
-checkStderrRedirect params redir@(T_Redirecting _ [
-    T_FdRedirect id "2" (T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "1"])),
-    T_FdRedirect _ _ (T_IoFile _ op _)
-    ] _) = case op of
-            T_Greater _ -> error
-            T_DGREAT _ -> error
-            _ -> return ()
-  where
-    usesOutput t =
-        case t of
-            (T_Pipeline _ _ list) -> length list > 1 && not (isParentOf (parentMap params) (last list) redir)
-            (T_ProcSub {}) -> True
-            (T_DollarExpansion {}) -> True
-            (T_Backticked {}) -> True
-            _ -> False
-    isCaptured = any usesOutput $ getPath (parentMap params) redir
-
-    error = unless isCaptured $
-        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 = trace ("FAILURE " ++ show t)
-
-
-prop_checkSingleQuotedVariables  = verify checkSingleQuotedVariables "echo '$foo'"
-prop_checkSingleQuotedVariables2 = verify checkSingleQuotedVariables "echo 'lol$1.jpg'"
-prop_checkSingleQuotedVariables3 = verifyNot checkSingleQuotedVariables "sed 's/foo$/bar/'"
-prop_checkSingleQuotedVariables3a= verify checkSingleQuotedVariables "sed 's/${foo}/bar/'"
-prop_checkSingleQuotedVariables3b= verify checkSingleQuotedVariables "sed 's/$(echo cow)/bar/'"
-prop_checkSingleQuotedVariables3c= verify checkSingleQuotedVariables "sed 's/$((1+foo))/bar/'"
-prop_checkSingleQuotedVariables4 = verifyNot checkSingleQuotedVariables "awk '{print $1}'"
-prop_checkSingleQuotedVariables5 = verifyNot checkSingleQuotedVariables "trap 'echo $SECONDS' EXIT"
-prop_checkSingleQuotedVariables6 = verifyNot checkSingleQuotedVariables "sed -n '$p'"
-prop_checkSingleQuotedVariables6a= verify checkSingleQuotedVariables "sed -n '$pattern'"
-prop_checkSingleQuotedVariables7 = verifyNot checkSingleQuotedVariables "PS1='$PWD \\$ '"
-prop_checkSingleQuotedVariables8 = verify checkSingleQuotedVariables "find . -exec echo '$1' {} +"
-prop_checkSingleQuotedVariables9 = verifyNot checkSingleQuotedVariables "find . -exec awk '{print $1}' {} \\;"
-prop_checkSingleQuotedVariables10= verify checkSingleQuotedVariables "echo '`pwd`'"
-checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
-    when (s `matches` re) $
-        if "sed" == commandName
-        then unless (s `matches` sedContra) showMessage
-        else unless isProbablyOk showMessage
-  where
-    parents = parentMap params
-    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 $ if name == "find" then getFindCommand cmd else name
-
-    isProbablyOk =
-            any isOkAssignment (take 3 $ getPath parents t)
-            || commandName `elem` [
-                "trap"
-                ,"sh"
-                ,"bash"
-                ,"ksh"
-                ,"zsh"
-                ,"ssh"
-                ,"xprop"
-                ,"alias"
-                ]
-            || "awk" `isSuffixOf` commandName
-            || "perl" `isPrefixOf` commandName
-
-    commonlyQuoted = ["PS1", "PS2", "PS3", "PS4", "PROMPT_COMMAND"]
-    isOkAssignment t =
-        case t of
-            T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted
-            otherwise -> False
-
-    re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`"
-    sedContra = mkRegex "\\$[dpsaic]($|[^a-zA-Z])"
-
-    getFindCommand (T_SimpleCommand _ _ words) =
-        let list = map getLiteralString words
-            cmd  = dropWhile (\x -> x /= Just "-exec" && x /= Just "-execdir") list
-        in
-          case cmd of
-            (flag:cmd:rest) -> fromMaybe "find" cmd
-            _ -> "find"
-    getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
-    getFindCommand _ = "find"
-checkSingleQuotedVariables _ _ = return ()
-
-
-prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
-prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
-prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
-prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
-checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
-       err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
-checkUnquotedN _ _ = return ()
-
-prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"
-prop_checkNumberComparisons2 = verify checkNumberComparisons "[[ 0 >= $(cmd) ]]"
-prop_checkNumberComparisons3 = verifyNot checkNumberComparisons "[[ $foo ]] > 3"
-prop_checkNumberComparisons4 = verify checkNumberComparisons "[[ $foo > 2.72 ]]"
-prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]"
-prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]"
-prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]"
-prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]"
-prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"
-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)
-      then do
-        when (isLtGt op) $
-          err id 2071 $
-            op ++ " is for string comparisons. Use " ++ eqv op ++ " instead."
-        when (isLeGe op) $
-            err id 2071 $ op ++ " is not a valid operator. " ++
-              "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."
-
-    when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do
-        mapM_ checkDecimals [lhs, rhs]
-        when (typ == SingleBracket) $
-            checkStrings [lhs, rhs]
-
-  where
-      isLtGt = flip elem ["<", "\\<", ">", "\\>"]
-      isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="]
-
-      checkDecimals hs =
-        when (isFraction hs && not (hasFloatingPoint params)) $
-            err (getId hs) 2072 decimalError
-      decimalError = "Decimals are not supported. " ++
-        "Either use integers only, or use bc or awk to compare."
-
-      checkStrings hs =
-        mapM_ stringError . take 1 . filter isNonNum $ hs
-
-      isNonNum t = fromMaybe False $ do
-        s <- getLiteralStringExt (const $ return "") t
-        return . not . all numChar $ s
-      numChar x = isDigit x || x `elem` "+-. "
-
-      stringError t = err (getId t) 2170 $
-          "Numerical " ++ op ++ " does not dereference in [..]. Expand or use string operator."
-
-      isNum t =
-        case oversimplify t of
-            [v] -> all isDigit v
-            _ -> False
-      isFraction t =
-        case oversimplify t of
-            [v] -> isJust $ matchRegex floatRegex v
-            _ -> False
-
-      eqv ('\\':s) = eqv s
-      eqv "<" = "-lt"
-      eqv ">" = "-gt"
-      eqv "<=" = "-le"
-      eqv ">=" = "-ge"
-      eqv _ = "the numerical equivalent"
-
-      esc = if typ == SingleBracket then "\\" else ""
-      seqv "-ge" = "! a " ++ esc ++ "< b"
-      seqv "-gt" = esc ++ ">"
-      seqv "-le" = "! a " ++ esc ++ "> b"
-      seqv "-lt" = esc ++ "<"
-      seqv "-eq" = "="
-      seqv "-ne" = "!="
-      seqv _ = "the string equivalent"
-
-      invert ('\\':s) = invert s
-      invert "<=" = ">"
-      invert ">=" = "<"
-
-      floatRegex = mkRegex "^[0-9]+\\.[0-9]+$"
-checkNumberComparisons _ _ = return ()
-
-prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]"
-prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]"
-prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]"
-prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done"
-checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)
-    | typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] =
-        err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]."
-checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)
-    | typ == SingleBracket && op == "=~" =
-        err id 2074 $ "Can't use " ++ op ++" in [ ]. Use [[..]] instead."
-checkSingleBracketOperators _ _ = return ()
-
-prop_checkDoubleBracketOperators1 = verify checkDoubleBracketOperators "[[ 3 \\< 4 ]]"
-prop_checkDoubleBracketOperators3 = verifyNot checkDoubleBracketOperators "[[ foo < bar ]]"
-checkDoubleBracketOperators _ x@(TC_Binary id typ op lhs rhs)
-    | typ == DoubleBracket && op `elem` ["\\<", "\\>", "\\<=", "\\>="] =
-        err id 2075 $ "Escaping " ++ op ++" is required in [..], but invalid in [[..]]"
-checkDoubleBracketOperators _ _ = return ()
-
-prop_checkConditionalAndOrs1 = verify checkConditionalAndOrs "[ foo && bar ]"
-prop_checkConditionalAndOrs2 = verify checkConditionalAndOrs "[[ foo -o bar ]]"
-prop_checkConditionalAndOrs3 = verifyNot checkConditionalAndOrs "[[ foo || bar ]]"
-prop_checkConditionalAndOrs4 = verify checkConditionalAndOrs "[ foo -a bar ]"
-prop_checkConditionalAndOrs5 = verify checkConditionalAndOrs "[ -z 3 -o a = b ]"
-checkConditionalAndOrs _ t =
-    case t of
-        (TC_And id SingleBracket "&&" _ _) ->
-            err id 2107 "Instead of [ a && b ], use [ a ] && [ b ]."
-        (TC_And id DoubleBracket "-a" _ _) ->
-            err id 2108 "In [[..]], use && instead of -a."
-        (TC_Or id SingleBracket "||" _ _) ->
-            err id 2109 "Instead of [ a || b ], use [ a ] || [ b ]."
-        (TC_Or id DoubleBracket "-o" _ _) ->
-            err id 2110 "In [[..]], use || instead of -o."
-
-        (TC_And id SingleBracket "-a" _ _) ->
-            warn id 2166 "Prefer [ p ] && [ q ] as [ p -a q ] is not well defined."
-        (TC_Or id SingleBracket "-o" _ _) ->
-            warn id 2166 "Prefer [ p ] || [ q ] as [ p -o q ] is not well defined."
-
-        otherwise -> return ()
-
-prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]"
-prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]"
-prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"
-prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"
-prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]"
-checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
-    case rhs of
-        T_NormalWord id [T_DoubleQuoted _ _] -> error rhs
-        T_NormalWord id [T_SingleQuoted _ _] -> error rhs
-        _ -> return ()
-  where
-    error t =
-        unless (isConstantNonRe t) $
-            err (getId t) 2076
-                "Don't quote rhs of =~, it'll match literally rather than as a regex."
-    re = mkRegex "[][*.+()]"
-    hasMetachars s = s `matches` re
-    isConstantNonRe t = fromMaybe False $ do
-        s <- getLiteralString t
-        return . not $ hasMetachars s
-checkQuotedCondRegex _ _ = return ()
-
-prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
-prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
-prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
-prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
-prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
-checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
-    let s = concat $ oversimplify rhs in
-        when (isConfusedGlobRegex s) $
-            warn (getId rhs) 2049 "=~ is for regex. Use == for globs."
-checkGlobbedRegex _ _ = return ()
-
-
-prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]"
-prop_checkConstantIfs2a= verify checkConstantIfs "[ n -le 4 ]"
-prop_checkConstantIfs2b= verifyNot checkConstantIfs "[[ n -le 4 ]]"
-prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n != 2 ]]"
-prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"
-prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"
-prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]"
-prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"
-checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
-    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
-    isDynamic =
-        op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
-            && typ == DoubleBracket
-        || op `elem` [ "-nt", "-ot", "-ef"]
-checkConstantIfs _ _ = return ()
-
-prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]"
-prop_checkLiteralBreakingTest2 = verify checkLiteralBreakingTest "[ $foo=3 ]"
-prop_checkLiteralBreakingTest3 = verify checkLiteralBreakingTest "[ $foo!=3 ]"
-prop_checkLiteralBreakingTest4 = verify checkLiteralBreakingTest "[ \"$(ls) \" ]"
-prop_checkLiteralBreakingTest5 = verify checkLiteralBreakingTest "[ -n \"$(true) \" ]"
-prop_checkLiteralBreakingTest6 = verify checkLiteralBreakingTest "[ -z $(true)z ]"
-prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]"
-prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]"
-prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"
-checkLiteralBreakingTest _ t = potentially $
-        case t of
-            (TC_Noary _ _ w@(T_NormalWord _ l)) -> do
-                guard . not $ isConstant w -- Covered by SC2078
-                comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings."
-            (TC_Unary _ _ op w@(T_NormalWord _ l)) ->
-                case op of
-                    "-n" -> tautologyWarning w "Argument to -n is always true due to literal strings."
-                    "-z" -> tautologyWarning w "Argument to -z is always false due to literal strings."
-                    _ -> fail "not relevant"
-            _ -> fail "not my problem"
-  where
-    hasEquals = matchToken ('=' `elem`)
-    isNonEmpty = matchToken (not . null)
-    matchToken m t = isJust $ do
-        str <- getLiteralString t
-        guard $ m str
-        return ()
-
-    comparisonWarning list = do
-        token <- listToMaybe $ filter hasEquals list
-        return $ err (getId token) 2077 "You need spaces around the comparison operator."
-    tautologyWarning t s = do
-        token <- listToMaybe $ filter isNonEmpty $ getWordParts t
-        return $ err (getId token) 2157 s
-
-prop_checkConstantNoary = verify checkConstantNoary "[[ '$(foo)' ]]"
-prop_checkConstantNoary2 = verify checkConstantNoary "[ \"-f lol\" ]"
-prop_checkConstantNoary3 = verify checkConstantNoary "[[ cmd ]]"
-prop_checkConstantNoary4 = verify checkConstantNoary "[[ ! cmd ]]"
-prop_checkConstantNoary5 = verify checkConstantNoary "[[ true ]]"
-prop_checkConstantNoary6 = verify checkConstantNoary "[ 1 ]"
-prop_checkConstantNoary7 = verify checkConstantNoary "[ false ]"
-checkConstantNoary _ (TC_Noary _ _ t) | isConstant t =
-    case fromMaybe "" $ getLiteralString t of
-        "false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets."
-        "0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead."
-        "true" -> style (getId t) 2160 "Instead of '[ true ]', just use 'true'."
-        "1" -> style (getId t) 2161 "Instead of '[ 1 ]', use 'true'."
-        _ -> err (getId t) 2078 "This expression is constant. Did you forget a $ somewhere?"
-  where
-    string = fromMaybe "" $ getLiteralString t
-
-checkConstantNoary _ _ = return ()
-
-prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
-prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
-checkBraceExpansionVars _ (T_BraceExpansion id list) = mapM_ check list
-  where
-    check element =
-        when ("..$" `isInfixOf` toString element) $
-            warn id 2051 "Bash doesn't support variables in brace range expansions."
-    literalExt t =
-        case t of
-            T_DollarBraced {} -> return "$"
-            T_DollarExpansion {} -> return "$"
-            T_DollarArithmetic {} -> return "$"
-            otherwise -> return "-"
-    toString t = fromJust $ getLiteralStringExt literalExt t
-checkBraceExpansionVars _ _ = return ()
-
-prop_checkForDecimals = verify checkForDecimals "((3.14*c))"
-checkForDecimals params t@(TA_Expansion id _) = potentially $ do
-    guard $ not (hasFloatingPoint params)
-    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))"
-prop_checkDivBeforeMult3 = verifyNot checkDivBeforeMult "echo $((c/10*10))"
-checkDivBeforeMult params (TA_Binary _ "*" (TA_Binary id "/" _ x) y)
-    | not (hasFloatingPoint params) && x /= y =
-        info id 2017 "Increase precision by replacing a/b*c with a*c/b."
-checkDivBeforeMult _ _ = return ()
-
-prop_checkArithmeticDeref = verify checkArithmeticDeref "echo $((3+$foo))"
-prop_checkArithmeticDeref2 = verify checkArithmeticDeref "cow=14; (( s+= $cow ))"
-prop_checkArithmeticDeref3 = verifyNot checkArithmeticDeref "cow=1/40; (( s+= ${cow%%/*} ))"
-prop_checkArithmeticDeref4 = verifyNot checkArithmeticDeref "(( ! $? ))"
-prop_checkArithmeticDeref5 = verifyNot checkArithmeticDeref "(($1))"
-prop_checkArithmeticDeref6 = verify checkArithmeticDeref "(( a[$i] ))"
-prop_checkArithmeticDeref7 = verifyNot checkArithmeticDeref "(( 10#$n ))"
-prop_checkArithmeticDeref8 = verifyNot checkArithmeticDeref "let i=$i+1"
-prop_checkArithmeticDeref9 = verifyNot checkArithmeticDeref "(( a[foo] ))"
-prop_checkArithmeticDeref10= verifyNot checkArithmeticDeref "(( a[\\$foo] ))"
-prop_checkArithmeticDeref11= verifyNot checkArithmeticDeref "a[$foo]=wee"
-prop_checkArithmeticDeref12= verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done"
-prop_checkArithmeticDeref13= verifyNot checkArithmeticDeref "(( $$ ))"
-checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
-    unless (isException $ bracedString b) getWarning
-  where
-    isException [] = True
-    isException s = any (`elem` "/.:#%?*@$") s || isDigit (head s)
-    getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t
-    warningFor t =
-        case t of
-            T_Arithmetic {} -> return normalWarning
-            T_DollarArithmetic {} -> return normalWarning
-            T_ForArithmetic {} -> return normalWarning
-            TA_Index {} -> return indexWarning
-            T_SimpleCommand {} -> return noWarning
-            _ -> Nothing
-
-    normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
-    indexWarning = style id 2149 "Remove $/${} for numeric index, or escape it for string."
-    noWarning = return ()
-checkArithmeticDeref _ _ = return ()
-
-prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))"
-prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))"
-prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))"
-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 ]]"
-prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow == \"$bar\" ]]"
-prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
-prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
-prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
-checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
-    | op `elem` ["=", "==", "!="] =
-        warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
-checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
-        | (op == "=" || op == "==") && isGlob word =
-    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
-checkCommarrays _ _ = return ()
-
-prop_checkOrNeq1 = verify checkOrNeq "if [[ $lol -ne cow || $lol -ne foo ]]; then echo foo; fi"
-prop_checkOrNeq2 = verify checkOrNeq "(( a!=lol || a!=foo ))"
-prop_checkOrNeq3 = verify checkOrNeq "[ \"$a\" != lol || \"$a\" != foo ]"
-prop_checkOrNeq4 = verifyNot checkOrNeq "[ a != $cow || b != $foo ]"
--- This only catches the most idiomatic cases. Fixme?
-checkOrNeq _ (TC_Or id typ op (TC_Binary _ _ op1 word1 _) (TC_Binary _ _ op2 word2 _))
-    | word1 == word2 && (op1 == op2 && (op1 == "-ne" || op1 == "!=")) =
-        warn id 2055 $ "You probably wanted " ++ (if typ == SingleBracket then "-a" else "&&") ++ " here."
-checkOrNeq _ (TA_Binary id "||" (TA_Binary _ "!=" word1 _) (TA_Binary _ "!=" word2 _))
-    | word1 == word2 =
-        warn id 2056 "You probably wanted && here."
-checkOrNeq _ _ = return ()
-
-
-prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]"
-prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]"
-prop_checkValidCondOps2a= verifyNot checkValidCondOps "[ 3 \\> 2 ]"
-prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]"
-prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]"
-checkValidCondOps _ (TC_Binary id _ s _ _)
-    | s `notElem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="] =
-        warn id 2057 "Unknown binary operator."
-checkValidCondOps _ (TC_Unary id _ s _)
-    | 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 ()
-
---- Context seeking
-
-getParentTree t =
-    snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
-  where
-    pre t = modify (first ((:) t))
-    post t = do
-        (_:rest, map) <- get
-        case rest of [] -> put (rest, map)
-                     (x:_) -> put (rest, Map.insert (getId t) x map)
-
-getTokenMap t =
-    execState (doAnalysis f t) Map.empty
-  where
-    f t = modify (Map.insert (getId t) t)
-
-
--- Is this node self quoting for a regular element?
-isQuoteFree = isQuoteFreeNode False
-
--- Is this node striclty self quoting, for array expansions
-isStrictlyQuoteFree = isQuoteFreeNode True
-
-
-isQuoteFreeNode strict tree t =
-    (isQuoteFreeElement t == Just True) ||
-        head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
-  where
-    -- Is this node self-quoting in itself?
-    isQuoteFreeElement t =
-        case t of
-            T_Assignment {} -> return True
-            T_FdRedirect {} -> return True
-            _ -> Nothing
-
-    -- Are any subnodes inherently self-quoting?
-    isQuoteFreeContext t =
-        case t of
-            TC_Noary _ DoubleBracket _ -> return True
-            TC_Unary _ DoubleBracket _ _ -> return True
-            TC_Binary _ DoubleBracket _ _ _ -> return True
-            TA_Sequence {} -> return True
-            T_Arithmetic {} -> return True
-            T_Assignment {} -> return True
-            T_Redirecting {} -> return $
-                if strict then False else
-                    -- Not true, just a hack to prevent warning about non-expansion refs
-                    any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
-            T_DoubleQuoted _ _ -> return True
-            T_DollarDoubleQuoted _ _ -> return True
-            T_CaseExpression {} -> return True
-            T_HereDoc {} -> return True
-            T_DollarBraced {} -> return True
-            -- When non-strict, pragmatically assume it's desirable to split here
-            T_ForIn {} -> return (not strict)
-            T_SelectIn {} -> return (not strict)
-            _ -> Nothing
-
-isParamTo tree cmd =
-    go
-  where
-    go x = case Map.lookup (getId x) tree of
-                Nothing -> False
-                Just parent -> check parent
-    check t =
-        case t of
-            T_SingleQuoted _ _ -> go t
-            T_DoubleQuoted _ _ -> go t
-            T_NormalWord _ _ -> go t
-            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 _ = 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 _ _ = False
-
--- A list of the element and all its parents
-getPath tree t = t :
-    case Map.lookup (getId t) tree of
-        Nothing -> []
-        Just parent -> getPath tree parent
-
-isParentOf tree parent child =
-    elem (getId parent) . map getId $ getPath tree child
-
-parents params = getPath (parentMap params)
-
---- Command specific checks
-
-checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
-    when (t `isCommand` str) $ f cmd rest
-checkCommand _ _ _ = return ()
-
-checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
-    when (t `isUnqualifiedCommand` str) $ f cmd rest
-checkUnqualifiedCommand _ _ _ = return ()
-
-isCommand token str = isCommandMatch token (\cmd -> cmd  == str || ('/' : str) `isSuffixOf` cmd)
-isUnqualifiedCommand token str = isCommandMatch token (== str)
-
-isCommandMatch token matcher = fromMaybe False $ do
-    cmd <- getCommandName token
-    return $ matcher cmd
-
-prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
-prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
-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 (format:params) = check format
-    f _ = return ()
-    check format =
-        unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
-          warn (getId format) 2059
-              "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
-
-
--- Check whether a word is entirely output from a single command
-tokenIsJustCommandOutput t = case t of
-    T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
-    T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
-    T_NormalWord id [T_Backticked _ cmds] -> check cmds
-    T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
-    _ -> False
-  where
-    check [x] = not $ isOnlyRedirection x
-    check _ = False
-
-prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
-prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
-prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
-prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
-prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
-prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
-checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where
-    msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
-    f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
-    f _ = return ()
-
-prop_checkUuoeVar1 = verify checkUuoeVar "for f in $(echo $tmp); do echo lol; done"
-prop_checkUuoeVar2 = verify checkUuoeVar "date +`echo \"$format\"`"
-prop_checkUuoeVar3 = verifyNot checkUuoeVar "foo \"$(echo -e '\r')\""
-prop_checkUuoeVar4 = verifyNot checkUuoeVar "echo $tmp"
-prop_checkUuoeVar5 = verify checkUuoeVar "foo \"$(echo \"$(date) value:\" $value)\""
-prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\""
-prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005
-prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)"
-prop_checkUuoeVar9 = verify checkUuoeVar "foo $(echo $(<file))"
-checkUuoeVar _ p =
-    case p of
-        T_Backticked id [cmd] -> check id cmd
-        T_DollarExpansion id [cmd] -> check id cmd
-        _ -> return ()
-  where
-    couldBeOptimized f = case f of
-        T_Glob {} -> False
-        T_Extglob {} -> False
-        T_BraceExpansion {} -> False
-        T_NormalWord _ l -> all couldBeOptimized l
-        T_DoubleQuoted _ l -> all couldBeOptimized l
-        _ -> True
-
-    check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c
-    check _ _ = return ()
-    isCovered first rest = null rest && tokenIsJustCommandOutput first
-    warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars ->
-        case vars of
-          (first:rest) ->
-            unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $
-                when (all couldBeOptimized vars) $ style id 2116
-                    "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."
-          otherwise -> return ()
-
-prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
-prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
-prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
-prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
-prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
-prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
-prop_checkTr4 = verifyNot checkTr "ls [a-z]"
-prop_checkTr5 = verify checkTr "tr foo bar"
-prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
-prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
-prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
-prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
-prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
-checkTr _ = checkCommand "tr" (const $ mapM_ f)
-  where
-    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) $
-            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) && ('*' `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 relevant /= nub relevant
-
-
-prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
-prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
-prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
-checkFindNameGlob _ = checkCommand "find" (const f) where
-    acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
-    acceptsGlob _ = False
-    f [] = return ()
-    f [x] = return ()
-    f (a:b:r) = do
-        when (acceptsGlob (getLiteralString a) && isGlob b) $ do
-            let (Just s) = getLiteralString a
-            warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
-        f (b:r)
-
-
-prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
-prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
-prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
-prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
-prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v  --regex=moo *"
-prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
-prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
-prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
-prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
-prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
-prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
-
-checkGrepRe _ = checkCommand "grep" (const f) where
-    -- --regex=*(extglob) doesn't work. Fixme?
-    skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
-    skippable _ = False
-    f [] = return ()
-    f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
-    f (re:_) = do
-        when (isGlob re) $
-            warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
-        let string = concat $ oversimplify re
-        if isConfusedGlobRegex string then
-            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 ++ "'."
-
-wordStartingWith c =
-    head . filter ([c] `isPrefixOf`) $ candidates
-  where
-    candidates =
-        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"
-prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
-prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
-checkTrapQuotes _ = checkCommand "trap" (const f) where
-    f (x:_) = checkTrap x
-    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."
-    checkExpansions (T_DollarExpansion id _) = warning id
-    checkExpansions (T_Backticked id _) = warning id
-    checkExpansions (T_DollarBraced id _) = warning id
-    checkExpansions (T_DollarArithmetic id _) = warning id
-    checkExpansions _ = return ()
-
-prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
-prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
-prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
-checkTimeParameters _ = checkUnqualifiedCommand "time" f where
-    f cmd (x:_) = let s = concat $ oversimplify x in
-                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."
-    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"
-prop_checkTestRedirects4 = verifyNot checkTestRedirects "test 1 -eq 2 2> file"
-checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
-    mapM_ check redirs
-  where
-    check t =
-        when (suspicious t) $
-            warn (getId t) 2065 "This is interpreted as a shell file redirection, not a comparison."
-    suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors,
-        case t of  -- and >> and similar redirections because these are probably not comparisons.
-            T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op
-            otherwise -> False
-    isComparison t =
-        case t of
-            T_Greater _ -> True
-            T_Less _ -> True
-            otherwise -> False
-checkTestRedirects _ _ = return ()
-
-prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
-prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
-prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
-prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
-prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
-prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
-prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
-checkSudoRedirect _ (T_Redirecting _ redirs cmd) | cmd `isCommand` "sudo" =
-    mapM_ warnAbout redirs
-  where
-    warnAbout (T_FdRedirect _ s (T_IoFile id op file))
-        | (s == "" || s == "&") && not (special file) =
-        case op of
-            T_Less _ ->
-              info (getId op) 2024
-                "sudo doesn't affect redirects. Use sudo cat file | .."
-            T_Greater _ ->
-              warn (getId op) 2024
-                "sudo doesn't affect redirects. Use ..| sudo tee file"
-            T_DGREAT _ ->
-              warn (getId op) 2024
-                "sudo doesn't affect redirects. Use .. | sudo tee -a file"
-            _ -> return ()
-    warnAbout _ = return ()
-    special file = concat (oversimplify file) == "/dev/null"
-checkSudoRedirect _ _ = return ()
-
-prop_checkReturn1 = verifyNot checkReturn "return"
-prop_checkReturn2 = verifyNot checkReturn "return 1"
-prop_checkReturn3 = verifyNot checkReturn "return $var"
-prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
-prop_checkReturn5 = verify checkReturn "return -1"
-prop_checkReturn6 = verify checkReturn "return 1000"
-prop_checkReturn7 = verify checkReturn "return 'hello world'"
-checkReturn _ = checkCommand "return" (const f)
-  where
-    f (first:second:_) =
-        err (getId second) 2151
-            "Only one integer 0-255 can be returned. Use stdout for other data."
-    f [value] =
-        when (isInvalid $ literal value) $
-            err (getId value) 2152
-                "Can only return 0-255. Other data should be written to stdout."
-    f _ = return ()
-
-    isInvalid s = s == "" || any (not . isDigit) s || length s > 5
-        || let value = (read s :: Integer) in value > 255
-
-    literal token = fromJust $ getLiteralStringExt lit token
-    lit (T_DollarBraced {}) = return "0"
-    lit (T_DollarArithmetic {}) = return "0"
-    lit (T_DollarExpansion {}) = return "0"
-    lit (T_Backticked {}) = return "0"
-    lit _ = return "WTF"
-
-
-prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
-prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
-prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
-prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
-prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
-prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
-prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
-prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
-prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
-prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
-checkPS1Assignments _ (T_Assignment _ _ "PS1" _ word) = warnFor word
-  where
-    warnFor word =
-        let contents = concat $ oversimplify word in
-            when (containsUnescaped contents) $
-                info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"
-    containsUnescaped s =
-        let unenclosed = subRegex enclosedRegex s "" in
-           isJust $ matchRegex escapeRegex unenclosed
-    enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
-    escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
-checkPS1Assignments _ _ = return ()
-
-prop_checkBackticks1 = verify checkBackticks "echo `foo`"
-prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
-prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
-checkBackticks _ (T_Backticked id list) | not (null list) =
-    style id 2006 "Use $(..) instead of legacy `..`."
-checkBackticks _ _ = return ()
-
-prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
-prop_checkIndirectExpansion2 = verifyNot checkIndirectExpansion "${foo//$n/lol}"
-prop_checkIndirectExpansion3 = verify checkIndirectExpansion "${$#}"
-prop_checkIndirectExpansion4 = verify checkIndirectExpansion "${var${n}_$((i%2))}"
-prop_checkIndirectExpansion5 = verifyNot checkIndirectExpansion "${bar}"
-checkIndirectExpansion _ (T_DollarBraced i (T_NormalWord _ contents)) =
-    when (isIndirection contents) $
-        err i 2082 "To expand via indirection, use arrays, ${!name} or (for sh only) eval."
-  where
-    isIndirection vars =
-        let list = mapMaybe isIndirectionPart vars in
-            not (null list) && and list
-    isIndirectionPart t =
-        case t of T_DollarExpansion _ _ ->  Just True
-                  T_Backticked _ _ ->       Just True
-                  T_DollarBraced _ _ ->     Just True
-                  T_DollarArithmetic _ _ -> Just True
-                  T_Literal _ s -> if all isVariableChar s
-                                    then Nothing
-                                    else Just False
-                  _ -> Just False
-
-checkIndirectExpansion _ _ = return ()
-
-prop_checkInexplicablyUnquoted1 = verify checkInexplicablyUnquoted "echo 'var='value';'"
-prop_checkInexplicablyUnquoted2 = verifyNot checkInexplicablyUnquoted "'foo'*"
-prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --user-agent='something'"
-prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\""
-prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
-prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\""
-checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
-  where
-    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'? "
-
-    check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) =
-        case trapped of
-            T_DollarExpansion id _ -> warnAboutExpansion id
-            T_DollarBraced id _ -> warnAboutExpansion id
-            T_Literal id s ->
-                unless (quotesSingleThing a && quotesSingleThing b) $
-                    warnAboutLiteral id
-            _ -> return ()
-
-    check _ = return ()
-
-    -- If the surrounding quotes quote single things, like "$foo"_and_then_some_"$stuff",
-    -- the quotes were probably intentional and harmless.
-    quotesSingleThing x = case x of
-        [T_DollarExpansion _ _] -> True
-        [T_DollarBraced _ _] -> True
-        [T_Backticked _ _] -> True
-        _ -> False
-
-    warnAboutExpansion id =
-        warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them."
-    warnAboutLiteral id =
-        warn id 2140 "Word is on the form \"A\"B\"C\" (B indicated). Did you mean \"ABC\" or \"A\\\"B\\\"C\"?"
-checkInexplicablyUnquoted _ _ = return ()
-
-prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""
-prop_checkTildeInQuotes2 = verify checkTildeInQuotes "foo > '~/dir'"
-prop_checkTildeInQuotes4 = verifyNot checkTildeInQuotes "~/file"
-prop_checkTildeInQuotes5 = verifyNot checkTildeInQuotes "echo '/~foo/cow'"
-prop_checkTildeInQuotes6 = verifyNot checkTildeInQuotes "awk '$0 ~ /foo/'"
-checkTildeInQuotes _ = check
-  where
-    verify id ('~':'/':_) = warn id 2088 "Tilde does not expand in quotes. Use $HOME."
-    verify _ _ = return ()
-    check (T_NormalWord _ (T_SingleQuoted id str:_)) =
-        verify id str
-    check (T_NormalWord _ (T_DoubleQuoted _ (T_Literal id str:_):_)) =
-        verify id str
-    check _ = return ()
-
-prop_checkLonelyDotDash1 = verify checkLonelyDotDash "./ file"
-prop_checkLonelyDotDash2 = verifyNot checkLonelyDotDash "./file"
-checkLonelyDotDash _ t@(T_Redirecting id _ _)
-    | isUnqualifiedCommand t "./" =
-        err id 2083 "Don't add spaces after the slash in './file'."
-checkLonelyDotDash _ _ = return ()
-
-
-prop_checkSpuriousExec1 = verify checkSpuriousExec "exec foo; true"
-prop_checkSpuriousExec2 = verify checkSpuriousExec "if a; then exec b; exec c; fi"
-prop_checkSpuriousExec3 = verifyNot checkSpuriousExec "echo cow; exec foo"
-prop_checkSpuriousExec4 = verifyNot checkSpuriousExec "if a; then exec b; fi"
-prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"
-prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
-prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
-checkSpuriousExec _ = doLists
-  where
-    doLists (T_Script _ _ cmds) = doList cmds
-    doLists (T_BraceGroup _ cmds) = doList cmds
-    doLists (T_WhileExpression _ _ cmds) = doList cmds
-    doLists (T_UntilExpression _ _ cmds) = doList cmds
-    doLists (T_ForIn _ _ _ cmds) = doList cmds
-    doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds
-    doLists (T_IfExpression _ thens elses) = do
-        mapM_ (\(_, l) -> doList l) thens
-        doList elses
-    doLists _ = return ()
-
-    stripCleanup = reverse . dropWhile cleanup . reverse
-    cleanup (T_Pipeline _ _ [cmd]) =
-        isCommandMatch cmd (`elem` ["echo", "exit"])
-    cleanup _ = False
-
-    doList = doList' . stripCleanup
-    doList' t@(current:following:_) = do
-        commentIfExec current
-        doList (tail t)
-    doList' _ = return ()
-
-    commentIfExec (T_Pipeline id _ list) =
-      mapM_ commentIfExec $ take 1 list
-    commentIfExec (T_Redirecting _ _ f@(
-      T_SimpleCommand id _ (cmd:arg:_))) =
-        when (f `isUnqualifiedCommand` "exec") $
-          warn id 2093
-            "Remove \"exec \" if script should continue after this command."
-    commentIfExec _ = return ()
-
-
-prop_checkSpuriousExpansion1 = verify checkSpuriousExpansion "if $(true); then true; fi"
-prop_checkSpuriousExpansion2 = verify checkSpuriousExpansion "while \"$(cmd)\"; do :; done"
-prop_checkSpuriousExpansion3 = verifyNot checkSpuriousExpansion "$(cmd) --flag1 --flag2"
-prop_checkSpuriousExpansion4 = verify checkSpuriousExpansion "$((i++))"
-checkSpuriousExpansion _ (T_SimpleCommand _ _ [T_NormalWord _ [word]]) = check word
-  where
-    check word = case word of
-        T_DollarExpansion id _ ->
-            warn id 2091 "Remove surrounding $() to avoid executing output."
-        T_Backticked id _ ->
-            warn id 2092 "Remove backticks to avoid executing output."
-        T_DollarArithmetic id _ ->
-            err id 2084 "Remove '$' or use '_=$((expr))' to avoid executing output."
-        T_DoubleQuoted id [subword] -> check subword
-        _ -> return ()
-checkSpuriousExpansion _ _ = return ()
-
-
-prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
-prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
-prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
-prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
-prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
-checkUnusedEchoEscapes _ = checkCommand "echo" (const f)
-  where
-    isDashE = mkRegex "^-.*e"
-    hasEscapes = mkRegex "\\\\[rnt]"
-    f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
-        return ()
-      where allButLast = reverse . drop 1 . reverse $ args
-    f args = mapM_ checkEscapes args
-
-    checkEscapes (T_NormalWord _ args) =
-        mapM_ checkEscapes args
-    checkEscapes (T_DoubleQuoted id args) =
-        mapM_ checkEscapes args
-    checkEscapes (T_Literal id str) = examine id str
-    checkEscapes (T_SingleQuoted id str) = examine id str
-    checkEscapes _ = return ()
-
-    examine id str =
-        when (str `matches` hasEscapes) $
-            info id 2028 "echo won't expand escape sequences. Consider printf."
-
-
-prop_checkDollarBrackets1 = verify checkDollarBrackets "echo $[1+2]"
-prop_checkDollarBrackets2 = verifyNot checkDollarBrackets "echo $((1+2))"
-checkDollarBrackets _ (T_DollarBracket id _) =
-    style id 2007 "Use $((..)) instead of deprecated $[..]"
-checkDollarBrackets _ _ = return ()
-
-prop_checkSshHereDoc1 = verify checkSshHereDoc "ssh host << foo\necho $PATH\nfoo"
-prop_checkSshHereDoc2 = verifyNot checkSshHereDoc "ssh host << 'foo'\necho $PATH\nfoo"
-checkSshHereDoc _ (T_Redirecting _ redirs cmd)
-        | cmd `isCommand` "ssh" =
-    mapM_ checkHereDoc redirs
-  where
-    hasVariables = mkRegex "[`$]"
-    checkHereDoc (T_FdRedirect _ _ (T_HereDoc id _ Unquoted token tokens))
-        | not (all isConstant tokens) =
-        warn id 2087 $ "Quote '" ++ token ++ "' to make here document expansions happen on the server side rather than on the client."
-    checkHereDoc _ = return ()
-checkSshHereDoc _ _ = return ()
-
--- This is hard to get right without properly parsing ssh args
-prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
-prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
-prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
-checkSshCommandString _ = checkCommand "ssh" (const f)
-  where
-    nonOptions =
-        filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
-    f args =
-        case nonOptions args of
-            (hostport:r@(_:_)) -> checkArg $ last r
-            _ -> return ()
-    checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
-        case filter (not . isConstant) parts of
-            [] -> return ()
-            (x:_) -> info (getId x) 2029
-                "Note that, unescaped, this expands on the client side."
-    checkArg _ = return ()
-
-
---- Subshell detection
-prop_subshellAssignmentCheck = verifyTree     subshellAssignmentCheck "cat foo | while read bar; do a=$bar; done; echo \"$a\""
-prop_subshellAssignmentCheck2 = verifyNotTree subshellAssignmentCheck "while read bar; do a=$bar; done < file; echo \"$a\""
-prop_subshellAssignmentCheck3 = verifyTree    subshellAssignmentCheck "( A=foo; ); rm $A"
-prop_subshellAssignmentCheck4 = verifyNotTree subshellAssignmentCheck "( A=foo; rm $A; )"
-prop_subshellAssignmentCheck5 = verifyTree    subshellAssignmentCheck "cat foo | while read cow; do true; done; echo $cow;"
-prop_subshellAssignmentCheck6 = verifyTree    subshellAssignmentCheck "( export lol=$(ls); ); echo $lol;"
-prop_subshellAssignmentCheck6a= verifyTree    subshellAssignmentCheck "( typeset -a lol=a; ); echo $lol;"
-prop_subshellAssignmentCheck7 = verifyTree    subshellAssignmentCheck "cmd | while read foo; do (( n++ )); done; echo \"$n lines\""
-prop_subshellAssignmentCheck8 = verifyTree    subshellAssignmentCheck "n=3 & echo $((n++))"
-prop_subshellAssignmentCheck9 = verifyTree    subshellAssignmentCheck "read n & n=foo$n"
-prop_subshellAssignmentCheck10 = verifyTree    subshellAssignmentCheck "(( n <<= 3 )) & (( n |= 4 )) &"
-prop_subshellAssignmentCheck11 = verifyTree subshellAssignmentCheck "cat /etc/passwd | while read line; do let n=n+1; done\necho $n"
-prop_subshellAssignmentCheck12 = verifyTree subshellAssignmentCheck "cat /etc/passwd | while read line; do let ++n; done\necho $n"
-prop_subshellAssignmentCheck13 = verifyTree subshellAssignmentCheck "#!/bin/bash\necho foo | read bar; echo $bar"
-prop_subshellAssignmentCheck14 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh93\necho foo | read bar; echo $bar"
-prop_subshellAssignmentCheck15 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh\ncat foo | while read bar; do a=$bar; done\necho \"$a\""
-prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@"
-prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar"
-prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n"
-subshellAssignmentCheck params t =
-    let flow = variableFlow params
-        check = findSubshelled flow [("oops",[])] Map.empty
-    in snd $ runWriter check
-
-
-data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
-data StackData =
-    StackScope Scope
-    | StackScopeEnd
-    -- (Base expression, specific position, var name, assigned values)
-    | Assignment (Token, Token, String, DataType)
-    | Reference (Token, Token, String)
-  deriving (Show)
-
-data DataType = DataString DataSource | DataArray DataSource
-  deriving (Show)
-
-data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
-  deriving (Show)
-
-data VariableState = Dead Token String | Alive deriving (Show)
-
-dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
-
-leadType shell parents t =
-    case t of
-        T_DollarExpansion _ _  -> SubshellScope "$(..) expansion"
-        T_Backticked _ _  -> SubshellScope "`..` expansion"
-        T_Backgrounded _ _  -> SubshellScope "backgrounding &"
-        T_Subshell _ _  -> SubshellScope "(..) group"
-        T_CoProcBody _ _  -> SubshellScope "coproc"
-        T_Redirecting {}  ->
-            if fromMaybe False causesSubshell
-            then SubshellScope "pipeline"
-            else NoneScope
-        _ -> NoneScope
-  where
-    parentPipeline = do
-        parent <- Map.lookup (getId t) parents
-        case parent of
-            T_Pipeline {} -> return parent
-            _ -> Nothing
-
-    causesSubshell = do
-        (T_Pipeline _ _ list) <- parentPipeline
-        if length list <= 1
-            then return False
-            else if lastCreatesSubshell
-                then return True
-                else return . not $ (getId . head $ reverse list) == getId t
-
-    lastCreatesSubshell =
-        case shell of
-            Bash -> True
-            Dash -> True
-            Sh -> True
-            Ksh -> False
-
-isClosingFileOp op =
-    case op of
-        T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
-        T_IoFile _ (T_LESSAND  _) (T_NormalWord _ [T_Literal _ "-"]) -> True
-        _ -> False
-
-getModifiedVariables t =
-    case t of
-        T_SimpleCommand _ vars [] ->
-            concatMap (\x -> case x of
-                                T_Assignment id _ name _ w  ->
-                                    [(x, x, name, dataTypeFrom DataString w)]
-                                _ -> []
-                      ) vars
-        c@(T_SimpleCommand {}) ->
-            getModifiedVariableCommand c
-
-        TA_Unary _ "++|" var -> maybeToList $ do
-            name <- getLiteralString var
-            return (t, t, name, DataString $ SourceFrom [t])
-        TA_Unary _ "|++" var -> maybeToList $ do
-            name <- getLiteralString var
-            return (t, t, name, DataString $ SourceFrom [t])
-        TA_Binary _ op lhs rhs -> maybeToList $ do
-            guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
-            name <- getLiteralString lhs
-            return (t, t, name, DataString $ SourceFrom [rhs])
-
-        t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
-            [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
-
-        t@(T_CoProc _ name _) ->
-            [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
-
-        --Points to 'for' rather than variable
-        T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
-        T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
-        _ -> []
-
--- Consider 'export/declare -x' a reference, since it makes the var available
-getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
-    case x of
-        "export" -> if "f" `elem` flags
-            then []
-            else concatMap getReference rest
-        "declare" -> if any (`elem` flags) ["x", "p"]
-            then concatMap getReference rest
-            else []
-        "readonly" -> concatMap getReference rest
-        "trap" ->
-            case rest of
-                head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
-                _ -> []
-        _ -> []
-  where
-    getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
-    getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
-    getReference _ = []
-    flags = map snd $ getAllFlags base
-
-getReferencedVariableCommand _ = []
-
-getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
-   filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
-    case x of
-        "read" ->
-            let params = map getLiteral rest in
-                catMaybes . takeWhile isJust . reverse $ params
-        "getopts" ->
-            case rest of
-                opts:var:_ -> maybeToList $ getLiteral var
-                _ -> []
-
-        "let" -> concatMap letParamToLiteral rest
-
-        "export" ->
-            if "f" `elem` flags then [] else concatMap getModifierParamString rest
-
-        "declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
-        "typeset" -> declaredVars
-
-        "local" -> concatMap getModifierParamString rest
-        "readonly" -> concatMap getModifierParamString rest
-        "set" -> maybeToList $ do
-            params <- getSetParams rest
-            return (base, base, "@", DataString $ SourceFrom params)
-
-        "printf" -> maybeToList $ getPrintfVariable rest
-
-        "mapfile" -> maybeToList $ getMapfileArray base rest
-        "readarray" -> maybeToList $ getMapfileArray base rest
-
-        _ -> []
-  where
-    flags = map snd $ getAllFlags base
-    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_DoubleQuoted id2 [T_Literal id3 s]]) =
-        T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
-    stripEqualsFrom t = t
-
-    declaredVars = concatMap (getModifierParam defaultType) rest
-      where
-        defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
-
-    getLiteral t = do
-        s <- getLiteralString t
-        when ("-" `isPrefixOf` s) $ fail "argument"
-        return (base, t, s, DataString SourceExternal)
-
-    getModifierParamString = getModifierParam DataString
-
-    getModifierParam def t@(T_Assignment _ _ name _ value) =
-        [(base, t, name, dataTypeFrom def value)]
-    getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
-        name <- getLiteralString t
-        guard $ isVariableName name
-        return (base, t, name, def SourceDeclaration)
-    getModifierParam _ _ = []
-
-    letParamToLiteral token =
-          if var == ""
-            then []
-            else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
-        where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
-
-    getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
-    getSetParams (t:rest) =
-        let s = getLiteralString t in
-            case s of
-                Just "--" -> return rest
-                Just ('-':_) -> getSetParams rest
-                _ -> return (t:fromMaybe [] (getSetParams rest))
-    getSetParams [] = Nothing
-
-    getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
-      where
-        f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
-        f (_:rest) = f rest
-        f [] = fail "not found"
-
-    -- mapfile has some curious syntax allowing flags plus 0..n variable names
-    -- where only the first non-option one is used if any. Here we cheat and
-    -- just get the last one, if it's a variable name.
-    getMapfileArray base arguments = do
-        lastArg <- listToMaybe (reverse arguments)
-        name <- getLiteralString lastArg
-        guard $ isVariableName name
-        return (base, lastArg, name, DataArray SourceExternal)
-
-getModifiedVariableCommand _ = []
-
-prop_getBracedReference1 = getBracedReference "foo" == "foo"
-prop_getBracedReference2 = getBracedReference "#foo" == "foo"
-prop_getBracedReference3 = getBracedReference "#" == "#"
-prop_getBracedReference4 = getBracedReference "##" == "#"
-prop_getBracedReference5 = getBracedReference "#!" == "!"
-prop_getBracedReference6 = getBracedReference "!#" == "#"
-prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
-prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
-prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
-prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
-prop_getBracedReference11= getBracedReference "!os*" == ""
-prop_getBracedReference12= getBracedReference "!os?bar**" == ""
-getBracedReference s = fromMaybe s $
-    nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
-  where
-    noPrefix = dropPrefix s
-    dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
-    dropPrefix "" = ""
-    takeName s = do
-        let name = takeWhile isVariableChar s
-        guard . not $ null name
-        return name
-    getSpecial (c:_) =
-        if c `elem` "*@#?-$!" then return [c] else fail "not special"
-    getSpecial _ = fail "empty"
-
-    nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
-        let suffix = dropWhile isVariableChar rest
-        guard $ suffix /= rest -- e.g. ${!@}
-        first <- suffix !!! 0
-        guard $ first `elem` "*?"
-        return ""
-    nameExpansion _ = Nothing
-
-getIndexReferences s = fromMaybe [] $ do
-    match <- matchRegex re s
-    index <- match !!! 0
-    return $ matchAllStrings variableNameRegex index
-  where
-    re = mkRegex "(\\[.*\\])"
-
-getReferencedVariables t =
-    case t of
-        T_DollarBraced id l -> let str = bracedString t in
-            (t, t, getBracedReference str) :
-                map (\x -> (l, l, x)) (getIndexReferences str)
-        TA_Expansion id _ -> getIfReference t t
-        T_Assignment id mode str _ word ->
-            [(t, t, str) | mode == Append] ++ specialReferences str t word
-
-        TC_Unary id _ "-v" token -> getIfReference t token
-        TC_Unary id _ "-R" token -> getIfReference t token
-        TC_Binary id DoubleBracket op lhs rhs ->
-            if isDereferencing op
-            then concatMap (getIfReference t) [lhs, rhs]
-            else []
-
-        t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
-            [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
-        x -> getReferencedVariableCommand x
-  where
-    -- Try to reduce false positives for unused vars only referenced from evaluated vars
-    specialReferences name base word =
-        if name `elem` [
-            "PS1", "PS2", "PS3", "PS4",
-            "PROMPT_COMMAND"
-          ]
-        then
-            map (\x -> (base, base, x)) $
-                getVariablesFromLiteralToken word
-        else []
-
-    literalizer (TA_Index {}) = return ""  -- x[0] becomes a reference of x
-    literalizer _ = Nothing
-
-    getIfReference context token = maybeToList $ do
-            str <- getLiteralStringExt literalizer token
-            guard . not $ null str
-            when (isDigit $ head str) $ fail "is a number"
-            return (context, token, getBracedReference str)
-
-    isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
-
--- Try to get referenced variables from a literal string like "$foo"
--- Ignores tons of cases like arithmetic evaluation and array indices.
-prop_getVariablesFromLiteral1 =
-    getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
-getVariablesFromLiteral string =
-    map (!! 0) $ matchAllSubgroups variableRegex string
-  where
-    variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-
-getVariablesFromLiteralToken token =
-    getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
-
-getVariableFlow shell parents t =
-    let (_, stack) = runState (doStackAnalysis startScope endScope t) []
-    in reverse stack
-  where
-    startScope t =
-        let scopeType = leadType shell parents t
-        in do
-            when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
-            when (assignFirst t) $ setWritten t
-
-    endScope t =
-        let scopeType = leadType shell parents t
-        in do
-            setRead t
-            unless (assignFirst t) $ setWritten t
-            when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
-
-    assignFirst (T_ForIn {}) = True
-    assignFirst (T_SelectIn {}) = True
-    assignFirst _ = False
-
-    setRead t =
-        let read    = getReferencedVariables t
-        in mapM_ (\v -> modify (Reference v:)) read
-
-    setWritten t =
-        let written = getModifiedVariables t
-        in mapM_ (\v -> modify (Assignment v:)) written
-
-findSubshelled [] _ _ = return ()
-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
-    unless (shouldIgnore str) $ case Map.findWithDefault Alive str deadVars of
-        Alive -> return ()
-        Dead writeToken reason -> do
-                    info (getId writeToken) 2030 $ "Modification of " ++ str ++ " is local (to subshell caused by "++ reason ++")."
-                    info (getId readToken) 2031 $ str ++ " was modified in a subshell. That change might be lost."
-    findSubshelled rest scopes deadVars
-  where
-    shouldIgnore str =
-        str `elem` ["@", "*", "IFS"]
-
-findSubshelled (StackScope (SubshellScope reason):rest) scopes deadVars =
-    findSubshelled rest ((reason,[]):scopes) deadVars
-
-findSubshelled (StackScopeEnd:rest) ((reason, scope):oldScopes) deadVars =
-    findSubshelled rest oldScopes $
-        foldl (\m (_, token, var, _) ->
-            Map.insert var (Dead token reason) m) deadVars scope
-
-
--- FIXME: This is a very strange way of doing it.
--- For each variable read/write, run a stateful function that emits
--- comments. The comments are collected and returned.
-doVariableFlowAnalysis ::
-    (Token -> Token -> String -> State t [v])
-    -> (Token -> Token -> String -> DataType -> State t [v])
-    -> t
-    -> [StackData]
-    -> [v]
-
-doVariableFlowAnalysis readFunc writeFunc empty flow = evalState (
-    foldM (\list x -> do { l <- doFlow x;  return $ l ++ list; }) [] flow
-    ) empty
-  where
-    doFlow (Reference (base, token, name)) =
-        readFunc base token name
-    doFlow (Assignment (base, token, name, values)) =
-        writeFunc base token name values
-    doFlow _ = return []
-
----- Check whether variables could have spaces/globs
-prop_checkSpacefulness1 = verifyTree checkSpacefulness "a='cow moo'; echo $a"
-prop_checkSpacefulness2 = verifyNotTree checkSpacefulness "a='cow moo'; [[ $a ]]"
-prop_checkSpacefulness3 = verifyNotTree checkSpacefulness "a='cow*.mp3'; echo \"$a\""
-prop_checkSpacefulness4 = verifyTree checkSpacefulness "for f in *.mp3; do echo $f; done"
-prop_checkSpacefulness4a= verifyNotTree checkSpacefulness "foo=3; foo=$(echo $foo)"
-prop_checkSpacefulness5 = verifyTree checkSpacefulness "a='*'; b=$a; c=lol${b//foo/bar}; echo $c"
-prop_checkSpacefulness6 = verifyTree checkSpacefulness "a=foo$(lol); echo $a"
-prop_checkSpacefulness7 = verifyTree checkSpacefulness "a=foo\\ bar; rm $a"
-prop_checkSpacefulness8 = verifyNotTree checkSpacefulness "a=foo\\ bar; a=foo; rm $a"
-prop_checkSpacefulness10= verifyTree checkSpacefulness "rm $1"
-prop_checkSpacefulness11= verifyTree checkSpacefulness "rm ${10//foo/bar}"
-prop_checkSpacefulness12= verifyNotTree checkSpacefulness "(( $1 + 3 ))"
-prop_checkSpacefulness13= verifyNotTree checkSpacefulness "if [[ $2 -gt 14 ]]; then true; fi"
-prop_checkSpacefulness14= verifyNotTree checkSpacefulness "foo=$3 env"
-prop_checkSpacefulness15= verifyNotTree checkSpacefulness "local foo=$1"
-prop_checkSpacefulness16= verifyNotTree checkSpacefulness "declare foo=$1"
-prop_checkSpacefulness17= verifyTree checkSpacefulness "echo foo=$1"
-prop_checkSpacefulness18= verifyNotTree checkSpacefulness "$1 --flags"
-prop_checkSpacefulness19= verifyTree checkSpacefulness "echo $PWD"
-prop_checkSpacefulness20= verifyNotTree checkSpacefulness "n+='foo bar'"
-prop_checkSpacefulness21= verifyNotTree checkSpacefulness "select foo in $bar; do true; done"
-prop_checkSpacefulness22= verifyNotTree checkSpacefulness "echo $\"$1\""
-prop_checkSpacefulness23= verifyNotTree checkSpacefulness "a=(1); echo ${a[@]}"
-prop_checkSpacefulness24= verifyTree checkSpacefulness "a='a    b'; cat <<< $a"
-prop_checkSpacefulness25= verifyTree checkSpacefulness "a='s/[0-9]//g'; sed $a"
-prop_checkSpacefulness26= verifyTree checkSpacefulness "a='foo bar'; echo {1,2,$a}"
-prop_checkSpacefulness27= verifyNotTree checkSpacefulness "echo ${a:+'foo'}"
-prop_checkSpacefulness28= verifyNotTree checkSpacefulness "exec {n}>&1; echo $n"
-prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>&-;"
-prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"
-prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""
-prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
-
-checkSpacefulness params t =
-    doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
-  where
-    defaults = zip variablesWithoutSpaces (repeat False)
-
-    hasSpaces name = do
-        map <- get
-        return $ Map.findWithDefault True name map
-
-    setSpaces name bool =
-        modify $ Map.insert name bool
-
-    readF _ token name = do
-        spaced <- hasSpaces name
-        return [makeComment InfoC (getId token) 2086 warning |
-                  isExpansion token && spaced
-                  && not (isArrayExpansion token) -- There's another warning for this
-                  && not (isCounting token)
-                  && not (isQuoteFree parents token)
-                  && not (isQuotedAlternative token)
-                  && not (usedAsCommandName parents token)]
-      where
-        warning = "Double quote to prevent globbing and word splitting."
-
-    writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
-    writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
-
-    writeF _ _ name (DataString (SourceFrom vals)) = do
-        map <- get
-        setSpaces name
-            (isSpacefulWord (\x -> Map.findWithDefault True x map) vals)
-        return []
-
-    writeF _ _ _ _ = return []
-
-    parents = parentMap params
-
-    isExpansion t =
-        case t of
-            (T_DollarBraced _ _ ) -> True
-            _ -> False
-
-    isCounting (T_DollarBraced id token) =
-        case concat $ oversimplify token of
-            '#':_ -> True
-            _ -> False
-    isCounting _ = False
-
-    -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
-    isQuotedAlternative t =
-        case t of
-            T_DollarBraced _ _ ->
-                ":+" `isInfixOf` bracedString t
-            _ -> False
-
-    isSpacefulWord :: (String -> Bool) -> [Token] -> Bool
-    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_Literal _ s      -> s `containsAny` globspace
-          T_SingleQuoted _ s -> s `containsAny` globspace
-          T_DollarBraced _ _ -> spacefulF $ getBracedReference $ bracedString x
-          T_NormalWord _ w   -> isSpacefulWord spacefulF w
-          T_DoubleQuoted _ w -> isSpacefulWord spacefulF w
-          _ -> False
-      where
-        globspace = "*?[] \t\n"
-        containsAny s = any (`elem` s)
-
-prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
-prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
-prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""
-prop_checkQuotesInLiterals3 =verifyNotTree checkQuotesInLiterals "param=('--foo='); app \"${param[@]}\""
-prop_checkQuotesInLiterals4 = verifyNotTree checkQuotesInLiterals "param=\"don't bother with this one\"; app $param"
-prop_checkQuotesInLiterals5 = verifyNotTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; eval app $param"
-prop_checkQuotesInLiterals6 = verifyTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm $param\"; $cmd"
-prop_checkQuotesInLiterals6a= verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd"
-prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param"
-prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param"
-prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm ${#param}"
-checkQuotesInLiterals params t =
-    doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)
-  where
-    getQuotes name = liftM (Map.lookup name) get
-    setQuotes name ref = modify $ Map.insert name ref
-    deleteQuotes = modify . Map.delete
-    parents = parentMap params
-    quoteRegex = mkRegex "\"|([/= ]|^)'|'( |$)|\\\\ "
-    containsQuotes s = s `matches` quoteRegex
-
-    writeF _ _ name (DataString (SourceFrom values)) = do
-        quoteMap <- get
-        let quotedVars = msum $ map (forToken quoteMap) values
-        case quotedVars of
-            Nothing -> deleteQuotes name
-            Just x -> setQuotes name x
-        return []
-    writeF _ _ _ _ = return []
-
-    forToken map (T_DollarBraced id t) =
-        -- skip getBracedReference here to avoid false positives on PE
-        Map.lookup (concat . oversimplify $ t) map
-    forToken quoteMap (T_DoubleQuoted id tokens) =
-        msum $ map (forToken quoteMap) tokens
-    forToken quoteMap (T_NormalWord id tokens) =
-        msum $ map (forToken quoteMap) tokens
-    forToken _ t =
-        if containsQuotes (concat $ oversimplify t)
-        then return $ getId t
-        else Nothing
-
-    squashesQuotes t =
-        case t of
-            T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t
-            otherwise -> False
-
-    readF _ expr name = do
-        assignment <- getQuotes name
-        return
-          (if isJust assignment
-              && not (isParamTo parents "eval" expr)
-              && not (isQuoteFree parents expr)
-              && not (squashesQuotes expr)
-              then [
-                  makeComment WarningC (fromJust assignment) 2089
-                      "Quotes/backslashes will be treated literally. Use an array.",
-                  makeComment WarningC (getId expr) 2090
-                      "Quotes/backslashes in this variable will not be respected."
-                ]
-              else [])
-
-
-prop_checkFunctionsUsedExternally1 =
-  verifyTree checkFunctionsUsedExternally "foo() { :; }; sudo foo"
-prop_checkFunctionsUsedExternally2 =
-  verifyTree checkFunctionsUsedExternally "alias f='a'; xargs -n 1 f"
-prop_checkFunctionsUsedExternally3 =
-  verifyNotTree checkFunctionsUsedExternally "f() { :; }; echo f"
-prop_checkFunctionsUsedExternally4 =
-  verifyNotTree checkFunctionsUsedExternally "foo() { :; }; sudo \"foo\""
-checkFunctionsUsedExternally params t =
-    runNodeAnalysis checkCommand params t
-  where
-    invokingCmds = [
-        "chroot",
-        "find",
-        "screen",
-        "ssh",
-        "su",
-        "sudo",
-        "xargs"
-        ]
-    checkCommand _ t@(T_SimpleCommand _ _ (cmd:args)) =
-        let name = fromMaybe "" $ getCommandBasename t in
-          when (name `elem` invokingCmds) $
-            mapM_ (checkArg name) args
-    checkCommand _ _ = return ()
-
-    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))
-        | t `isUnqualifiedCommand` "alias" = mapM_ getAlias args
-    findFunctions _ = return ()
-    getAlias arg =
-        let string = concat $ oversimplify arg
-        in when ('=' `elem` string) $
-            modify ((takeWhile (/= '=') string, getId arg):)
-    checkArg cmd arg = potentially $ do
-        literalArg <- getUnquotedLiteral arg  -- only consider unquoted literals
-        definitionId <- Map.lookup literalArg functions
-        return $ do
-            warn (getId arg) 2033
-              "Shell functions can't be passed to external commands."
-            info definitionId 2032 $
-              "Use own script or sh -c '..' to run this from " ++ cmd ++ "."
-
-prop_checkUnused0 = verifyNotTree checkUnusedAssignments "var=foo; echo $var"
-prop_checkUnused1 = verifyTree checkUnusedAssignments "var=foo; echo $bar"
-prop_checkUnused2 = verifyNotTree checkUnusedAssignments "var=foo; export var;"
-prop_checkUnused3 = verifyTree checkUnusedAssignments "for f in *; do echo '$f'; done"
-prop_checkUnused4 = verifyTree checkUnusedAssignments "local i=0"
-prop_checkUnused5 = verifyNotTree checkUnusedAssignments "read lol; echo $lol"
-prop_checkUnused6 = verifyNotTree checkUnusedAssignments "var=4; (( var++ ))"
-prop_checkUnused7 = verifyNotTree checkUnusedAssignments "var=2; $((var))"
-prop_checkUnused8 = verifyTree checkUnusedAssignments "var=2; var=3;"
-prop_checkUnused9 = verifyNotTree checkUnusedAssignments "read ''"
-prop_checkUnused10= verifyNotTree checkUnusedAssignments "read -p 'test: '"
-prop_checkUnused11= verifyNotTree checkUnusedAssignments "bar=5; export foo[$bar]=3"
-prop_checkUnused12= verifyNotTree checkUnusedAssignments "read foo; echo ${!foo}"
-prop_checkUnused13= verifyNotTree checkUnusedAssignments "x=(1); (( x[0] ))"
-prop_checkUnused14= verifyNotTree checkUnusedAssignments "x=(1); n=0; echo ${x[n]}"
-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"
-prop_checkUnused20= verifyNotTree checkUnusedAssignments "a=1; PS1='$a'"
-prop_checkUnused21= verifyNotTree checkUnusedAssignments "a=1; trap 'echo $a' INT"
-prop_checkUnused22= verifyNotTree checkUnusedAssignments "a=1; [ -v a ]"
-prop_checkUnused23= verifyNotTree checkUnusedAssignments "a=1; [ -R a ]"
-prop_checkUnused24= verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}"
-prop_checkUnused25= verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}"
-prop_checkUnused26= verifyNotTree checkUnusedAssignments "declare -F foo"
-prop_checkUnused27= verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]"
-prop_checkUnused28= verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]"
-prop_checkUnused29= verifyNotTree checkUnusedAssignments "var=(a b); declare -p var"
-checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
-  where
-    flow = variableFlow params
-    references = foldl (flip ($)) defaultMap (map insertRef flow)
-    insertRef (Reference (base, token, name)) =
-        Map.insert (stripSuffix name) ()
-    insertRef _ = id
-
-    assignments = foldl (flip ($)) Map.empty (map insertAssignment flow)
-    insertAssignment (Assignment (_, token, name, _)) | isVariableName name =
-        Map.insert name token
-    insertAssignment _ = id
-
-    unused = Map.assocs $ Map.difference assignments references
-
-    warnFor (name, token) =
-        warn (getId token) 2034 $
-            name ++ " appears unused. Verify it or export it."
-
-    stripSuffix = takeWhile isVariableChar
-    defaultMap = Map.fromList $ zip internalVariables $ repeat ()
-
-prop_checkUnassignedReferences1 = verifyTree checkUnassignedReferences "echo $foo"
-prop_checkUnassignedReferences2 = verifyNotTree checkUnassignedReferences "foo=hello; echo $foo"
-prop_checkUnassignedReferences3 = verifyTree checkUnassignedReferences "MY_VALUE=3; echo $MYVALUE"
-prop_checkUnassignedReferences4 = verifyNotTree checkUnassignedReferences "RANDOM2=foo; echo $RANDOM"
-prop_checkUnassignedReferences5 = verifyNotTree checkUnassignedReferences "declare -A foo=([bar]=baz); echo ${foo[bar]}"
-prop_checkUnassignedReferences6 = verifyNotTree checkUnassignedReferences "foo=..; echo ${foo-bar}"
-prop_checkUnassignedReferences7 = verifyNotTree checkUnassignedReferences "getopts ':h' foo; echo $foo"
-prop_checkUnassignedReferences8 = verifyNotTree checkUnassignedReferences "let 'foo = 1'; echo $foo"
-prop_checkUnassignedReferences9 = verifyNotTree checkUnassignedReferences "echo ${foo-bar}"
-prop_checkUnassignedReferences10= verifyNotTree checkUnassignedReferences "echo ${foo:?}"
-prop_checkUnassignedReferences11= verifyNotTree checkUnassignedReferences "declare -A foo; echo \"${foo[@]}\""
-prop_checkUnassignedReferences12= verifyNotTree checkUnassignedReferences "typeset -a foo; echo \"${foo[@]}\""
-prop_checkUnassignedReferences13= verifyNotTree checkUnassignedReferences "f() { local foo; echo $foo; }"
-prop_checkUnassignedReferences14= verifyNotTree checkUnassignedReferences "foo=; echo $foo"
-prop_checkUnassignedReferences15= verifyNotTree checkUnassignedReferences "f() { true; }; export -f f"
-prop_checkUnassignedReferences16= verifyNotTree checkUnassignedReferences "declare -A foo=( [a b]=bar ); echo ${foo[a b]}"
-prop_checkUnassignedReferences17= verifyNotTree checkUnassignedReferences "USERS=foo; echo $USER"
-prop_checkUnassignedReferences18= verifyNotTree checkUnassignedReferences "FOOBAR=42; export FOOBAR="
-prop_checkUnassignedReferences19= verifyNotTree checkUnassignedReferences "readonly foo=bar; echo $foo"
-prop_checkUnassignedReferences20= verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo"
-prop_checkUnassignedReferences21= verifyTree checkUnassignedReferences "echo ${#foo}"
-prop_checkUnassignedReferences22= verifyNotTree checkUnassignedReferences "echo ${!os*}"
-checkUnassignedReferences params t = warnings
-  where
-    (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
-    defaultAssigned = Map.fromList $ map (\a -> (a, ())) $ filter (not . null) internalVariables
-
-    tally (Assignment (_, _, name, _))  =
-        modify (\(read, written) -> (read, Map.insert name () written))
-    tally (Reference (_, place, name)) =
-        modify (\(read, written) -> (Map.insertWith' (const id) name place read, written))
-    tally _ = return ()
-
-    unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned
-    writtenVars = filter isVariableName $ Map.keys writeMap
-
-    getBestMatch var = do
-        (match, score) <- listToMaybe best
-        guard $ goodMatch var match score
-        return match
-      where
-        matches = map (\x -> (x, match var x)) writtenVars
-        best = sortBy (comparing snd) matches
-        goodMatch var match score =
-            let l = length match in
-                l > 3 && score <= 1
-                || l > 7 && score <= 2
-
-    isLocal = any isLower
-
-    warningForGlobals var place = do
-        match <- getBestMatch var
-        return $ warn (getId place) 2153 $
-            "Possible misspelling: " ++ var ++ " may not be assigned, but " ++ match ++ " is."
-
-    warningForLocals var place =
-        return $ warn (getId place) 2154 $
-            var ++ " is referenced but not assigned" ++ optionalTip ++ "."
-      where
-        optionalTip =
-            if var `elem` commonCommands
-            then " (for output from commands, use \"$(" ++ var ++ " ..." ++ ")\" )"
-            else fromMaybe "" $ do
-                    match <- getBestMatch var
-                    return $ " (did you mean '" ++ match ++ "'?)"
-
-    warningFor var place = do
-        guard . not $ isInArray var place || isGuarded place
-        (if isLocal var then warningForLocals else warningForGlobals) var place
-
-    warnings = execWriter . sequence $ mapMaybe (uncurry warningFor) unassigned
-
-    -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.
-    -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.
-    isInArray var t = any isArray $ getPath (parentMap params) t
-      where
-        isArray (T_Array {}) = True
-        isArray b@(T_DollarBraced _ _) | var /= getBracedReference (bracedString b) = True
-        isArray _ = False
-
-    isGuarded (T_DollarBraced _ v) =
-        any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
-      where
-        name = concat $ oversimplify v
-        rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
-    isGuarded _ = False
-
-    match var candidate =
-        if var /= candidate && map toLower var == map toLower candidate
-        then 1
-        else dist var candidate
-
-
-prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"
-prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*"
-prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"
-checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =
-    mapM_ check $ takeWhile (not . isEndOfArgs) args
-  where
-    check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" =
-        info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options."
-    check _ = return ()
-
-    isEndOfArgs t =
-        case concat $ oversimplify t of
-            "--" -> True
-            ":::" -> True
-            "::::" -> True
-            _ -> False
-
-checkGlobsAsOptions _ _ = return ()
-
-
-prop_checkWhileReadPitfalls1 = verify checkWhileReadPitfalls "while read foo; do ssh $foo uptime; done < file"
-prop_checkWhileReadPitfalls2 = verifyNot checkWhileReadPitfalls "while read -u 3 foo; do ssh $foo uptime; done 3< file"
-prop_checkWhileReadPitfalls3 = verifyNot checkWhileReadPitfalls "while true; do ssh host uptime; done"
-prop_checkWhileReadPitfalls4 = verifyNot checkWhileReadPitfalls "while read foo; do ssh $foo hostname < /dev/null; done"
-prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done"
-prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo"
-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 =
-    mapM_ checkMuncher contents
-  where
-    munchers = [ "ssh", "ffmpeg", "mplayer" ]
-
-    isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) =
-        let plaintext = oversimplify cmd
-        in head (plaintext ++ [""]) == "read"
-            && ("-u" `notElem` plaintext)
-            && all (not . stdinRedirect) redirs
-    isStdinReadCommand _ = False
-
-    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]
-
-            _ -> potentially $ do
-                name <- getCommandBasename cmd
-                guard $ name `elem` munchers
-                return $ do
-                    info id 2095 $
-                        name ++ " may swallow stdin, preventing this loop from working properly."
-                    warn (getId cmd) 2095 $
-                        "Add < /dev/null to prevent " ++ name ++ " from swallowing stdin."
-    checkMuncher _ = return ()
-
-    stdinRedirect (T_FdRedirect _ fd _)
-        | fd == "" || fd == "0" = True
-    stdinRedirect _ = False
-checkWhileReadPitfalls _ _ = return ()
-
-
-prop_checkPrefixAssign1 = verify checkPrefixAssignmentReference "var=foo echo $var"
-prop_checkPrefixAssign2 = verifyNot checkPrefixAssignmentReference "var=$(echo $var) cmd"
-checkPrefixAssignmentReference params t@(T_DollarBraced id value) =
-    check path
-  where
-    name = getBracedReference $ bracedString t
-    path = getPath (parentMap params) t
-    idPath = map getId path
-
-    check [] = return ()
-    check (t:rest) =
-        case t of
-            T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars
-            otherwise -> check rest
-    checkVar (T_Assignment aId mode aName Nothing value) |
-            aName == name && (aId `notElem` idPath) = do
-        warn aId 2097 "This assignment is only seen by the forked process."
-        warn id 2098 "This expansion will not see the mentioned assignment."
-    checkVar _ = return ()
-
-checkPrefixAssignmentReference _ _ = return ()
-
-prop_checkCharRangeGlob1 = verify checkCharRangeGlob "ls *[:digit:].jpg"
-prop_checkCharRangeGlob2 = verifyNot checkCharRangeGlob "ls *[[:digit:]].jpg"
-prop_checkCharRangeGlob3 = verify checkCharRangeGlob "ls [10-15]"
-prop_checkCharRangeGlob4 = verifyNot checkCharRangeGlob "ls [a-zA-Z]"
-prop_checkCharRangeGlob5 = verifyNot checkCharRangeGlob "tr -d [a-zA-Z]" -- tr has 2060
-checkCharRangeGlob p t@(T_Glob id str) |
-  isCharClass str && not (isParamTo (parentMap p) "tr" t) =
-    if ":" `isPrefixOf` contents
-        && ":" `isSuffixOf` contents
-        && contents /= ":"
-    then warn id 2101 "Named class needs outer [], e.g. [[:digit:]]."
-    else
-        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
-    hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents
-checkCharRangeGlob _ _ = return ()
-
-
-
-prop_checkCdAndBack1 = verify checkCdAndBack "for f in *; do cd $f; git pull; cd ..; done"
-prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
-prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
-prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
-checkCdAndBack params = doLists
-  where
-    shell = shellType params
-    doLists (T_ForIn _ _ _ cmds) = doList cmds
-    doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds
-    doLists (T_WhileExpression _ _ cmds) = doList cmds
-    doLists (T_UntilExpression _ _ cmds) = doList cmds
-    doLists (T_Script _ _ cmds) = doList cmds
-    doLists (T_IfExpression _ thens elses) = do
-        mapM_ (\(_, l) -> doList l) thens
-        doList elses
-    doLists _ = return ()
-
-    isCdRevert t =
-        case oversimplify t of
-            ["cd", p] -> p `elem` ["..", "-"]
-            _ -> False
-
-    getCmd (T_Annotation id _ x) = getCmd x
-    getCmd (T_Pipeline id _ [x]) = getCommandName x
-    getCmd _ = Nothing
-
-    doList list =
-        let cds = filter ((== Just "cd") . getCmd) list in
-            when (length cds >= 2 && isCdRevert (last cds)) $
-               info (getId $ last cds) 2103 message
-
-    message = "Use a ( subshell ) to avoid having to cd back."
-
-prop_checkLoopKeywordScope1 = verify checkLoopKeywordScope "continue 2"
-prop_checkLoopKeywordScope2 = verify checkLoopKeywordScope "for f; do ( break; ); done"
-prop_checkLoopKeywordScope3 = verify checkLoopKeywordScope "if true; then continue; fi"
-prop_checkLoopKeywordScope4 = verifyNot checkLoopKeywordScope "while true; do break; done"
-prop_checkLoopKeywordScope5 = verify checkLoopKeywordScope "if true; then break; fi"
-prop_checkLoopKeywordScope6 = verify checkLoopKeywordScope "while true; do true | { break; }; done"
-prop_checkLoopKeywordScope7 = verifyNot checkLoopKeywordScope "#!/bin/ksh\nwhile true; do true | { break; }; done"
-checkLoopKeywordScope params t |
-        name `elem` map Just ["continue", "break"] =
-    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."
-    else case map subshellType $ filter (not . isFunction) path of
-        Just str:_ -> warn (getId t) 2106 $
-            "This only exits the subshell caused by the " ++ str ++ "."
-        _ -> return ()
-  where
-    name = getCommandName t
-    path = let p = getPath (parentMap params) t in filter relevant p
-    subshellType t = case leadType (shellType params) (parentMap params) t of
-        NoneScope -> Nothing
-        SubshellScope str -> return str
-    relevant t = isLoop t || isFunction t || isJust (subshellType t)
-checkLoopKeywordScope _ _ = return ()
-
-
-prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
-prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
-checkLocalScope params t | t `isCommand` "local" && not (isInFunction t) =
-    err (getId t) 2168 "'local' is only valid in functions."
-  where
-    isInFunction t = any isFunction $ getPath (parentMap params) t
-checkLocalScope _ _ = return ()
-
-prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }"
-prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }"
-prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }"
-checkFunctionDeclarations params
-        (T_Function id (FunctionKeyword hasKeyword) (FunctionParentheses hasParens) _ _) =
-    case shellType params of
-        Bash -> return ()
-        Ksh ->
-            when (hasKeyword && hasParens) $
-                err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."
-        Dash -> forSh
-        Sh   -> forSh
-
-    where
-        forSh = do
-            when (hasKeyword && hasParens) $
-                warn id 2112 "'function' keyword is non-standard. Delete it."
-            when (hasKeyword && not hasParens) $
-                warn id 2113 "'function' keyword is non-standard. Use 'foo()' instead of 'function foo'."
-checkFunctionDeclarations _ _ = return ()
-
-
-prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
-prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
-prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
-prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
-prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
-prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
-prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
-prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"
-prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
-prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
-prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
-prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
-prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
-prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
-checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =
-    when (any isRecursiveFlag simpleArgs) $
-        mapM_ (mapM_ checkWord . braceExpand) tokens
-  where
-    simpleArgs = oversimplify t
-
-    checkWord token =
-        case getLiteralString token of
-            Just str ->
-                when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $
-                    warn (getId token) 2114 "Warning: deletes a system directory. Use 'rm --' to disable this message."
-            Nothing ->
-                checkWord' token
-
-    checkWord' token = fromMaybe (return ()) $ do
-        filename <- getPotentialPath token
-        let path = fixPath filename
-        return . when (path `elem` importantPaths) $
-            warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."
-
-    fixPath filename =
-        let normalized = skipRepeating '/' . skipRepeating '*' $ filename in
-            if normalized == "/" then normalized else stripTrailing '/' normalized
-
-    getPotentialPath = getLiteralStringExt f
-      where
-        f (T_Glob _ str) = return str
-        f (T_DollarBraced _ word) =
-            let var = onlyLiteralString word in
-                if any (flip isInfixOf var) [":?", ":-", ":="]
-                then Nothing
-                else return ""
-        f _ = return ""
-
-    isRecursiveFlag "--recursive" = True
-    isRecursiveFlag ('-':'-':_) = False
-    isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str
-    isRecursiveFlag _ = False
-
-    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 _ [] = []
-
-    paths = [
-        "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
-        "/var", "/lib"
-        ]
-    importantPaths = filter (not . null) $
-        ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
-checkCatastrophicRm _ _ = return ()
-
-
-prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
-prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
-prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
-prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
-checkInteractiveSu params = checkCommand "su" f
-  where
-    f cmd l = when (length l <= 1) $
-        when (all undirected $ getPath (parentMap params) cmd) $
-            info (getId cmd) 2117
-                "To run commands as another user, use su -c or sudo."
-
-    undirected (T_Pipeline _ _ l) = length l <= 1
-    -- This should really just be modifications to stdin, but meh
-    undirected (T_Redirecting _ list _) = null list
-    undirected _ = True
-
-
-prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar"
-prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar"
-checkStderrPipe params =
-    case shellType params of
-        Ksh -> match
-        _ -> const $ return ()
-  where
-    match (T_Pipe id "|&") =
-        err id 2118 "Ksh does not support |&. Use 2>&1 |."
-    match _ = return ()
-
-prop_checkUnpassedInFunctions1 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo"
-prop_checkUnpassedInFunctions2 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; };"
-prop_checkUnpassedInFunctions3 = verifyNotTree checkUnpassedInFunctions "foo() { echo $lol; }; foo"
-prop_checkUnpassedInFunctions4 = verifyNotTree checkUnpassedInFunctions "foo() { echo $0; }; foo"
-prop_checkUnpassedInFunctions5 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; }; foo 'lol'; foo"
-prop_checkUnpassedInFunctions6 = verifyNotTree checkUnpassedInFunctions "foo() { set -- *; echo $1; }; foo"
-prop_checkUnpassedInFunctions7 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo; foo;"
-prop_checkUnpassedInFunctions8 = verifyNotTree checkUnpassedInFunctions "foo() { echo $((1)); }; foo;"
-prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;"
-prop_checkUnpassedInFunctions10= verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;"
-prop_checkUnpassedInFunctions11= verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;"
-checkUnpassedInFunctions params root =
-    execWriter $ mapM_ warnForGroup referenceGroups
-  where
-    functionMap :: Map.Map String Token
-    functionMap = Map.fromList $
-        map (\t@(T_Function _ _ _ name _) -> (name,t)) functions
-    functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
-
-    findFunction t@(T_Function id _ _ name body) =
-        let flow = getVariableFlow (shellType params) (parentMap params) body
-        in
-          if any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
-            then return t
-            else Nothing
-    findFunction _ = Nothing
-
-    isPositionalAssignment x =
-        case x of
-            Assignment (_, _, str, _) -> isPositional str
-            _ -> False
-    isPositionalReference function x =
-        case x of
-            Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function
-            _ -> False
-
-    isDirectChildOf child parent = fromMaybe False $ do
-        function <- find (\x -> case x of T_Function {} -> True; _ -> False) $ getPath (parentMap params) child
-        return $ getId parent == getId function
-
-    referenceList :: [(String, Bool, Token)]
-    referenceList = execWriter $
-        doAnalysis (fromMaybe (return ()) . checkCommand) root
-    checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())
-    checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do
-        str <- getLiteralString cmd
-        unless (Map.member str functionMap) $ fail "irrelevant"
-        return $ tell [(str, null args, t)]
-    checkCommand _ = Nothing
-
-    isPositional str = str == "*" || str == "@"
-        || (all isDigit str && str /= "0")
-
-    isArgumentless (_, b, _) = b
-    referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList
-    updateWith x@(name, _, _) = Map.insertWith (++) name [x]
-
-    warnForGroup group =
-        when (all isArgumentless group) $ do
-            mapM_ suggestParams group
-            warnForDeclaration group
-
-    suggestParams (name, _, thing) =
-        info (getId thing) 2119 $
-            "Use " ++ name ++ " \"$@\" if function's $1 should mean script's $1."
-    warnForDeclaration ((name, _, _):_) =
-        warn (getId . fromJust $ Map.lookup name functionMap) 2120 $
-            name ++ " references arguments, but none are ever passed."
-
-
-prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
-prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
-prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
-prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
-prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
-prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
-checkSetAssignment params = checkUnqualifiedCommand "set" f
-  where
-    f cmd (var:value:rest) =
-        let str = literal var in
-            when (isVariableName str || isAssignment str) $
-                msg (getId var)
-    f cmd (var:_) =
-        when (isAssignment $ literal var) $
-            msg (getId var)
-    f _ _ = return ()
-
-    msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
-
-    isAssignment str = '=' `elem` str
-    literal (T_NormalWord _ l) = concatMap literal l
-    literal (T_Literal _ str) = str
-    literal _ = "*"
-
-
-prop_checkOverridingPath1 = verify checkOverridingPath "PATH=\"$var/$foo\""
-prop_checkOverridingPath2 = verify checkOverridingPath "PATH=\"mydir\""
-prop_checkOverridingPath3 = verify checkOverridingPath "PATH=/cow/foo"
-prop_checkOverridingPath4 = verifyNot checkOverridingPath "PATH=/cow/foo/bin"
-prop_checkOverridingPath5 = verifyNot checkOverridingPath "PATH='/bin:/sbin'"
-prop_checkOverridingPath6 = verifyNot checkOverridingPath "PATH=\"$var/$foo\" cmd"
-prop_checkOverridingPath7 = verifyNot checkOverridingPath "PATH=$OLDPATH"
-prop_checkOverridingPath8 = verifyNot checkOverridingPath "PATH=$PATH:/stuff"
-checkOverridingPath _ (T_SimpleCommand _ vars []) =
-    mapM_ checkVar vars
-  where
-    checkVar (T_Assignment id Assign "PATH" Nothing word) =
-        let string = concat $ oversimplify word
-        in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do
-            when ('/' `elem` string && ':' `notElem` string) $ notify id
-            when (isLiteral word && ':' `notElem` string && '/' `notElem` string) $ notify id
-    checkVar _ = return ()
-    notify id = warn id 2123 "PATH is the shell search path. Use another name."
-checkOverridingPath _ _ = return ()
-
-prop_checkTildeInPath1 = verify checkTildeInPath "PATH=\"$PATH:~/bin\""
-prop_checkTildeInPath2 = verify checkTildeInPath "PATH='~foo/bin'"
-prop_checkTildeInPath3 = verifyNot checkTildeInPath "PATH=~/bin"
-checkTildeInPath _ (T_SimpleCommand _ vars _) =
-    mapM_ checkVar vars
-  where
-    checkVar (T_Assignment id Assign "PATH" Nothing (T_NormalWord _ parts)) =
-        when (any (\x -> isQuoted x && hasTilde x) parts) $
-            warn id 2147 "Literal tilde in PATH works poorly across programs."
-    checkVar _ = return ()
-
-    hasTilde t = fromMaybe False (liftM2 elem (return '~') (getLiteralStringExt (const $ return "") t))
-    isQuoted (T_DoubleQuoted {}) = True
-    isQuoted (T_SingleQuoted {}) = True
-    isQuoted _ = False
-checkTildeInPath _ _ = return ()
-
-prop_checkUnsupported3 = verify checkUnsupported "#!/bin/sh\ncase foo in bar) baz ;& esac"
-prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac"
-prop_checkUnsupported5 = verify checkUnsupported "#!/bin/bash\necho \"${ ls; }\""
-checkUnsupported params t =
-    when (not (null support) && (shellType params `notElem` support)) $
-        report name
- where
-    (name, support) = shellSupport t
-    report s = err (getId t) 2127 $
-        "To use " ++ s ++ ", specify #!/usr/bin/env " ++
-            (map toLower . intercalate " or " . map show $ support)
-
--- TODO: Move more of these checks here
-shellSupport t =
-  case t of
-    T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list)
-    T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Ksh])
-    otherwise -> ("", [])
-  where
-    forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash])
-    forCase seps | CaseFallThrough `elem` seps = ("cases with ;&", [Bash, Ksh])
-    forCase _ = ("", [])
-
-
-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;"
-checkMultipleAppends params t =
-    mapM_ checkList $ getCommandSequences t
-  where
-    checkList list =
-        mapM_ checkGroup (groupWith (liftM fst) $ map getTarget list)
-    checkGroup (f:_:_:_) | isJust f =
-        style (snd $ fromJust f) 2129
-            "Consider using { cmd1; cmd2; } >> file instead of individual redirects."
-    checkGroup _ = return ()
-    getTarget (T_Annotation _ _ t) = getTarget t
-    getTarget (T_Pipeline _ _ args@(_:_)) = getTarget (last args)
-    getTarget (T_Redirecting id list _) = do
-        file <- mapMaybe getAppend list !!! 0
-        return (file, id)
-    getTarget _ = Nothing
-    getAppend (T_FdRedirect _ _ (T_IoFile _ (T_DGREAT {}) f)) = return f
-    getAppend _ = Nothing
-
-
-prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
-prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
-prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
-checkAliasesExpandEarly params =
-    checkUnqualifiedCommand "alias" (const f)
-  where
-    f = mapM_ checkArg
-    checkArg arg | '=' `elem` concat (oversimplify 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 ()
-
-prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\""
-prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'"
-checkSuspiciousIFS params (T_Assignment id Assign "IFS" Nothing value) =
-    potentially $ do
-        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'"
-    check value =
-        case value of
-            "\\n" -> suggest n
-            "/n" -> suggest n
-            "\\t" -> suggest t
-            "/t" -> suggest t
-            _ -> return ()
-    suggest r = warn id 2141 $ "Did you mean IFS=" ++ r ++ " ?"
-checkSuspiciousIFS _ _ = return ()
-
-prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
-prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
-prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
-checkAliasesUsesArgs params =
-    checkUnqualifiedCommand "alias" (const f)
-  where
-    re = mkRegex "\\$\\{?[0-9*@]"
-    f = mapM_ checkArg
-    checkArg arg =
-        let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
-            when ('=' `elem` string && string `matches` re) $
-                err (getId arg) 2142
-                    "Aliases can't use positional parameters. Use a function."
-
-prop_checkGrepQ1= verify checkShouldUseGrepQ "[[ $(foo | grep bar) ]]"
-prop_checkGrepQ2= verify checkShouldUseGrepQ "[ -z $(fgrep lol) ]"
-prop_checkGrepQ3= verify checkShouldUseGrepQ "[ -n \"$(foo | zgrep lol)\" ]"
-prop_checkGrepQ4= verifyNot checkShouldUseGrepQ "[ -z $(grep bar | cmd) ]"
-prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"
-prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]"
-checkShouldUseGrepQ params t =
-    potentially $ case t of
-        TC_Noary id _ token -> check id True token
-        TC_Unary id _ "-n" token -> check id True token
-        TC_Unary id _ "-z" token -> check id False token
-        _ -> fail "not check"
-  where
-    check id bool token = do
-        name <- getFinalGrep token
-        let op = if bool then "-n" else "-z"
-        let flip = if bool then "" else "! "
-        return . style id 2143 $
-            "Use " ++ flip ++ name ++ " -q instead of " ++
-                "comparing output with [ " ++ op ++ " .. ]."
-
-    getFinalGrep t = do
-        cmds <- getPipeline t
-        guard . not . null $ cmds
-        name <- getCommandBasename $ last cmds
-        guard . isGrep $ name
-        return name
-    getPipeline t =
-        case t of
-            T_NormalWord _ [x] -> getPipeline x
-            T_DoubleQuoted _ [x] -> getPipeline x
-            T_DollarExpansion _ [x] -> getPipeline x
-            T_Pipeline _ _ cmds -> return cmds
-            _ -> fail "unknown"
-    isGrep = (`elem` ["grep", "egrep", "fgrep", "zgrep"])
-
-prop_checkTestGlobs1 = verify checkTestGlobs "[ -e *.mp3 ]"
-prop_checkTestGlobs2 = verifyNot checkTestGlobs "[[ $a == *b* ]]"
-checkTestGlobs params (TC_Unary _ _ op token) | isGlob token =
-    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", "-or"], isMatch, const True, isAction]
-    f list | length list < length pattern = return ()
-    f list@(_:rest) =
-        if and (zipWith ($) pattern list)
-        then warnFor (list !! (length pattern - 1))
-        else f rest
-    isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
-    isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
-    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."
-
-
-prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
-prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
-prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
-checkFindExecWithSingleArgument _ = checkCommand "find" (const f)
-  where
-    f = void . sequence . mapMaybe check . tails
-    check (exec:arg:term:_) = do
-        execS <- getLiteralString exec
-        termS <- getLiteralString term
-        cmdS <- getLiteralStringExt (const $ return " ") arg
-
-        guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
-        guard $ cmdS `matches` commandRegex
-        return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
-    check _ = Nothing
-    commandRegex = mkRegex "[ |;]"
-
-
-prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }"
-prop_checkMaskedReturns2 = verify checkMaskedReturns "declare a=$(false)"
-prop_checkMaskedReturns3 = verify checkMaskedReturns "declare a=\"`false`\""
-prop_checkMaskedReturns4 = verifyNot checkMaskedReturns "declare a; a=$(false)"
-prop_checkMaskedReturns5 = verifyNot checkMaskedReturns "f() { local -r a=$(false); }"
-checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = potentially $ do
-    name <- getCommandName t
-    guard $ name `elem` ["declare", "export"]
-        || name == "local" && "r" `notElem` map snd (getAllFlags t)
-    return $ mapM_ checkArgs rest
-  where
-    checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
-        warn id 2155 "Declare and assign separately to avoid masking return values."
-    checkArgs _ = return ()
-
-    hasReturn t = case t of
-        T_Backticked {} -> True
-        T_DollarExpansion {} -> True
-        _ -> False
-checkMaskedReturns _ _ = return ()
-
-prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
-prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
-prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
-checkInjectableFindSh _ = checkCommand "find" (const check)
-  where
-    check args = do
-        let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
-        match pattern idStrings
-
-    match _ [] = return ()
-    match [] (next:_) = action next
-    match (p:tests) ((id, arg):args) = do
-        when (p arg) $ match tests args
-        match (p:tests) args
-
-    pattern = [
-        (`elem` ["-exec", "-execdir"]),
-        (`elem` ["sh", "bash", "ksh"]),
-        (== "-c")
-        ]
-    action (id, arg) =
-        when ("{}" `isInfixOf` arg) $
-            warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
-
-prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
-prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
-checkReadWithoutR _ t@(T_SimpleCommand {}) | t `isUnqualifiedCommand` "read" =
-    unless ("r" `elem` map snd (getAllFlags t)) $
-        info (getId t) 2162 "read without -r will mangle backslashes."
-checkReadWithoutR _ _ = return ()
-
-prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
-prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
-prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
-checkExportedExpansions _ = checkUnqualifiedCommand "export" (const check)
-  where
-    check = mapM_ checkForVariables
-    checkForVariables f =
-        case getWordParts f of
-            [t@(T_DollarBraced {})] ->
-                warn (getId t) 2163 "Exporting an expansion rather than a variable."
-            _ -> return ()
-
-
-prop_checkUncheckedCd1 = verifyTree checkUncheckedCd "cd ~/src; rm -r foo"
-prop_checkUncheckedCd2 = verifyNotTree checkUncheckedCd "cd ~/src || exit; rm -r foo"
-prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCd "set -e; cd ~/src; rm -r foo"
-prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi"
-prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi"
-prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .."
-prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar"
-prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar"
-checkUncheckedCd params root =
-    if hasSetE then [] else execWriter $ doAnalysis checkElement root
-  where
-    checkElement t@(T_SimpleCommand {}) =
-        when(t `isUnqualifiedCommand` "cd"
-            && not (isCdDotDot t)
-            && not (isCondition $ getPath (parentMap params) t)) $
-                warn (getId t) 2164 "Use cd ... || exit in case cd fails."
-    checkElement _ = return ()
-    isCdDotDot t = oversimplify t == ["cd", ".."]
-    hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root
-    isSetE t =
-        case t of
-            T_Script _ str _ -> str `matches` re
-            T_SimpleCommand {}  ->
-                t `isUnqualifiedCommand` "set" &&
-                    ("errexit" `elem` oversimplify t || "e" `elem` map snd (getAllFlags t))
-            _ -> False
-    re = mkRegex "[[:space:]]-[^-]*e"
-
-prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done"
-prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"
-prop_checkLoopVariableReassignment3 = verifyNot checkLoopVariableReassignment "for i in *; do for j in *.bar; do true; done; done"
-checkLoopVariableReassignment params token =
-    potentially $ case token of
-        T_ForIn {} -> check
-        T_ForArithmetic {} -> check
-        _ -> Nothing
-  where
-    check = do
-        str <- loopVariable token
-        next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path
-        return $ do
-            warn (getId token) 2165 "This nested loop overrides the index variable of its parent."
-            warn (getId next)  2167 "This parent loop has its index variable overridden."
-    path = drop 1 $ getPath (parentMap params) token
-    loopVariable :: Token -> Maybe String
-    loopVariable t =
-        case t of
-            T_ForIn _ s _ _ -> return s
-            T_ForArithmetic _
-                (TA_Sequence _
-                    [TA_Binary _ "="
-                        (TA_Expansion _ [T_Literal _ var]) _])
-                            _ _ _ -> return var
-            _ -> fail "not loop"
-
-prop_checkTrailingBracket1 = verify checkTrailingBracket "if -z n ]]; then true; fi "
-prop_checkTrailingBracket2 = verifyNot checkTrailingBracket "if [[ -z n ]]; then true; fi "
-prop_checkTrailingBracket3 = verify checkTrailingBracket "a || b ] && thing"
-prop_checkTrailingBracket4 = verifyNot checkTrailingBracket "run [ foo ]"
-prop_checkTrailingBracket5 = verifyNot checkTrailingBracket "run bar ']'"
-checkTrailingBracket _ token =
-    case token of
-        T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token
-        otherwise -> return ()
-  where
-    check t command =
-        case t of
-            T_NormalWord id [T_Literal _ str] -> potentially $ do
-                guard $ str `elem` [ "]]", "]" ]
-                let opposite = invert str
-                    parameters = oversimplify command
-                guard $ opposite `notElem` parameters
-                return $ warn id 2171 $
-                    "Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?"
-            otherwise -> return ()
-    invert s =
-        case s of
-            "]]" -> "[["
-            "]" -> "["
-            x -> x
-
-prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
-prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
-prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
-prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
-prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
-prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
-checkNonportableSignals _ = checkUnqualifiedCommand "trap" (const f)
-  where
-    f = mapM_ check
-    check param = potentially $ do
-        str <- getLiteralString param
-        let id = getId param
-        return $ sequence_ $ mapMaybe (\f -> f id str) [
-            checkNumeric,
-            checkUntrappable
-            ]
-
-    checkNumeric id str = do
-        guard $ not (null str)
-        guard $ all isDigit str
-        guard $ str /= "0" -- POSIX exit trap
-        guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
-        return $ warn id 2172
-            "Trapping signals by number is not well defined. Prefer signal names."
-
-    checkUntrappable id str = do
-        guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
-        return $ err id 2173
-            "SIGKILL/SIGSTOP can not be trapped."
-
-prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
-prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
-prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
-prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
-prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
-prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
-prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
-prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
-prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
-prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
-prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
-prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
-prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
-prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
-prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
-checkMkdirDashPM _ t@(T_SimpleCommand _ _ args) = potentially $ do
-    name <- getCommandName t
-    guard $ name == "mkdir"
-    dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
-    dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
-    guard $ any couldHaveSubdirs (drop 1 args) -- mkdir -pm 0700 dir  is fine, but dir/subdir is not.
-    return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
-  where
-    flags = getAllFlags t
-    couldHaveSubdirs t = fromMaybe True $ do
-        name <- getLiteralString t
-        return $ '/' `elem` name
-checkMkdirDashPM _ _ = return ()
+import ShellCheck.AnalyzerLib
+import ShellCheck.Data
+import ShellCheck.Parser
+import ShellCheck.Interface
+import ShellCheck.Regex
+
+import Control.Arrow (first)
+import Control.Monad
+import Control.Monad.Identity
+import Control.Monad.State
+import Control.Monad.Writer
+import Control.Monad.Reader
+import Data.Char
+import Data.Functor
+import Data.Function (on)
+import Data.List
+import Data.Maybe
+import Data.Ord
+import Debug.Trace
+import qualified Data.Map as Map
+import Test.QuickCheck.All (forAllProperties)
+import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
+
+-- Checks that are run on the AST root
+treeChecks :: [Parameters -> Token -> [TokenComment]]
+treeChecks = [
+    runNodeAnalysis
+        (\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p))
+            (nodeChecks ++ checksFor (shellType p))))
+    ,subshellAssignmentCheck
+    ,checkSpacefulness
+    ,checkQuotesInLiterals
+    ,checkShebangParameters
+    ,checkFunctionsUsedExternally
+    ,checkUnusedAssignments
+    ,checkUnpassedInFunctions
+    ,checkArrayWithoutIndex
+    ,checkShebang
+    ,checkUnassignedReferences
+    ,checkUncheckedCd
+    ]
+
+checksFor Sh = [
+    checkBashisms
+    ,checkTimeParameters
+    ,checkForDecimals
+    ,checkTimedCommand
+    ]
+checksFor Dash = [
+    checkBashisms
+    ,checkForDecimals
+    ,checkLocalScope
+    ,checkTimedCommand
+    ]
+checksFor Ksh = [
+    checkEchoSed
+    ]
+checksFor Bash = [
+    checkTimeParameters
+    ,checkBraceExpansionVars
+    ,checkEchoSed
+    ,checkForDecimals
+    ,checkLocalScope
+    ]
+
+runAnalytics :: AnalysisSpec -> [TokenComment]
+runAnalytics options =
+        runList options treeChecks
+
+runList :: AnalysisSpec -> [Parameters -> Token -> [TokenComment]]
+    -> [TokenComment]
+runList spec list = notes
+    where
+        root = asScript spec
+        params = makeParameters spec
+        notes = concatMap (\f -> f params root) list
+
+
+checkList l t = concatMap (\f -> f t) l
+
+
+-- Checks that are run on each node in the AST
+runNodeAnalysis f p t = execWriter (doAnalysis (f p) t)
+
+nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()]
+nodeChecks = [
+    checkUuoc
+    ,checkPipePitfalls
+    ,checkForInQuoted
+    ,checkForInLs
+    ,checkShorthandIf
+    ,checkDollarStar
+    ,checkUnquotedDollarAt
+    ,checkStderrRedirect
+    ,checkUnquotedN
+    ,checkNumberComparisons
+    ,checkSingleBracketOperators
+    ,checkDoubleBracketOperators
+    ,checkLiteralBreakingTest
+    ,checkConstantNoary
+    ,checkDivBeforeMult
+    ,checkArithmeticDeref
+    ,checkArithmeticBadOctal
+    ,checkComparisonAgainstGlob
+    ,checkCommarrays
+    ,checkOrNeq
+    ,checkEchoWc
+    ,checkConstantIfs
+    ,checkPipedAssignment
+    ,checkAssignAteCommand
+    ,checkUuoeVar
+    ,checkQuotedCondRegex
+    ,checkForInCat
+    ,checkFindExec
+    ,checkValidCondOps
+    ,checkGlobbedRegex
+    ,checkTestRedirects
+    ,checkIndirectExpansion
+    ,checkSudoRedirect
+    ,checkPS1Assignments
+    ,checkBackticks
+    ,checkInexplicablyUnquoted
+    ,checkTildeInQuotes
+    ,checkLonelyDotDash
+    ,checkSpuriousExec
+    ,checkSpuriousExpansion
+    ,checkDollarBrackets
+    ,checkSshHereDoc
+    ,checkGlobsAsOptions
+    ,checkWhileReadPitfalls
+    ,checkArithmeticOpCommand
+    ,checkCharRangeGlob
+    ,checkUnquotedExpansions
+    ,checkSingleQuotedVariables
+    ,checkRedirectToSame
+    ,checkPrefixAssignmentReference
+    ,checkLoopKeywordScope
+    ,checkCdAndBack
+    ,checkWrongArithmeticAssignment
+    ,checkConditionalAndOrs
+    ,checkFunctionDeclarations
+    ,checkCatastrophicRm
+    ,checkStderrPipe
+    ,checkOverridingPath
+    ,checkArrayAsString
+    ,checkUnsupported
+    ,checkMultipleAppends
+    ,checkSuspiciousIFS
+    ,checkShouldUseGrepQ
+    ,checkTestGlobs
+    ,checkConcatenatedDollarAt
+    ,checkTildeInPath
+    ,checkMaskedReturns
+    ,checkReadWithoutR
+    ,checkLoopVariableReassignment
+    ,checkTrailingBracket
+    ]
+
+
+wouldHaveBeenGlob s = '*' `elem` s
+
+verify :: (Parameters -> Token -> Writer [TokenComment] ()) -> String -> Bool
+verify f s = checkNode f s == Just True
+
+verifyNot :: (Parameters -> Token -> Writer [TokenComment] ()) -> String -> Bool
+verifyNot f s = checkNode f s == Just False
+
+verifyTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool
+verifyTree f s = producesComments f s == Just True
+
+verifyNotTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool
+verifyNotTree f s = producesComments f s == Just False
+
+checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
+    when (t `isCommand` str) $ f cmd rest
+checkCommand _ _ _ = return ()
+
+checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
+    when (t `isUnqualifiedCommand` str) $ f cmd rest
+checkUnqualifiedCommand _ _ _ = return ()
+
+
+checkNode f = producesComments (runNodeAnalysis f)
+producesComments :: (Parameters -> Token -> [TokenComment]) -> String -> Maybe Bool
+producesComments f s = do
+        root <- pScript s
+        return . not . null $ runList (defaultSpec root) [f]
+
+-- Copied from https://wiki.haskell.org/Edit_distance
+dist :: Eq a => [a] -> [a] -> Int
+dist a b
+    = last (if lab == 0 then mainDiag
+            else if lab > 0 then lowers !! (lab - 1)
+                 else{- < 0 -}   uppers !! (-1 - lab))
+    where mainDiag = oneDiag a b (head uppers) (-1 : head lowers)
+          uppers = eachDiag a b (mainDiag : uppers) -- upper diagonals
+          lowers = eachDiag b a (mainDiag : lowers) -- lower diagonals
+          eachDiag a [] diags = []
+          eachDiag a (bch:bs) (lastDiag:diags) = oneDiag a bs nextDiag lastDiag : eachDiag a bs diags
+              where nextDiag = head (tail diags)
+          oneDiag a b diagAbove diagBelow = thisdiag
+              where doDiag [] b nw n w = []
+                    doDiag a [] nw n w = []
+                    doDiag (ach:as) (bch:bs) nw n w = me : doDiag as bs me (tail n) (tail w)
+                        where me = if ach == bch then nw else 1 + min3 (head w) nw (head n)
+                    firstelt = 1 + head diagBelow
+                    thisdiag = firstelt : doDiag a b firstelt diagAbove (tail diagBelow)
+          lab = length a - length b
+          min3 x y z = if x < y then x else min y z
+
+hasFloatingPoint params = shellType params == Ksh
+
+-- Checks whether the current parent path is part of a condition
+isCondition [] = False
+isCondition [_] = False
+isCondition (child:parent:rest) =
+    getId child `elem` map getId (getConditionChildren parent) || isCondition (parent:rest)
+  where
+    getConditionChildren t =
+        case t of
+            T_AndIf _ left right -> [left]
+            T_OrIf id left right -> [left]
+            T_IfExpression id conditions elses -> concatMap (take 1 . reverse . fst) conditions
+            T_WhileExpression id c l -> take 1 . reverse $ c
+            T_UntilExpression id c l -> take 1 . reverse $ c
+            _ -> []
+
+prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)"
+checkEchoWc _ (T_Pipeline id _ [a, b]) =
+    when (acmd == ["echo", "${VAR}"]) $
+        case bcmd of
+            ["wc", "-c"] -> countMsg
+            ["wc", "-m"] -> countMsg
+            _ -> return ()
+  where
+    acmd = oversimplify a
+    bcmd = oversimplify b
+    countMsg = style id 2000 "See if you can use ${#variable} instead."
+checkEchoWc _ _ = return ()
+
+prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
+prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
+checkEchoSed _ (T_Pipeline id _ [a, b]) =
+    when (acmd == ["echo", "${VAR}"]) $
+        case bcmd of
+            ["sed", v] -> checkIn v
+            ["sed", "-e", v] -> checkIn v
+            _ -> return ()
+  where
+    -- This should have used backreferences, but TDFA doesn't support them
+    sedRe = mkRegex "^s(.)([^\n]*)g?$"
+    isSimpleSed s = fromMaybe False $ do
+        [first,rest] <- matchRegex sedRe s
+        let delimiters = filter (== (head first)) rest
+        guard $ length delimiters == 2
+        return True
+
+    acmd = oversimplify a
+    bcmd = oversimplify b
+    checkIn s =
+        when (isSimpleSed s) $
+            style id 2001 "See if you can use ${variable//search/replace} instead."
+checkEchoSed _ _ = return ()
+
+prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo"
+prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo"
+prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo"
+checkPipedAssignment _ (T_Pipeline _ _ (T_Redirecting _ _ (T_SimpleCommand id (_:_) []):_:_)) =
+    warn id 2036 "If you wanted to assign the output of the pipeline, use a=$(b | c) ."
+checkPipedAssignment _ _ = return ()
+
+prop_checkAssignAteCommand1 = verify checkAssignAteCommand "A=ls -l"
+prop_checkAssignAteCommand2 = verify checkAssignAteCommand "A=ls --sort=$foo"
+prop_checkAssignAteCommand3 = verify checkAssignAteCommand "A=cat foo | grep bar"
+prop_checkAssignAteCommand4 = verifyNot checkAssignAteCommand "A=foo ls -l"
+prop_checkAssignAteCommand5 = verifyNot checkAssignAteCommand "PAGER=cat grep bar"
+checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) (firstWord:_)) =
+    when ("-" `isPrefixOf` concat (oversimplify firstWord) ||
+        isCommonCommand (getLiteralString assignmentTerm)
+            && not (isCommonCommand (getLiteralString firstWord))) $
+                warn id 2037 "To assign the output of a command, use var=$(cmd) ."
+  where
+    isCommonCommand (Just s) = s `elem` commonCommands
+    isCommonCommand _ = False
+checkAssignAteCommand _ _ = return ()
+
+prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1"
+prop_checkArithmeticOpCommand2 = verify checkArithmeticOpCommand "foo=bar * 2"
+prop_checkArithmeticOpCommand3 = verifyNot checkArithmeticOpCommand "foo + opts"
+checkArithmeticOpCommand _ (T_SimpleCommand id [T_Assignment {}] (firstWord:_)) =
+    fromMaybe (return ()) $ check <$> getGlobOrLiteralString firstWord
+  where
+    check op =
+        when (op `elem` ["+", "-", "*", "/"]) $
+            warn (getId firstWord) 2099 $
+                "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
+checkArithmeticOpCommand _ _ = return ()
+
+prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1"
+prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2"
+checkWrongArithmeticAssignment params (T_SimpleCommand id (T_Assignment _ _ _ _ val:[]) []) =
+  fromMaybe (return ()) $ do
+    str <- getNormalString val
+    match <- matchRegex regex str
+    var <- match !!! 0
+    op <- match !!! 1
+    Map.lookup var references
+    return . warn (getId val) 2100 $
+        "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
+  where
+    regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$"
+    references = foldl (flip ($)) Map.empty (map insertRef $ variableFlow params)
+    insertRef (Assignment (_, _, name, _)) =
+        Map.insert name ()
+    insertRef _ = Prelude.id
+
+    getNormalString (T_NormalWord _ words) = do
+        parts <- foldl (liftM2 (\x y -> x ++ [y])) (Just []) $ map getLiterals words
+        return $ concat parts
+    getNormalString _ = Nothing
+
+    getLiterals (T_Literal _ s) = return s
+    getLiterals (T_Glob _ s) = return s
+    getLiterals _ = Nothing
+checkWrongArithmeticAssignment _ _ = return ()
+
+
+prop_checkUuoc1 = verify checkUuoc "cat foo | grep bar"
+prop_checkUuoc2 = verifyNot checkUuoc "cat * | grep bar"
+prop_checkUuoc3 = verify checkUuoc "cat $var | grep bar"
+prop_checkUuoc4 = verifyNot checkUuoc "cat $var"
+prop_checkUuoc5 = verifyNot checkUuoc "cat \"$@\""
+prop_checkUuoc6 = verifyNot checkUuoc "cat -n | grep bar"
+checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) =
+    checkCommand "cat" (const f) cmd
+  where
+    f [word] = unless (mayBecomeMultipleArgs word || isOption word) $
+        style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead."
+    f _ = return ()
+    isOption word = "-" `isPrefixOf` onlyLiteralString word
+checkUuoc _ _ = return ()
+
+prop_checkPipePitfalls3 = verify checkPipePitfalls "ls | grep -v mp3"
+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 = oversimplify xargs ++ oversimplify find
+          in
+            unless (any ($ 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?"
+
+    for' ["ps", "grep"] $
+        \x -> info x 2009 "Consider using pgrep instead of grepping ps output."
+
+    for' ["grep", "wc"] $
+        \x -> style x 2126 "Consider using grep -c instead of grep|wc."
+
+    didLs <- liftM or . sequence $ [
+        for' ["ls", "grep"] $
+            \x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.",
+        for' ["ls", "xargs"] $
+            \x -> warn x 2011 "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames."
+        ]
+    unless didLs $ do
+        for ["ls", "?"] $
+            \(ls:_) -> unless (hasShortParameter 'N' (oversimplify 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 "" . oversimplify) commands)
+        in do
+            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 char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
+    hasParameter string =
+        any (isPrefixOf string . dropWhile (== '-'))
+checkPipePitfalls _ _ = return ()
+
+indexOfSublists sub = f 0
+  where
+    f _ [] = []
+    f n a@(r:rest) =
+        let others = f (n+1) rest in
+            if match sub a
+              then n:others
+              else others
+    match ("?":r1) (_:r2) = match r1 r2
+    match (x1:r1) (x2:r2) | x1 == x2 = match r1 r2
+    match [] _ = True
+    match _ _ = False
+
+
+prop_checkShebangParameters1 = verifyTree checkShebangParameters "#!/usr/bin/env bash -x\necho cow"
+prop_checkShebangParameters2 = verifyNotTree checkShebangParameters "#! /bin/sh  -l "
+checkShebangParameters p (T_Annotation _ _ t) = checkShebangParameters p t
+checkShebangParameters _ (T_Script id sb _) =
+    [makeComment ErrorC id 2096 "On most OS, shebangs can only specify a single parameter." | length (words sb) > 2]
+
+prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho cow"
+prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh  -l "
+prop_checkShebang3 = verifyTree checkShebang "ls -l"
+checkShebang params (T_Annotation _ _ t) = checkShebang params t
+checkShebang params (T_Script id sb _) =
+    [makeComment ErrorC id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
+        | not (shellTypeSpecified params) && sb == "" ]
+
+prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
+prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
+prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
+prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
+prop_checkBashisms5 = verify checkBashisms "source file"
+prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
+prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
+prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
+prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
+prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
+prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
+prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
+prop_checkBashisms13= verify checkBashisms "exec -c env"
+prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
+prop_checkBashisms15= verify checkBashisms "let n++"
+prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
+prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
+prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
+prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
+prop_checkBashisms20= verify checkBashisms "read -ra foo"
+prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
+prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
+prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
+prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
+prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
+prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
+prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
+prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
+prop_checkBashisms29= verify checkBashisms "echo ${!var}"
+prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
+prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
+prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
+prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
+prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
+prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
+prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
+prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
+prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
+prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
+prop_checkBashisms40= verify checkBashisms "echo $(<file)"
+prop_checkBashisms41= verify checkBashisms "echo `<file`"
+prop_checkBashisms42= verify checkBashisms "trap foo int"
+prop_checkBashisms43= verify checkBashisms "trap foo sigint"
+prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
+prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
+prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
+prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
+prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
+prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
+checkBashisms params = bashism
+  where
+    isDash = shellType params == Dash
+    warnMsg id s =
+        if isDash
+        then warn id 2169 $ "In dash, " ++ s ++ " not supported."
+        else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
+
+    bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
+    bashism (T_Extglob id _ _) = warnMsg id "extglob is"
+    bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
+    bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
+    bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
+    bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
+    bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
+    bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
+    bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
+    bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
+    bashism (T_HereString id _) = warnMsg id "here-strings are"
+    bashism (TC_Binary id SingleBracket op _ _)
+        | op `elem` [ "-nt", "-ef", "\\<", "\\>"] =
+            unless isDash $ warnMsg id $ op ++ " is"
+    bashism (TC_Binary id SingleBracket "==" _ _) =
+            warnMsg id "== in place of = is"
+    bashism (TC_Unary id _ "-a" _) =
+            warnMsg id "unary -a in place of -e is"
+    bashism (TA_Unary id op _)
+        | op `elem` [ "|++", "|--", "++|", "--|"] =
+            warnMsg id $ filter (/= '|') op ++ " is"
+    bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
+    bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
+    bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
+    bashism (T_FdRedirect id num _)
+        | all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
+    bashism (T_IoFile id _ word) | isNetworked =
+            warnMsg id "/dev/{tcp,udp} is"
+        where
+            file = onlyLiteralString word
+            isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
+    bashism (T_Glob id str) | "[^" `isInfixOf` str =
+            warnMsg id "^ in place of ! in glob bracket expressions is"
+
+    bashism t@(TA_Expansion id _) | isBashism =
+        warnMsg id $ fromJust str ++ " is"
+      where
+        str = getLiteralString t
+        isBashism = isJust str && isBashVariable (fromJust str)
+    bashism t@(T_DollarBraced id token) = do
+        mapM_ check expansion
+        when (isBashVariable var) $
+                    warnMsg id $ var ++ " is"
+      where
+        str = bracedString t
+        var = getBracedReference str
+        check (regex, feature) =
+            when (isJust $ matchRegex regex str) $ warnMsg id feature
+
+    bashism t@(T_Pipe id "|&") =
+        warnMsg id "|& in place of 2>&1 | is"
+    bashism (T_Array id _) =
+        warnMsg id "arrays are"
+    bashism (T_IoFile id _ t) | isGlob t =
+        warnMsg id "redirecting to/from globs is"
+    bashism (T_CoProc id _ _) =
+        warnMsg id "coproc is"
+
+    bashism (T_Function id _ _ str _) | not (isVariableName str) =
+        warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
+
+    bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
+        warnMsg id "$(<file) to read files is"
+    bashism (T_Backticked id [x]) | isOnlyRedirection x =
+        warnMsg id "`<file` to read files is"
+
+    bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
+        | t `isCommand` "echo" && "-" `isPrefixOf` argString =
+            unless ("--" `isPrefixOf` argString) $ -- echo "-----"
+                if isDash
+                then
+                    when (argString /= "-n") $
+                        warnMsg (getId arg) "echo flags besides -n"
+                else
+                    warnMsg (getId arg) "echo flags are"
+      where argString = concat $ oversimplify arg
+    bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
+        | t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
+            warnMsg (getId arg) "exec flags are"
+    bashism t@(T_SimpleCommand id _ _)
+        | t `isCommand` "let" = warnMsg id "'let' is"
+
+    bashism t@(T_SimpleCommand id _ (cmd:rest)) =
+        let name = fromMaybe "" $ getCommandName t
+            flags = getLeadingFlags t
+        in do
+            when (name `elem` unsupportedCommands) $
+                warnMsg id $ "'" ++ name ++ "' is"
+            potentially $ do
+                allowed <- Map.lookup name allowedFlags
+                (word, flag) <- listToMaybe $
+                    filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
+                return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
+
+            when (name == "source") $ warnMsg id "'source' in place of '.' is"
+            when (name == "trap") $
+                let
+                    check token = potentially $ do
+                        str <- getLiteralString token
+                        let upper = map toUpper str
+                        return $ do
+                            when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
+                                warnMsg (getId token) $ "trapping " ++ str ++ " is"
+                            when ("SIG" `isPrefixOf` upper) $
+                                warnMsg (getId token)
+                                    "prefixing signal names with 'SIG' is"
+                            when (not isDash && upper /= str) $
+                                warnMsg (getId token)
+                                    "using lower/mixed case for signal names is"
+                in
+                    mapM_ check (drop 1 rest)
+
+            when (name == "printf") $ potentially $ do
+                format <- rest !!! 0  -- flags are covered by allowedFlags
+                let literal = onlyLiteralString format
+                guard $ "%q" `isInfixOf` literal
+                return $ warnMsg (getId format) "printf %q is"
+      where
+        unsupportedCommands = [
+            "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
+            "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
+            "typeset"
+            ] ++ if not isDash then ["local", "type"] else []
+        allowedFlags = Map.fromList [
+            ("read", if isDash then ["r", "p"] else ["r"]),
+            ("ulimit", ["f"]),
+            ("printf", []),
+            ("exec", [])
+            ]
+
+    bashism _ = return ()
+
+    varChars="_0-9a-zA-Z"
+    expansion = let re = mkRegex in [
+        (re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
+        (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
+        (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
+        (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
+        (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
+        (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
+        ]
+    bashVars = [
+        "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
+        "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
+        ]
+    bashDynamicVars = [ "RANDOM", "SECONDS" ]
+    dashVars = [ "LINENO" ]
+    isBashVariable var =
+        (var `elem` bashDynamicVars
+            || var `elem` bashVars && not (isAssigned var))
+        && not (isDash && var `elem` dashVars)
+    isAssigned var = any f (variableFlow params)
+      where
+        f x = case x of
+                Assignment (_, _, name, _) -> name == var
+                _ -> False
+
+
+prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
+prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done"
+prop_checkForInQuoted2a = verifyNot checkForInQuoted "for f in *.mp3; do echo foo; done"
+prop_checkForInQuoted2b = verify checkForInQuoted "for f in \"*.mp3\"; do echo foo; done"
+prop_checkForInQuoted3 = verify checkForInQuoted "for f in 'find /'; do true; done"
+prop_checkForInQuoted4 = verify checkForInQuoted "for f in 1,2,3; do true; done"
+prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done"
+prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done"
+prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done"
+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."
+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 " ++ f ++ "='" ++ s ++ "'."
+checkForInQuoted _ _ = return ()
+
+prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"
+prop_checkForInCat1a= verify checkForInCat "for f in `cat foo`; do stuff; done"
+prop_checkForInCat2 = verify checkForInCat "for f in $(cat foo | grep lol); do stuff; done"
+prop_checkForInCat2a= verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done"
+prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done"
+checkForInCat _ (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w
+  where
+    checkF (T_DollarExpansion id [T_Pipeline _ _ r])
+        | all isLineBased r =
+            info id 2013 "To read lines rather than words, pipe/redirect to a 'while read' loop."
+    checkF (T_Backticked id cmds) = checkF (T_DollarExpansion id cmds)
+    checkF _ = return ()
+    isLineBased cmd = any (cmd `isCommand`)
+                        ["grep", "fgrep", "egrep", "sed", "cat", "awk", "cut", "sort"]
+checkForInCat _ _ = return ()
+
+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 _ = try
+  where
+   try (T_ForIn _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) =
+        check id f x
+   try (T_ForIn _ f [T_NormalWord _ [T_Backticked id [x]]] _) =
+        check id f x
+   try _ = return ()
+   check id f x =
+    case oversimplify x of
+      ("ls":n) ->
+        let warntype = if any ("-" `isPrefixOf`) n then warn else err in
+          warntype id 2045 "Iterating over ls output is fragile. Use globs."
+      ("find":_) -> warn id 2044 "For loops over find output are fragile. Use find -exec or a while read loop."
+      _ -> return ()
+
+
+prop_checkFindExec1 = verify checkFindExec "find / -name '*.php' -exec rm {};"
+prop_checkFindExec2 = verify checkFindExec "find / -exec touch {} && ls {} \\;"
+prop_checkFindExec3 = verify checkFindExec "find / -execdir cat {} | grep lol +"
+prop_checkFindExec4 = verifyNot checkFindExec "find / -name '*.php' -exec foo {} +"
+prop_checkFindExec5 = verifyNot checkFindExec "find / -execdir bash -c 'a && b' \\;"
+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 $
+        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)
+        case getLiteralString w of
+            Just "-exec" -> broken r True
+            Just "-execdir" -> broken r True
+            Just "+" -> broken r False
+            Just ";" -> broken r False
+            _ -> broken r v
+
+    shouldWarn x =
+      case x of
+        T_DollarExpansion _ _ -> True
+        T_Backticked _ _ -> True
+        T_Glob _ _ -> True
+        T_Extglob {} -> True
+        _ -> False
+
+    warnFor x =
+        when(shouldWarn x) $
+            info (getId x) 2014 "This will expand once before find runs, not per file found."
+
+    fromWord (T_NormalWord _ l) = l
+    fromWord _ = []
+checkFindExec _ _ = return ()
+
+
+prop_checkUnquotedExpansions1 = verify checkUnquotedExpansions "rm $(ls)"
+prop_checkUnquotedExpansions1a= verify checkUnquotedExpansions "rm `ls`"
+prop_checkUnquotedExpansions2 = verify checkUnquotedExpansions "rm foo$(date)"
+prop_checkUnquotedExpansions3 = verify checkUnquotedExpansions "[ $(foo) == cow ]"
+prop_checkUnquotedExpansions3a= verify checkUnquotedExpansions "[ ! $(foo) ]"
+prop_checkUnquotedExpansions4 = verifyNot checkUnquotedExpansions "[[ $(foo) == cow ]]"
+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 =
+    check
+  where
+    check t@(T_DollarExpansion _ _) = examine t
+    check t@(T_Backticked _ _) = examine t
+    check t@(T_DollarBraceCommandExpansion _ _) = examine t
+    check _ = return ()
+    tree = parentMap params
+    examine t =
+        unless (isQuoteFree tree t || usedAsCommandName tree t) $
+            warn (getId t) 2046 "Quote this to prevent word splitting."
+
+
+prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo"
+prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/g' > lol"
+prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol"
+prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null"
+prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
+prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
+prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
+checkRedirectToSame params s@(T_Pipeline _ _ list) =
+    mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
+  where
+    note x = makeComment InfoC x 2094
+                "Make sure not to read and write the same file in the same pipeline."
+    checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) =
+        when (exceptId /= newId
+                && x == y
+                && not (isOutput t && isOutput u)
+                && not (special t)
+                && not (any isHarmlessCommand [t,u])) $ do
+            addComment $ note newId
+            addComment $ note exceptId
+    checkOccurrences _ _ = return ()
+    getAllRedirs = concatMap (\t ->
+        case t of
+            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 (oversimplify x)
+    isOutput t =
+        case drop 1 $ getPath (parentMap params) t of
+            T_IoFile _ op _:_ ->
+                case op of
+                    T_Greater _  -> True
+                    T_DGREAT _ -> True
+                    _ -> False
+            _ -> False
+    isHarmlessCommand arg = fromMaybe False $ do
+        cmd <- getClosestCommand (parentMap params) arg
+        name <- getCommandBasename cmd
+        return $ name `elem` ["echo", "printf", "sponge"]
+
+checkRedirectToSame _ _ = return ()
+
+
+prop_checkShorthandIf  = verify checkShorthandIf "[[ ! -z file ]] && scp file host || rm file"
+prop_checkShorthandIf2 = verifyNot checkShorthandIf "[[ ! -z file ]] && { scp file host || echo 'Eek'; }"
+prop_checkShorthandIf3 = verifyNot checkShorthandIf "foo && bar || echo baz"
+prop_checkShorthandIf4 = verifyNot checkShorthandIf "foo && a=b || a=c"
+prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b"
+prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi"
+prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done"
+prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi"
+checkShorthandIf params x@(T_AndIf id _ (T_OrIf _ _ (T_Pipeline _ _ t)))
+        | not (isOk t || inCondition) =
+    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
+        name <- getCommandBasename t
+        return $ name `elem` ["echo", "exit", "return", "printf"])
+    isOk _ = False
+    inCondition = isCondition $ getPath (parentMap params) x
+checkShorthandIf _ _ = return ()
+
+
+prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done"
+prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"
+checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _)])
+      | bracedString b == "*"  =
+    unless isAssigned $
+        warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."
+  where
+    path = getPath (parentMap p) t
+    isAssigned = any isAssignment . take 2 $ path
+checkDollarStar _ _ = return ()
+
+
+prop_checkUnquotedDollarAt = verify checkUnquotedDollarAt "ls $@"
+prop_checkUnquotedDollarAt1= verifyNot checkUnquotedDollarAt "ls ${#@}"
+prop_checkUnquotedDollarAt2 = verify checkUnquotedDollarAt "ls ${foo[@]}"
+prop_checkUnquotedDollarAt3 = verifyNot checkUnquotedDollarAt "ls ${#foo[@]}"
+prop_checkUnquotedDollarAt4 = verifyNot checkUnquotedDollarAt "ls \"$@\""
+prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }"
+prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@"
+prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done"
+prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\""
+prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}"
+checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word =
+    forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->
+        unless (isAlternative x) $
+            err (getId x) 2068
+                "Double quote array expansions to avoid re-splitting elements."
+  where
+    -- Fixme: should detect whether the alternative is quoted
+    isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b
+    isAlternative _ = False
+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[@]}\""
+prop_checkArrayAsString3 = verify checkArrayAsString "a=*.png"
+prop_checkArrayAsString4 = verify checkArrayAsString "a={1..10}"
+prop_checkArrayAsString5 = verifyNot checkArrayAsString "a='*.gif'"
+prop_checkArrayAsString6 = verifyNot checkArrayAsString "a=$*"
+prop_checkArrayAsString7 = verifyNot checkArrayAsString "a=( $@ )"
+checkArrayAsString _ (T_Assignment id _ _ _ word) =
+    if willConcatInAssignment word
+    then
+      warn (getId word) 2124
+        "Assigning an array to a string! Assign as array, or use * instead of @ to concatenate."
+    else
+      when (willBecomeMultipleArgs word) $
+        warn (getId word) 2125
+          "Brace expansions and globs are literal in assignments. Quote it or use an array."
+checkArrayAsString _ _ = return ()
+
+prop_checkArrayWithoutIndex1 = verifyTree checkArrayWithoutIndex "foo=(a b); echo $foo"
+prop_checkArrayWithoutIndex2 = verifyNotTree checkArrayWithoutIndex "foo='bar baz'; foo=($foo); echo ${foo[0]}"
+prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo"
+prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC"
+prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo $a"
+prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
+prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
+prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
+checkArrayWithoutIndex params _ =
+    doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
+  where
+    defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables
+    readF _ (T_DollarBraced id token) _ = do
+        map <- get
+        return . maybeToList $ do
+            name <- getLiteralString token
+            assigned <- Map.lookup name map
+            return $ makeComment WarningC id 2128
+                    "Expanding an array without an index only gives the first element."
+    readF _ _ _ = return []
+
+    writeF _ (T_Assignment id mode name Nothing _) _ (DataString _) = do
+        isArray <- gets (isJust . Map.lookup name)
+        return $ if not isArray then [] else
+            case mode of
+                Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."]
+                Append -> [makeComment WarningC id 2179 "Use array+=(\"item\") to append items to an array."]
+
+    writeF _ t name (DataArray _) = do
+        modify (Map.insert name ())
+        return []
+    writeF _ expr name _ = do
+        if isIndexed expr
+          then modify (Map.insert name ())
+          else modify (Map.delete name)
+        return []
+
+    isIndexed expr =
+        case expr of
+            T_Assignment _ _ _ (Just _) _ -> True
+            _ -> False
+
+prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow"
+prop_checkStderrRedirect2 = verifyNot checkStderrRedirect "test > cow 2>&1"
+prop_checkStderrRedirect3 = verifyNot checkStderrRedirect "test 2>&1 > file | grep stderr"
+prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
+prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
+prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
+checkStderrRedirect params redir@(T_Redirecting _ [
+    T_FdRedirect id "2" (T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "1"])),
+    T_FdRedirect _ _ (T_IoFile _ op _)
+    ] _) = case op of
+            T_Greater _ -> error
+            T_DGREAT _ -> error
+            _ -> return ()
+  where
+    usesOutput t =
+        case t of
+            (T_Pipeline _ _ list) -> length list > 1 && not (isParentOf (parentMap params) (last list) redir)
+            (T_ProcSub {}) -> True
+            (T_DollarExpansion {}) -> True
+            (T_Backticked {}) -> True
+            _ -> False
+    isCaptured = any usesOutput $ getPath (parentMap params) redir
+
+    error = unless isCaptured $
+        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 ("Tracing " ++ show x) x
+ltt t = trace ("Tracing " ++ show t)
+
+
+prop_checkSingleQuotedVariables  = verify checkSingleQuotedVariables "echo '$foo'"
+prop_checkSingleQuotedVariables2 = verify checkSingleQuotedVariables "echo 'lol$1.jpg'"
+prop_checkSingleQuotedVariables3 = verifyNot checkSingleQuotedVariables "sed 's/foo$/bar/'"
+prop_checkSingleQuotedVariables3a= verify checkSingleQuotedVariables "sed 's/${foo}/bar/'"
+prop_checkSingleQuotedVariables3b= verify checkSingleQuotedVariables "sed 's/$(echo cow)/bar/'"
+prop_checkSingleQuotedVariables3c= verify checkSingleQuotedVariables "sed 's/$((1+foo))/bar/'"
+prop_checkSingleQuotedVariables4 = verifyNot checkSingleQuotedVariables "awk '{print $1}'"
+prop_checkSingleQuotedVariables5 = verifyNot checkSingleQuotedVariables "trap 'echo $SECONDS' EXIT"
+prop_checkSingleQuotedVariables6 = verifyNot checkSingleQuotedVariables "sed -n '$p'"
+prop_checkSingleQuotedVariables6a= verify checkSingleQuotedVariables "sed -n '$pattern'"
+prop_checkSingleQuotedVariables7 = verifyNot checkSingleQuotedVariables "PS1='$PWD \\$ '"
+prop_checkSingleQuotedVariables8 = verify checkSingleQuotedVariables "find . -exec echo '$1' {} +"
+prop_checkSingleQuotedVariables9 = verifyNot checkSingleQuotedVariables "find . -exec awk '{print $1}' {} \\;"
+prop_checkSingleQuotedVariables10= verify checkSingleQuotedVariables "echo '`pwd`'"
+prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'"
+prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
+checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
+    when (s `matches` re) $
+        if "sed" == commandName
+        then unless (s `matches` sedContra) showMessage
+        else unless isProbablyOk showMessage
+  where
+    parents = parentMap params
+    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 $ if name == "find" then getFindCommand cmd else name
+
+    isProbablyOk =
+            any isOkAssignment (take 3 $ getPath parents t)
+            || commandName `elem` [
+                "trap"
+                ,"sh"
+                ,"bash"
+                ,"ksh"
+                ,"zsh"
+                ,"ssh"
+                ,"eval"
+                ,"xprop"
+                ,"alias"
+                ,"sudo" -- covering "sudo sh" and such
+                ,"dpkg-query"
+                ]
+            || "awk" `isSuffixOf` commandName
+            || "perl" `isPrefixOf` commandName
+
+    commonlyQuoted = ["PS1", "PS2", "PS3", "PS4", "PROMPT_COMMAND"]
+    isOkAssignment t =
+        case t of
+            T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted
+            otherwise -> False
+
+    re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`"
+    sedContra = mkRegex "\\$[{dpsaic]($|[^a-zA-Z])"
+
+    getFindCommand (T_SimpleCommand _ _ words) =
+        let list = map getLiteralString words
+            cmd  = dropWhile (\x -> x /= Just "-exec" && x /= Just "-execdir") list
+        in
+          case cmd of
+            (flag:cmd:rest) -> fromMaybe "find" cmd
+            _ -> "find"
+    getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
+    getFindCommand _ = "find"
+checkSingleQuotedVariables _ _ = return ()
+
+
+prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
+prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
+prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
+prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
+checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
+       err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
+checkUnquotedN _ _ = return ()
+
+prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"
+prop_checkNumberComparisons2 = verify checkNumberComparisons "[[ 0 >= $(cmd) ]]"
+prop_checkNumberComparisons3 = verifyNot checkNumberComparisons "[[ $foo ]] > 3"
+prop_checkNumberComparisons4 = verify checkNumberComparisons "[[ $foo > 2.72 ]]"
+prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]"
+prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]"
+prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]"
+prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]"
+prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"
+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)
+      then do
+        when (isLtGt op) $
+          err id 2071 $
+            op ++ " is for string comparisons. Use " ++ eqv op ++ " instead."
+        when (isLeGe op) $
+            err id 2071 $ op ++ " is not a valid operator. " ++
+              "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."
+
+    when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do
+        mapM_ checkDecimals [lhs, rhs]
+        when (typ == SingleBracket) $
+            checkStrings [lhs, rhs]
+
+  where
+      isLtGt = flip elem ["<", "\\<", ">", "\\>"]
+      isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="]
+
+      checkDecimals hs =
+        when (isFraction hs && not (hasFloatingPoint params)) $
+            err (getId hs) 2072 decimalError
+      decimalError = "Decimals are not supported. " ++
+        "Either use integers only, or use bc or awk to compare."
+
+      checkStrings hs =
+        mapM_ stringError . take 1 . filter isNonNum $ hs
+
+      isNonNum t = fromMaybe False $ do
+        s <- getLiteralStringExt (const $ return "") t
+        return . not . all numChar $ s
+      numChar x = isDigit x || x `elem` "+-. "
+
+      stringError t = err (getId t) 2170 $
+          "Numerical " ++ op ++ " does not dereference in [..]. Expand or use string operator."
+
+      isNum t =
+        case oversimplify t of
+            [v] -> all isDigit v
+            _ -> False
+      isFraction t =
+        case oversimplify t of
+            [v] -> isJust $ matchRegex floatRegex v
+            _ -> False
+
+      eqv ('\\':s) = eqv s
+      eqv "<" = "-lt"
+      eqv ">" = "-gt"
+      eqv "<=" = "-le"
+      eqv ">=" = "-ge"
+      eqv _ = "the numerical equivalent"
+
+      esc = if typ == SingleBracket then "\\" else ""
+      seqv "-ge" = "! a " ++ esc ++ "< b"
+      seqv "-gt" = esc ++ ">"
+      seqv "-le" = "! a " ++ esc ++ "> b"
+      seqv "-lt" = esc ++ "<"
+      seqv "-eq" = "="
+      seqv "-ne" = "!="
+      seqv _ = "the string equivalent"
+
+      invert ('\\':s) = invert s
+      invert "<=" = ">"
+      invert ">=" = "<"
+
+      floatRegex = mkRegex "^[0-9]+\\.[0-9]+$"
+checkNumberComparisons _ _ = return ()
+
+prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]"
+prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]"
+prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]"
+prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done"
+checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)
+    | typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] =
+        err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]."
+checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)
+    | typ == SingleBracket && op == "=~" =
+        err id 2074 $ "Can't use " ++ op ++" in [ ]. Use [[..]] instead."
+checkSingleBracketOperators _ _ = return ()
+
+prop_checkDoubleBracketOperators1 = verify checkDoubleBracketOperators "[[ 3 \\< 4 ]]"
+prop_checkDoubleBracketOperators3 = verifyNot checkDoubleBracketOperators "[[ foo < bar ]]"
+checkDoubleBracketOperators _ x@(TC_Binary id typ op lhs rhs)
+    | typ == DoubleBracket && op `elem` ["\\<", "\\>", "\\<=", "\\>="] =
+        err id 2075 $ "Escaping " ++ op ++" is required in [..], but invalid in [[..]]"
+checkDoubleBracketOperators _ _ = return ()
+
+prop_checkConditionalAndOrs1 = verify checkConditionalAndOrs "[ foo && bar ]"
+prop_checkConditionalAndOrs2 = verify checkConditionalAndOrs "[[ foo -o bar ]]"
+prop_checkConditionalAndOrs3 = verifyNot checkConditionalAndOrs "[[ foo || bar ]]"
+prop_checkConditionalAndOrs4 = verify checkConditionalAndOrs "[ foo -a bar ]"
+prop_checkConditionalAndOrs5 = verify checkConditionalAndOrs "[ -z 3 -o a = b ]"
+checkConditionalAndOrs _ t =
+    case t of
+        (TC_And id SingleBracket "&&" _ _) ->
+            err id 2107 "Instead of [ a && b ], use [ a ] && [ b ]."
+        (TC_And id DoubleBracket "-a" _ _) ->
+            err id 2108 "In [[..]], use && instead of -a."
+        (TC_Or id SingleBracket "||" _ _) ->
+            err id 2109 "Instead of [ a || b ], use [ a ] || [ b ]."
+        (TC_Or id DoubleBracket "-o" _ _) ->
+            err id 2110 "In [[..]], use || instead of -o."
+
+        (TC_And id SingleBracket "-a" _ _) ->
+            warn id 2166 "Prefer [ p ] && [ q ] as [ p -a q ] is not well defined."
+        (TC_Or id SingleBracket "-o" _ _) ->
+            warn id 2166 "Prefer [ p ] || [ q ] as [ p -o q ] is not well defined."
+
+        otherwise -> return ()
+
+prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]"
+prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]"
+prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"
+prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"
+prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]"
+checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
+    case rhs of
+        T_NormalWord id [T_DoubleQuoted _ _] -> error rhs
+        T_NormalWord id [T_SingleQuoted _ _] -> error rhs
+        _ -> return ()
+  where
+    error t =
+        unless (isConstantNonRe t) $
+            err (getId t) 2076
+                "Don't quote rhs of =~, it'll match literally rather than as a regex."
+    re = mkRegex "[][*.+()]"
+    hasMetachars s = s `matches` re
+    isConstantNonRe t = fromMaybe False $ do
+        s <- getLiteralString t
+        return . not $ hasMetachars s
+checkQuotedCondRegex _ _ = return ()
+
+prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
+prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
+prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
+prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
+prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
+checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
+    let s = concat $ oversimplify rhs in
+        when (isConfusedGlobRegex s) $
+            warn (getId rhs) 2049 "=~ is for regex. Use == for globs."
+checkGlobbedRegex _ _ = return ()
+
+
+prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]"
+prop_checkConstantIfs2a= verify checkConstantIfs "[ n -le 4 ]"
+prop_checkConstantIfs2b= verifyNot checkConstantIfs "[[ n -le 4 ]]"
+prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n != 2 ]]"
+prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"
+prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"
+prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]"
+prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"
+checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
+    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
+    isDynamic =
+        op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
+            && typ == DoubleBracket
+        || op `elem` [ "-nt", "-ot", "-ef"]
+checkConstantIfs _ _ = return ()
+
+prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]"
+prop_checkLiteralBreakingTest2 = verify checkLiteralBreakingTest "[ $foo=3 ]"
+prop_checkLiteralBreakingTest3 = verify checkLiteralBreakingTest "[ $foo!=3 ]"
+prop_checkLiteralBreakingTest4 = verify checkLiteralBreakingTest "[ \"$(ls) \" ]"
+prop_checkLiteralBreakingTest5 = verify checkLiteralBreakingTest "[ -n \"$(true) \" ]"
+prop_checkLiteralBreakingTest6 = verify checkLiteralBreakingTest "[ -z $(true)z ]"
+prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]"
+prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]"
+prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"
+checkLiteralBreakingTest _ t = potentially $
+        case t of
+            (TC_Noary _ _ w@(T_NormalWord _ l)) -> do
+                guard . not $ isConstant w -- Covered by SC2078
+                comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings."
+            (TC_Unary _ _ op w@(T_NormalWord _ l)) ->
+                case op of
+                    "-n" -> tautologyWarning w "Argument to -n is always true due to literal strings."
+                    "-z" -> tautologyWarning w "Argument to -z is always false due to literal strings."
+                    _ -> fail "not relevant"
+            _ -> fail "not my problem"
+  where
+    hasEquals = matchToken ('=' `elem`)
+    isNonEmpty = matchToken (not . null)
+    matchToken m t = isJust $ do
+        str <- getLiteralString t
+        guard $ m str
+        return ()
+
+    comparisonWarning list = do
+        token <- listToMaybe $ filter hasEquals list
+        return $ err (getId token) 2077 "You need spaces around the comparison operator."
+    tautologyWarning t s = do
+        token <- listToMaybe $ filter isNonEmpty $ getWordParts t
+        return $ err (getId token) 2157 s
+
+prop_checkConstantNoary = verify checkConstantNoary "[[ '$(foo)' ]]"
+prop_checkConstantNoary2 = verify checkConstantNoary "[ \"-f lol\" ]"
+prop_checkConstantNoary3 = verify checkConstantNoary "[[ cmd ]]"
+prop_checkConstantNoary4 = verify checkConstantNoary "[[ ! cmd ]]"
+prop_checkConstantNoary5 = verify checkConstantNoary "[[ true ]]"
+prop_checkConstantNoary6 = verify checkConstantNoary "[ 1 ]"
+prop_checkConstantNoary7 = verify checkConstantNoary "[ false ]"
+checkConstantNoary _ (TC_Noary _ _ t) | isConstant t =
+    case fromMaybe "" $ getLiteralString t of
+        "false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets."
+        "0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead."
+        "true" -> style (getId t) 2160 "Instead of '[ true ]', just use 'true'."
+        "1" -> style (getId t) 2161 "Instead of '[ 1 ]', use 'true'."
+        _ -> err (getId t) 2078 "This expression is constant. Did you forget a $ somewhere?"
+  where
+    string = fromMaybe "" $ getLiteralString t
+
+checkConstantNoary _ _ = return ()
+
+prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
+prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
+prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
+prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
+checkBraceExpansionVars params t@(T_BraceExpansion id list) = mapM_ check list
+  where
+    check element =
+        when (any (`isInfixOf` toString element) ["$..", "..$"]) $
+            if isEvaled
+            then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
+            else warn id 2051 "Bash doesn't support variables in brace range expansions."
+    literalExt t =
+        case t of
+            T_DollarBraced {} -> return "$"
+            T_DollarExpansion {} -> return "$"
+            T_DollarArithmetic {} -> return "$"
+            otherwise -> return "-"
+    toString t = fromJust $ getLiteralStringExt literalExt t
+    isEvaled = fromMaybe False $
+        (`isUnqualifiedCommand` "eval") <$> getClosestCommand (parentMap params) t
+checkBraceExpansionVars _ _ = return ()
+
+prop_checkForDecimals = verify checkForDecimals "((3.14*c))"
+checkForDecimals params t@(TA_Expansion id _) = potentially $ do
+    guard $ not (hasFloatingPoint params)
+    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))"
+prop_checkDivBeforeMult3 = verifyNot checkDivBeforeMult "echo $((c/10*10))"
+checkDivBeforeMult params (TA_Binary _ "*" (TA_Binary id "/" _ x) y)
+    | not (hasFloatingPoint params) && x /= y =
+        info id 2017 "Increase precision by replacing a/b*c with a*c/b."
+checkDivBeforeMult _ _ = return ()
+
+prop_checkArithmeticDeref = verify checkArithmeticDeref "echo $((3+$foo))"
+prop_checkArithmeticDeref2 = verify checkArithmeticDeref "cow=14; (( s+= $cow ))"
+prop_checkArithmeticDeref3 = verifyNot checkArithmeticDeref "cow=1/40; (( s+= ${cow%%/*} ))"
+prop_checkArithmeticDeref4 = verifyNot checkArithmeticDeref "(( ! $? ))"
+prop_checkArithmeticDeref5 = verifyNot checkArithmeticDeref "(($1))"
+prop_checkArithmeticDeref6 = verify checkArithmeticDeref "(( a[$i] ))"
+prop_checkArithmeticDeref7 = verifyNot checkArithmeticDeref "(( 10#$n ))"
+prop_checkArithmeticDeref8 = verifyNot checkArithmeticDeref "let i=$i+1"
+prop_checkArithmeticDeref9 = verifyNot checkArithmeticDeref "(( a[foo] ))"
+prop_checkArithmeticDeref10= verifyNot checkArithmeticDeref "(( a[\\$foo] ))"
+prop_checkArithmeticDeref11= verifyNot checkArithmeticDeref "a[$foo]=wee"
+prop_checkArithmeticDeref12= verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done"
+prop_checkArithmeticDeref13= verifyNot checkArithmeticDeref "(( $$ ))"
+checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
+    unless (isException $ bracedString b) getWarning
+  where
+    isException [] = True
+    isException s = any (`elem` "/.:#%?*@$") s || isDigit (head s)
+    getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t
+    warningFor t =
+        case t of
+            T_Arithmetic {} -> return normalWarning
+            T_DollarArithmetic {} -> return normalWarning
+            T_ForArithmetic {} -> return normalWarning
+            TA_Index {} -> return indexWarning
+            T_SimpleCommand {} -> return noWarning
+            _ -> Nothing
+
+    normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
+    indexWarning = style id 2149 "Remove $/${} for numeric index, or escape it for string."
+    noWarning = return ()
+checkArithmeticDeref _ _ = return ()
+
+prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))"
+prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))"
+prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))"
+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 ]]"
+prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow == \"$bar\" ]]"
+prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
+prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
+prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
+checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
+    | op `elem` ["=", "==", "!="] =
+        warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
+checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
+        | (op == "=" || op == "==") && isGlob word =
+    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
+checkCommarrays _ _ = return ()
+
+prop_checkOrNeq1 = verify checkOrNeq "if [[ $lol -ne cow || $lol -ne foo ]]; then echo foo; fi"
+prop_checkOrNeq2 = verify checkOrNeq "(( a!=lol || a!=foo ))"
+prop_checkOrNeq3 = verify checkOrNeq "[ \"$a\" != lol || \"$a\" != foo ]"
+prop_checkOrNeq4 = verifyNot checkOrNeq "[ a != $cow || b != $foo ]"
+prop_checkOrNeq5 = verifyNot checkOrNeq "[[ $a != /home || $a != */public_html/* ]]"
+-- This only catches the most idiomatic cases. Fixme?
+checkOrNeq _ (TC_Or id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2))
+    | lhs1 == lhs2 && (op1 == op2 && (op1 == "-ne" || op1 == "!=")) && not (any isGlob [rhs1,rhs2]) =
+        warn id 2055 $ "You probably wanted " ++ (if typ == SingleBracket then "-a" else "&&") ++ " here."
+
+checkOrNeq _ (TA_Binary id "||" (TA_Binary _ "!=" word1 _) (TA_Binary _ "!=" word2 _))
+    | word1 == word2 =
+        warn id 2056 "You probably wanted && here."
+checkOrNeq _ _ = return ()
+
+
+prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]"
+prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]"
+prop_checkValidCondOps2a= verifyNot checkValidCondOps "[ 3 \\> 2 ]"
+prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]"
+prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]"
+checkValidCondOps _ (TC_Binary id _ s _ _)
+    | s `notElem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="] =
+        warn id 2057 "Unknown binary operator."
+checkValidCondOps _ (TC_Unary id _ s _)
+    | 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 ()
+
+prop_checkUuoeVar1 = verify checkUuoeVar "for f in $(echo $tmp); do echo lol; done"
+prop_checkUuoeVar2 = verify checkUuoeVar "date +`echo \"$format\"`"
+prop_checkUuoeVar3 = verifyNot checkUuoeVar "foo \"$(echo -e '\r')\""
+prop_checkUuoeVar4 = verifyNot checkUuoeVar "echo $tmp"
+prop_checkUuoeVar5 = verify checkUuoeVar "foo \"$(echo \"$(date) value:\" $value)\""
+prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\""
+prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005
+prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)"
+prop_checkUuoeVar9 = verify checkUuoeVar "foo $(echo $(<file))"
+checkUuoeVar _ p =
+    case p of
+        T_Backticked id [cmd] -> check id cmd
+        T_DollarExpansion id [cmd] -> check id cmd
+        _ -> return ()
+  where
+    couldBeOptimized f = case f of
+        T_Glob {} -> False
+        T_Extglob {} -> False
+        T_BraceExpansion {} -> False
+        T_NormalWord _ l -> all couldBeOptimized l
+        T_DoubleQuoted _ l -> all couldBeOptimized l
+        _ -> True
+
+    check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c
+    check _ _ = return ()
+    isCovered first rest = null rest && tokenIsJustCommandOutput first
+    warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars ->
+        case vars of
+          (first:rest) ->
+            unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $
+                when (all couldBeOptimized vars) $ style id 2116
+                    "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."
+          otherwise -> return ()
+
+prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
+prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
+prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
+checkTimeParameters _ = checkUnqualifiedCommand "time" f where
+    f cmd (x:_) = let s = concat $ oversimplify x in
+                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."
+    f _ _ = return ()
+
+prop_checkTimedCommand1 = verify checkTimedCommand "time -p foo | bar"
+prop_checkTimedCommand2 = verify checkTimedCommand "time ( foo; bar; )"
+prop_checkTimedCommand3 = verifyNot checkTimedCommand "time sleep 1"
+checkTimedCommand _ = checkUnqualifiedCommand "time" f where
+    f c args@(_:_) = do
+        let cmd = last args
+        when (isPiped cmd) $
+            warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
+        when (isSimple cmd == Just False) $
+            warn (getId cmd) 2177 "'time' is undefined for compound commands, time sh -c instead."
+    f _ _ = return ()
+    isPiped cmd =
+        case cmd of
+            T_Pipeline _ _ (_:_:_) -> True
+            _ -> False
+    getCommand cmd =
+        case cmd of
+            T_Pipeline _ _ ((T_Redirecting _ _ a):_) -> return a
+            _ -> fail ""
+    isSimple cmd = do
+        innerCommand <- getCommand cmd
+        case innerCommand of
+            T_SimpleCommand {} -> return True
+            _ -> return False
+
+
+prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"
+prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"
+prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo"
+prop_checkTestRedirects4 = verifyNot checkTestRedirects "test 1 -eq 2 2> file"
+checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
+    mapM_ check redirs
+  where
+    check t =
+        when (suspicious t) $
+            warn (getId t) 2065 "This is interpreted as a shell file redirection, not a comparison."
+    suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors,
+        case t of  -- and >> and similar redirections because these are probably not comparisons.
+            T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op
+            otherwise -> False
+    isComparison t =
+        case t of
+            T_Greater _ -> True
+            T_Less _ -> True
+            otherwise -> False
+checkTestRedirects _ _ = return ()
+
+prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
+prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
+prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
+prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
+prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
+prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
+prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
+checkSudoRedirect _ (T_Redirecting _ redirs cmd) | cmd `isCommand` "sudo" =
+    mapM_ warnAbout redirs
+  where
+    warnAbout (T_FdRedirect _ s (T_IoFile id op file))
+        | (s == "" || s == "&") && not (special file) =
+        case op of
+            T_Less _ ->
+              info (getId op) 2024
+                "sudo doesn't affect redirects. Use sudo cat file | .."
+            T_Greater _ ->
+              warn (getId op) 2024
+                "sudo doesn't affect redirects. Use ..| sudo tee file"
+            T_DGREAT _ ->
+              warn (getId op) 2024
+                "sudo doesn't affect redirects. Use .. | sudo tee -a file"
+            _ -> return ()
+    warnAbout _ = return ()
+    special file = concat (oversimplify file) == "/dev/null"
+checkSudoRedirect _ _ = return ()
+
+prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
+prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
+prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
+prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
+prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
+prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
+prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
+prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
+prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
+prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
+checkPS1Assignments _ (T_Assignment _ _ "PS1" _ word) = warnFor word
+  where
+    warnFor word =
+        let contents = concat $ oversimplify word in
+            when (containsUnescaped contents) $
+                info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"
+    containsUnescaped s =
+        let unenclosed = subRegex enclosedRegex s "" in
+           isJust $ matchRegex escapeRegex unenclosed
+    enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
+    escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
+checkPS1Assignments _ _ = return ()
+
+prop_checkBackticks1 = verify checkBackticks "echo `foo`"
+prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
+prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
+checkBackticks _ (T_Backticked id list) | not (null list) =
+    style id 2006 "Use $(..) instead of legacy `..`."
+checkBackticks _ _ = return ()
+
+prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
+prop_checkIndirectExpansion2 = verifyNot checkIndirectExpansion "${foo//$n/lol}"
+prop_checkIndirectExpansion3 = verify checkIndirectExpansion "${$#}"
+prop_checkIndirectExpansion4 = verify checkIndirectExpansion "${var${n}_$((i%2))}"
+prop_checkIndirectExpansion5 = verifyNot checkIndirectExpansion "${bar}"
+checkIndirectExpansion _ (T_DollarBraced i (T_NormalWord _ contents)) =
+    when (isIndirection contents) $
+        err i 2082 "To expand via indirection, use arrays, ${!name} or (for sh only) eval."
+  where
+    isIndirection vars =
+        let list = mapMaybe isIndirectionPart vars in
+            not (null list) && and list
+    isIndirectionPart t =
+        case t of T_DollarExpansion _ _ ->  Just True
+                  T_Backticked _ _ ->       Just True
+                  T_DollarBraced _ _ ->     Just True
+                  T_DollarArithmetic _ _ -> Just True
+                  T_Literal _ s -> if all isVariableChar s
+                                    then Nothing
+                                    else Just False
+                  _ -> Just False
+
+checkIndirectExpansion _ _ = return ()
+
+prop_checkInexplicablyUnquoted1 = verify checkInexplicablyUnquoted "echo 'var='value';'"
+prop_checkInexplicablyUnquoted2 = verifyNot checkInexplicablyUnquoted "'foo'*"
+prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --user-agent='something'"
+prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\""
+prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
+prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\""
+checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
+  where
+    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'? "
+
+    check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) =
+        case trapped of
+            T_DollarExpansion id _ -> warnAboutExpansion id
+            T_DollarBraced id _ -> warnAboutExpansion id
+            T_Literal id s ->
+                unless (quotesSingleThing a && quotesSingleThing b) $
+                    warnAboutLiteral id
+            _ -> return ()
+
+    check _ = return ()
+
+    -- If the surrounding quotes quote single things, like "$foo"_and_then_some_"$stuff",
+    -- the quotes were probably intentional and harmless.
+    quotesSingleThing x = case x of
+        [T_DollarExpansion _ _] -> True
+        [T_DollarBraced _ _] -> True
+        [T_Backticked _ _] -> True
+        _ -> False
+
+    warnAboutExpansion id =
+        warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them."
+    warnAboutLiteral id =
+        warn id 2140 "Word is on the form \"A\"B\"C\" (B indicated). Did you mean \"ABC\" or \"A\\\"B\\\"C\"?"
+checkInexplicablyUnquoted _ _ = return ()
+
+prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""
+prop_checkTildeInQuotes2 = verify checkTildeInQuotes "foo > '~/dir'"
+prop_checkTildeInQuotes4 = verifyNot checkTildeInQuotes "~/file"
+prop_checkTildeInQuotes5 = verifyNot checkTildeInQuotes "echo '/~foo/cow'"
+prop_checkTildeInQuotes6 = verifyNot checkTildeInQuotes "awk '$0 ~ /foo/'"
+checkTildeInQuotes _ = check
+  where
+    verify id ('~':'/':_) = warn id 2088 "Tilde does not expand in quotes. Use $HOME."
+    verify _ _ = return ()
+    check (T_NormalWord _ (T_SingleQuoted id str:_)) =
+        verify id str
+    check (T_NormalWord _ (T_DoubleQuoted _ (T_Literal id str:_):_)) =
+        verify id str
+    check _ = return ()
+
+prop_checkLonelyDotDash1 = verify checkLonelyDotDash "./ file"
+prop_checkLonelyDotDash2 = verifyNot checkLonelyDotDash "./file"
+checkLonelyDotDash _ t@(T_Redirecting id _ _)
+    | isUnqualifiedCommand t "./" =
+        err id 2083 "Don't add spaces after the slash in './file'."
+checkLonelyDotDash _ _ = return ()
+
+
+prop_checkSpuriousExec1 = verify checkSpuriousExec "exec foo; true"
+prop_checkSpuriousExec2 = verify checkSpuriousExec "if a; then exec b; exec c; fi"
+prop_checkSpuriousExec3 = verifyNot checkSpuriousExec "echo cow; exec foo"
+prop_checkSpuriousExec4 = verifyNot checkSpuriousExec "if a; then exec b; fi"
+prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"
+prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
+prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
+checkSpuriousExec _ = doLists
+  where
+    doLists (T_Script _ _ cmds) = doList cmds
+    doLists (T_BraceGroup _ cmds) = doList cmds
+    doLists (T_WhileExpression _ _ cmds) = doList cmds
+    doLists (T_UntilExpression _ _ cmds) = doList cmds
+    doLists (T_ForIn _ _ _ cmds) = doList cmds
+    doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds
+    doLists (T_IfExpression _ thens elses) = do
+        mapM_ (\(_, l) -> doList l) thens
+        doList elses
+    doLists _ = return ()
+
+    stripCleanup = reverse . dropWhile cleanup . reverse
+    cleanup (T_Pipeline _ _ [cmd]) =
+        isCommandMatch cmd (`elem` ["echo", "exit"])
+    cleanup _ = False
+
+    doList = doList' . stripCleanup
+    doList' t@(current:following:_) = do
+        commentIfExec current
+        doList (tail t)
+    doList' _ = return ()
+
+    commentIfExec (T_Pipeline id _ list) =
+      mapM_ commentIfExec $ take 1 list
+    commentIfExec (T_Redirecting _ _ f@(
+      T_SimpleCommand id _ (cmd:arg:_))) =
+        when (f `isUnqualifiedCommand` "exec") $
+          warn id 2093
+            "Remove \"exec \" if script should continue after this command."
+    commentIfExec _ = return ()
+
+
+prop_checkSpuriousExpansion1 = verify checkSpuriousExpansion "if $(true); then true; fi"
+prop_checkSpuriousExpansion2 = verify checkSpuriousExpansion "while \"$(cmd)\"; do :; done"
+prop_checkSpuriousExpansion3 = verifyNot checkSpuriousExpansion "$(cmd) --flag1 --flag2"
+prop_checkSpuriousExpansion4 = verify checkSpuriousExpansion "$((i++))"
+checkSpuriousExpansion _ (T_SimpleCommand _ _ [T_NormalWord _ [word]]) = check word
+  where
+    check word = case word of
+        T_DollarExpansion id _ ->
+            warn id 2091 "Remove surrounding $() to avoid executing output."
+        T_Backticked id _ ->
+            warn id 2092 "Remove backticks to avoid executing output."
+        T_DollarArithmetic id _ ->
+            err id 2084 "Remove '$' or use '_=$((expr))' to avoid executing output."
+        T_DoubleQuoted id [subword] -> check subword
+        _ -> return ()
+checkSpuriousExpansion _ _ = return ()
+
+
+prop_checkDollarBrackets1 = verify checkDollarBrackets "echo $[1+2]"
+prop_checkDollarBrackets2 = verifyNot checkDollarBrackets "echo $((1+2))"
+checkDollarBrackets _ (T_DollarBracket id _) =
+    style id 2007 "Use $((..)) instead of deprecated $[..]"
+checkDollarBrackets _ _ = return ()
+
+prop_checkSshHereDoc1 = verify checkSshHereDoc "ssh host << foo\necho $PATH\nfoo"
+prop_checkSshHereDoc2 = verifyNot checkSshHereDoc "ssh host << 'foo'\necho $PATH\nfoo"
+checkSshHereDoc _ (T_Redirecting _ redirs cmd)
+        | cmd `isCommand` "ssh" =
+    mapM_ checkHereDoc redirs
+  where
+    hasVariables = mkRegex "[`$]"
+    checkHereDoc (T_FdRedirect _ _ (T_HereDoc id _ Unquoted token tokens))
+        | not (all isConstant tokens) =
+        warn id 2087 $ "Quote '" ++ token ++ "' to make here document expansions happen on the server side rather than on the client."
+    checkHereDoc _ = return ()
+checkSshHereDoc _ _ = return ()
+
+--- Subshell detection
+prop_subshellAssignmentCheck = verifyTree     subshellAssignmentCheck "cat foo | while read bar; do a=$bar; done; echo \"$a\""
+prop_subshellAssignmentCheck2 = verifyNotTree subshellAssignmentCheck "while read bar; do a=$bar; done < file; echo \"$a\""
+prop_subshellAssignmentCheck3 = verifyTree    subshellAssignmentCheck "( A=foo; ); rm $A"
+prop_subshellAssignmentCheck4 = verifyNotTree subshellAssignmentCheck "( A=foo; rm $A; )"
+prop_subshellAssignmentCheck5 = verifyTree    subshellAssignmentCheck "cat foo | while read cow; do true; done; echo $cow;"
+prop_subshellAssignmentCheck6 = verifyTree    subshellAssignmentCheck "( export lol=$(ls); ); echo $lol;"
+prop_subshellAssignmentCheck6a= verifyTree    subshellAssignmentCheck "( typeset -a lol=a; ); echo $lol;"
+prop_subshellAssignmentCheck7 = verifyTree    subshellAssignmentCheck "cmd | while read foo; do (( n++ )); done; echo \"$n lines\""
+prop_subshellAssignmentCheck8 = verifyTree    subshellAssignmentCheck "n=3 & echo $((n++))"
+prop_subshellAssignmentCheck9 = verifyTree    subshellAssignmentCheck "read n & n=foo$n"
+prop_subshellAssignmentCheck10 = verifyTree    subshellAssignmentCheck "(( n <<= 3 )) & (( n |= 4 )) &"
+prop_subshellAssignmentCheck11 = verifyTree subshellAssignmentCheck "cat /etc/passwd | while read line; do let n=n+1; done\necho $n"
+prop_subshellAssignmentCheck12 = verifyTree subshellAssignmentCheck "cat /etc/passwd | while read line; do let ++n; done\necho $n"
+prop_subshellAssignmentCheck13 = verifyTree subshellAssignmentCheck "#!/bin/bash\necho foo | read bar; echo $bar"
+prop_subshellAssignmentCheck14 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh93\necho foo | read bar; echo $bar"
+prop_subshellAssignmentCheck15 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh\ncat foo | while read bar; do a=$bar; done\necho \"$a\""
+prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@"
+prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar"
+prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n"
+subshellAssignmentCheck params t =
+    let flow = variableFlow params
+        check = findSubshelled flow [("oops",[])] Map.empty
+    in snd $ runWriter check
+
+
+findSubshelled [] _ _ = return ()
+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
+    unless (shouldIgnore str) $ case Map.findWithDefault Alive str deadVars of
+        Alive -> return ()
+        Dead writeToken reason -> do
+                    info (getId writeToken) 2030 $ "Modification of " ++ str ++ " is local (to subshell caused by "++ reason ++")."
+                    info (getId readToken) 2031 $ str ++ " was modified in a subshell. That change might be lost."
+    findSubshelled rest scopes deadVars
+  where
+    shouldIgnore str =
+        str `elem` ["@", "*", "IFS"]
+
+findSubshelled (StackScope (SubshellScope reason):rest) scopes deadVars =
+    findSubshelled rest ((reason,[]):scopes) deadVars
+
+findSubshelled (StackScopeEnd:rest) ((reason, scope):oldScopes) deadVars =
+    findSubshelled rest oldScopes $
+        foldl (\m (_, token, var, _) ->
+            Map.insert var (Dead token reason) m) deadVars scope
+
+
+-- FIXME: This is a very strange way of doing it.
+-- For each variable read/write, run a stateful function that emits
+-- comments. The comments are collected and returned.
+doVariableFlowAnalysis ::
+    (Token -> Token -> String -> State t [v])
+    -> (Token -> Token -> String -> DataType -> State t [v])
+    -> t
+    -> [StackData]
+    -> [v]
+
+doVariableFlowAnalysis readFunc writeFunc empty flow = evalState (
+    foldM (\list x -> do { l <- doFlow x;  return $ l ++ list; }) [] flow
+    ) empty
+  where
+    doFlow (Reference (base, token, name)) =
+        readFunc base token name
+    doFlow (Assignment (base, token, name, values)) =
+        writeFunc base token name values
+    doFlow _ = return []
+
+---- Check whether variables could have spaces/globs
+prop_checkSpacefulness1 = verifyTree checkSpacefulness "a='cow moo'; echo $a"
+prop_checkSpacefulness2 = verifyNotTree checkSpacefulness "a='cow moo'; [[ $a ]]"
+prop_checkSpacefulness3 = verifyNotTree checkSpacefulness "a='cow*.mp3'; echo \"$a\""
+prop_checkSpacefulness4 = verifyTree checkSpacefulness "for f in *.mp3; do echo $f; done"
+prop_checkSpacefulness4a= verifyNotTree checkSpacefulness "foo=3; foo=$(echo $foo)"
+prop_checkSpacefulness5 = verifyTree checkSpacefulness "a='*'; b=$a; c=lol${b//foo/bar}; echo $c"
+prop_checkSpacefulness6 = verifyTree checkSpacefulness "a=foo$(lol); echo $a"
+prop_checkSpacefulness7 = verifyTree checkSpacefulness "a=foo\\ bar; rm $a"
+prop_checkSpacefulness8 = verifyNotTree checkSpacefulness "a=foo\\ bar; a=foo; rm $a"
+prop_checkSpacefulness10= verifyTree checkSpacefulness "rm $1"
+prop_checkSpacefulness11= verifyTree checkSpacefulness "rm ${10//foo/bar}"
+prop_checkSpacefulness12= verifyNotTree checkSpacefulness "(( $1 + 3 ))"
+prop_checkSpacefulness13= verifyNotTree checkSpacefulness "if [[ $2 -gt 14 ]]; then true; fi"
+prop_checkSpacefulness14= verifyNotTree checkSpacefulness "foo=$3 env"
+prop_checkSpacefulness15= verifyNotTree checkSpacefulness "local foo=$1"
+prop_checkSpacefulness16= verifyNotTree checkSpacefulness "declare foo=$1"
+prop_checkSpacefulness17= verifyTree checkSpacefulness "echo foo=$1"
+prop_checkSpacefulness18= verifyNotTree checkSpacefulness "$1 --flags"
+prop_checkSpacefulness19= verifyTree checkSpacefulness "echo $PWD"
+prop_checkSpacefulness20= verifyNotTree checkSpacefulness "n+='foo bar'"
+prop_checkSpacefulness21= verifyNotTree checkSpacefulness "select foo in $bar; do true; done"
+prop_checkSpacefulness22= verifyNotTree checkSpacefulness "echo $\"$1\""
+prop_checkSpacefulness23= verifyNotTree checkSpacefulness "a=(1); echo ${a[@]}"
+prop_checkSpacefulness24= verifyTree checkSpacefulness "a='a    b'; cat <<< $a"
+prop_checkSpacefulness25= verifyTree checkSpacefulness "a='s/[0-9]//g'; sed $a"
+prop_checkSpacefulness26= verifyTree checkSpacefulness "a='foo bar'; echo {1,2,$a}"
+prop_checkSpacefulness27= verifyNotTree checkSpacefulness "echo ${a:+'foo'}"
+prop_checkSpacefulness28= verifyNotTree checkSpacefulness "exec {n}>&1; echo $n"
+prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>&-;"
+prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"
+prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""
+prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
+
+checkSpacefulness params t =
+    doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
+  where
+    defaults = zip variablesWithoutSpaces (repeat False)
+
+    hasSpaces name = do
+        map <- get
+        return $ Map.findWithDefault True name map
+
+    setSpaces name bool =
+        modify $ Map.insert name bool
+
+    readF _ token name = do
+        spaced <- hasSpaces name
+        return [makeComment InfoC (getId token) 2086 warning |
+                  isExpansion token && spaced
+                  && not (isArrayExpansion token) -- There's another warning for this
+                  && not (isCounting token)
+                  && not (isQuoteFree parents token)
+                  && not (isQuotedAlternative token)
+                  && not (usedAsCommandName parents token)]
+      where
+        warning = "Double quote to prevent globbing and word splitting."
+
+    writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
+    writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
+
+    writeF _ _ name (DataString (SourceFrom vals)) = do
+        map <- get
+        setSpaces name
+            (isSpacefulWord (\x -> Map.findWithDefault True x map) vals)
+        return []
+
+    writeF _ _ _ _ = return []
+
+    parents = parentMap params
+
+    isExpansion t =
+        case t of
+            (T_DollarBraced _ _ ) -> True
+            _ -> False
+
+    isCounting (T_DollarBraced id token) =
+        case concat $ oversimplify token of
+            '#':_ -> True
+            _ -> False
+    isCounting _ = False
+
+    -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
+    isQuotedAlternative t =
+        case t of
+            T_DollarBraced _ _ ->
+                ":+" `isInfixOf` bracedString t
+            _ -> False
+
+    isSpacefulWord :: (String -> Bool) -> [Token] -> Bool
+    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_Literal _ s      -> s `containsAny` globspace
+          T_SingleQuoted _ s -> s `containsAny` globspace
+          T_DollarBraced _ _ -> spacefulF $ getBracedReference $ bracedString x
+          T_NormalWord _ w   -> isSpacefulWord spacefulF w
+          T_DoubleQuoted _ w -> isSpacefulWord spacefulF w
+          _ -> False
+      where
+        globspace = "*?[] \t\n"
+        containsAny s = any (`elem` s)
+
+prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
+prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
+prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""
+prop_checkQuotesInLiterals3 =verifyNotTree checkQuotesInLiterals "param=('--foo='); app \"${param[@]}\""
+prop_checkQuotesInLiterals4 = verifyNotTree checkQuotesInLiterals "param=\"don't bother with this one\"; app $param"
+prop_checkQuotesInLiterals5 = verifyNotTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; eval app $param"
+prop_checkQuotesInLiterals6 = verifyTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm $param\"; $cmd"
+prop_checkQuotesInLiterals6a= verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd"
+prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param"
+prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param"
+prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm ${#param}"
+checkQuotesInLiterals params t =
+    doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)
+  where
+    getQuotes name = liftM (Map.lookup name) get
+    setQuotes name ref = modify $ Map.insert name ref
+    deleteQuotes = modify . Map.delete
+    parents = parentMap params
+    quoteRegex = mkRegex "\"|([/= ]|^)'|'( |$)|\\\\ "
+    containsQuotes s = s `matches` quoteRegex
+
+    writeF _ _ name (DataString (SourceFrom values)) = do
+        quoteMap <- get
+        let quotedVars = msum $ map (forToken quoteMap) values
+        case quotedVars of
+            Nothing -> deleteQuotes name
+            Just x -> setQuotes name x
+        return []
+    writeF _ _ _ _ = return []
+
+    forToken map (T_DollarBraced id t) =
+        -- skip getBracedReference here to avoid false positives on PE
+        Map.lookup (concat . oversimplify $ t) map
+    forToken quoteMap (T_DoubleQuoted id tokens) =
+        msum $ map (forToken quoteMap) tokens
+    forToken quoteMap (T_NormalWord id tokens) =
+        msum $ map (forToken quoteMap) tokens
+    forToken _ t =
+        if containsQuotes (concat $ oversimplify t)
+        then return $ getId t
+        else Nothing
+
+    squashesQuotes t =
+        case t of
+            T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t
+            otherwise -> False
+
+    readF _ expr name = do
+        assignment <- getQuotes name
+        return
+          (if isJust assignment
+              && not (isParamTo parents "eval" expr)
+              && not (isQuoteFree parents expr)
+              && not (squashesQuotes expr)
+              then [
+                  makeComment WarningC (fromJust assignment) 2089
+                      "Quotes/backslashes will be treated literally. Use an array.",
+                  makeComment WarningC (getId expr) 2090
+                      "Quotes/backslashes in this variable will not be respected."
+                ]
+              else [])
+
+
+prop_checkFunctionsUsedExternally1 =
+  verifyTree checkFunctionsUsedExternally "foo() { :; }; sudo foo"
+prop_checkFunctionsUsedExternally2 =
+  verifyTree checkFunctionsUsedExternally "alias f='a'; xargs -n 1 f"
+prop_checkFunctionsUsedExternally3 =
+  verifyNotTree checkFunctionsUsedExternally "f() { :; }; echo f"
+prop_checkFunctionsUsedExternally4 =
+  verifyNotTree checkFunctionsUsedExternally "foo() { :; }; sudo \"foo\""
+checkFunctionsUsedExternally params t =
+    runNodeAnalysis checkCommand params t
+  where
+    invokingCmds = [
+        "chroot",
+        "find",
+        "screen",
+        "ssh",
+        "su",
+        "sudo",
+        "xargs"
+        ]
+    checkCommand _ t@(T_SimpleCommand _ _ (cmd:args)) =
+        let name = fromMaybe "" $ getCommandBasename t in
+          when (name `elem` invokingCmds) $
+            mapM_ (checkArg name) args
+    checkCommand _ _ = return ()
+
+    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))
+        | t `isUnqualifiedCommand` "alias" = mapM_ getAlias args
+    findFunctions _ = return ()
+    getAlias arg =
+        let string = concat $ oversimplify arg
+        in when ('=' `elem` string) $
+            modify ((takeWhile (/= '=') string, getId arg):)
+    checkArg cmd arg = potentially $ do
+        literalArg <- getUnquotedLiteral arg  -- only consider unquoted literals
+        definitionId <- Map.lookup literalArg functions
+        return $ do
+            warn (getId arg) 2033
+              "Shell functions can't be passed to external commands."
+            info definitionId 2032 $
+              "Use own script or sh -c '..' to run this from " ++ cmd ++ "."
+
+prop_checkUnused0 = verifyNotTree checkUnusedAssignments "var=foo; echo $var"
+prop_checkUnused1 = verifyTree checkUnusedAssignments "var=foo; echo $bar"
+prop_checkUnused2 = verifyNotTree checkUnusedAssignments "var=foo; export var;"
+prop_checkUnused3 = verifyTree checkUnusedAssignments "for f in *; do echo '$f'; done"
+prop_checkUnused4 = verifyTree checkUnusedAssignments "local i=0"
+prop_checkUnused5 = verifyNotTree checkUnusedAssignments "read lol; echo $lol"
+prop_checkUnused6 = verifyNotTree checkUnusedAssignments "var=4; (( var++ ))"
+prop_checkUnused7 = verifyNotTree checkUnusedAssignments "var=2; $((var))"
+prop_checkUnused8 = verifyTree checkUnusedAssignments "var=2; var=3;"
+prop_checkUnused9 = verifyNotTree checkUnusedAssignments "read ''"
+prop_checkUnused10= verifyNotTree checkUnusedAssignments "read -p 'test: '"
+prop_checkUnused11= verifyNotTree checkUnusedAssignments "bar=5; export foo[$bar]=3"
+prop_checkUnused12= verifyNotTree checkUnusedAssignments "read foo; echo ${!foo}"
+prop_checkUnused13= verifyNotTree checkUnusedAssignments "x=(1); (( x[0] ))"
+prop_checkUnused14= verifyNotTree checkUnusedAssignments "x=(1); n=0; echo ${x[n]}"
+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"
+prop_checkUnused20= verifyNotTree checkUnusedAssignments "a=1; PS1='$a'"
+prop_checkUnused21= verifyNotTree checkUnusedAssignments "a=1; trap 'echo $a' INT"
+prop_checkUnused22= verifyNotTree checkUnusedAssignments "a=1; [ -v a ]"
+prop_checkUnused23= verifyNotTree checkUnusedAssignments "a=1; [ -R a ]"
+prop_checkUnused24= verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}"
+prop_checkUnused25= verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}"
+prop_checkUnused26= verifyNotTree checkUnusedAssignments "declare -F foo"
+prop_checkUnused27= verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]"
+prop_checkUnused28= verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]"
+prop_checkUnused29= verifyNotTree checkUnusedAssignments "var=(a b); declare -p var"
+prop_checkUnused30= verifyTree checkUnusedAssignments "let a=1"
+prop_checkUnused31= verifyTree checkUnusedAssignments "let 'a=1'"
+prop_checkUnused32= verifyTree checkUnusedAssignments "let a=b=c; echo $a"
+prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]"
+checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
+  where
+    flow = variableFlow params
+    references = foldl (flip ($)) defaultMap (map insertRef flow)
+    insertRef (Reference (base, token, name)) =
+        Map.insert (stripSuffix name) ()
+    insertRef _ = id
+
+    assignments = foldl (flip ($)) Map.empty (map insertAssignment flow)
+    insertAssignment (Assignment (_, token, name, _)) | isVariableName name =
+        Map.insert name token
+    insertAssignment _ = id
+
+    unused = Map.assocs $ Map.difference assignments references
+
+    warnFor (name, token) =
+        warn (getId token) 2034 $
+            name ++ " appears unused. Verify it or export it."
+
+    stripSuffix = takeWhile isVariableChar
+    defaultMap = Map.fromList $ zip internalVariables $ repeat ()
+
+prop_checkUnassignedReferences1 = verifyTree checkUnassignedReferences "echo $foo"
+prop_checkUnassignedReferences2 = verifyNotTree checkUnassignedReferences "foo=hello; echo $foo"
+prop_checkUnassignedReferences3 = verifyTree checkUnassignedReferences "MY_VALUE=3; echo $MYVALUE"
+prop_checkUnassignedReferences4 = verifyNotTree checkUnassignedReferences "RANDOM2=foo; echo $RANDOM"
+prop_checkUnassignedReferences5 = verifyNotTree checkUnassignedReferences "declare -A foo=([bar]=baz); echo ${foo[bar]}"
+prop_checkUnassignedReferences6 = verifyNotTree checkUnassignedReferences "foo=..; echo ${foo-bar}"
+prop_checkUnassignedReferences7 = verifyNotTree checkUnassignedReferences "getopts ':h' foo; echo $foo"
+prop_checkUnassignedReferences8 = verifyNotTree checkUnassignedReferences "let 'foo = 1'; echo $foo"
+prop_checkUnassignedReferences9 = verifyNotTree checkUnassignedReferences "echo ${foo-bar}"
+prop_checkUnassignedReferences10= verifyNotTree checkUnassignedReferences "echo ${foo:?}"
+prop_checkUnassignedReferences11= verifyNotTree checkUnassignedReferences "declare -A foo; echo \"${foo[@]}\""
+prop_checkUnassignedReferences12= verifyNotTree checkUnassignedReferences "typeset -a foo; echo \"${foo[@]}\""
+prop_checkUnassignedReferences13= verifyNotTree checkUnassignedReferences "f() { local foo; echo $foo; }"
+prop_checkUnassignedReferences14= verifyNotTree checkUnassignedReferences "foo=; echo $foo"
+prop_checkUnassignedReferences15= verifyNotTree checkUnassignedReferences "f() { true; }; export -f f"
+prop_checkUnassignedReferences16= verifyNotTree checkUnassignedReferences "declare -A foo=( [a b]=bar ); echo ${foo[a b]}"
+prop_checkUnassignedReferences17= verifyNotTree checkUnassignedReferences "USERS=foo; echo $USER"
+prop_checkUnassignedReferences18= verifyNotTree checkUnassignedReferences "FOOBAR=42; export FOOBAR="
+prop_checkUnassignedReferences19= verifyNotTree checkUnassignedReferences "readonly foo=bar; echo $foo"
+prop_checkUnassignedReferences20= verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo"
+prop_checkUnassignedReferences21= verifyTree checkUnassignedReferences "echo ${#foo}"
+prop_checkUnassignedReferences22= verifyNotTree checkUnassignedReferences "echo ${!os*}"
+checkUnassignedReferences params t = warnings
+  where
+    (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
+    defaultAssigned = Map.fromList $ map (\a -> (a, ())) $ filter (not . null) internalVariables
+
+    tally (Assignment (_, _, name, _))  =
+        modify (\(read, written) -> (read, Map.insert name () written))
+    tally (Reference (_, place, name)) =
+        modify (\(read, written) -> (Map.insertWith' (const id) name place read, written))
+    tally _ = return ()
+
+    unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned
+    writtenVars = filter isVariableName $ Map.keys writeMap
+
+    getBestMatch var = do
+        (match, score) <- listToMaybe best
+        guard $ goodMatch var match score
+        return match
+      where
+        matches = map (\x -> (x, match var x)) writtenVars
+        best = sortBy (comparing snd) matches
+        goodMatch var match score =
+            let l = length match in
+                l > 3 && score <= 1
+                || l > 7 && score <= 2
+
+    isLocal = any isLower
+
+    warningForGlobals var place = do
+        match <- getBestMatch var
+        return $ warn (getId place) 2153 $
+            "Possible misspelling: " ++ var ++ " may not be assigned, but " ++ match ++ " is."
+
+    warningForLocals var place =
+        return $ warn (getId place) 2154 $
+            var ++ " is referenced but not assigned" ++ optionalTip ++ "."
+      where
+        optionalTip =
+            if var `elem` commonCommands
+            then " (for output from commands, use \"$(" ++ var ++ " ..." ++ ")\" )"
+            else fromMaybe "" $ do
+                    match <- getBestMatch var
+                    return $ " (did you mean '" ++ match ++ "'?)"
+
+    warningFor var place = do
+        guard . not $ isInArray var place || isGuarded place
+        (if isLocal var then warningForLocals else warningForGlobals) var place
+
+    warnings = execWriter . sequence $ mapMaybe (uncurry warningFor) unassigned
+
+    -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.
+    -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.
+    isInArray var t = any isArray $ getPath (parentMap params) t
+      where
+        isArray (T_Array {}) = True
+        isArray b@(T_DollarBraced _ _) | var /= getBracedReference (bracedString b) = True
+        isArray _ = False
+
+    isGuarded (T_DollarBraced _ v) =
+        any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
+      where
+        name = concat $ oversimplify v
+        rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
+    isGuarded _ = False
+
+    match var candidate =
+        if var /= candidate && map toLower var == map toLower candidate
+        then 1
+        else dist var candidate
+
+
+prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"
+prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*"
+prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"
+checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =
+    mapM_ check $ takeWhile (not . isEndOfArgs) args
+  where
+    check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" =
+        info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options."
+    check _ = return ()
+
+    isEndOfArgs t =
+        case concat $ oversimplify t of
+            "--" -> True
+            ":::" -> True
+            "::::" -> True
+            _ -> False
+
+checkGlobsAsOptions _ _ = return ()
+
+
+prop_checkWhileReadPitfalls1 = verify checkWhileReadPitfalls "while read foo; do ssh $foo uptime; done < file"
+prop_checkWhileReadPitfalls2 = verifyNot checkWhileReadPitfalls "while read -u 3 foo; do ssh $foo uptime; done 3< file"
+prop_checkWhileReadPitfalls3 = verifyNot checkWhileReadPitfalls "while true; do ssh host uptime; done"
+prop_checkWhileReadPitfalls4 = verifyNot checkWhileReadPitfalls "while read foo; do ssh $foo hostname < /dev/null; done"
+prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done"
+prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo"
+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 =
+    mapM_ checkMuncher contents
+  where
+    munchers = [ "ssh", "ffmpeg", "mplayer" ]
+
+    isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) =
+        let plaintext = oversimplify cmd
+        in head (plaintext ++ [""]) == "read"
+            && ("-u" `notElem` plaintext)
+            && all (not . stdinRedirect) redirs
+    isStdinReadCommand _ = False
+
+    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]
+
+            _ -> potentially $ do
+                name <- getCommandBasename cmd
+                guard $ name `elem` munchers
+                return $ do
+                    info id 2095 $
+                        name ++ " may swallow stdin, preventing this loop from working properly."
+                    warn (getId cmd) 2095 $
+                        "Add < /dev/null to prevent " ++ name ++ " from swallowing stdin."
+    checkMuncher _ = return ()
+
+    stdinRedirect (T_FdRedirect _ fd _)
+        | fd == "" || fd == "0" = True
+    stdinRedirect _ = False
+checkWhileReadPitfalls _ _ = return ()
+
+
+prop_checkPrefixAssign1 = verify checkPrefixAssignmentReference "var=foo echo $var"
+prop_checkPrefixAssign2 = verifyNot checkPrefixAssignmentReference "var=$(echo $var) cmd"
+checkPrefixAssignmentReference params t@(T_DollarBraced id value) =
+    check path
+  where
+    name = getBracedReference $ bracedString t
+    path = getPath (parentMap params) t
+    idPath = map getId path
+
+    check [] = return ()
+    check (t:rest) =
+        case t of
+            T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars
+            otherwise -> check rest
+    checkVar (T_Assignment aId mode aName Nothing value) |
+            aName == name && (aId `notElem` idPath) = do
+        warn aId 2097 "This assignment is only seen by the forked process."
+        warn id 2098 "This expansion will not see the mentioned assignment."
+    checkVar _ = return ()
+
+checkPrefixAssignmentReference _ _ = return ()
+
+prop_checkCharRangeGlob1 = verify checkCharRangeGlob "ls *[:digit:].jpg"
+prop_checkCharRangeGlob2 = verifyNot checkCharRangeGlob "ls *[[:digit:]].jpg"
+prop_checkCharRangeGlob3 = verify checkCharRangeGlob "ls [10-15]"
+prop_checkCharRangeGlob4 = verifyNot checkCharRangeGlob "ls [a-zA-Z]"
+prop_checkCharRangeGlob5 = verifyNot checkCharRangeGlob "tr -d [a-zA-Z]" -- tr has 2060
+checkCharRangeGlob p t@(T_Glob id str) |
+  isCharClass str && not (isParamTo (parentMap p) "tr" t) =
+    if ":" `isPrefixOf` contents
+        && ":" `isSuffixOf` contents
+        && contents /= ":"
+    then warn id 2101 "Named class needs outer [], e.g. [[:digit:]]."
+    else
+        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
+    hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents
+checkCharRangeGlob _ _ = return ()
+
+
+
+prop_checkCdAndBack1 = verify checkCdAndBack "for f in *; do cd $f; git pull; cd ..; done"
+prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
+prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
+prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
+checkCdAndBack params = doLists
+  where
+    shell = shellType params
+    doLists (T_ForIn _ _ _ cmds) = doList cmds
+    doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds
+    doLists (T_WhileExpression _ _ cmds) = doList cmds
+    doLists (T_UntilExpression _ _ cmds) = doList cmds
+    doLists (T_Script _ _ cmds) = doList cmds
+    doLists (T_IfExpression _ thens elses) = do
+        mapM_ (\(_, l) -> doList l) thens
+        doList elses
+    doLists _ = return ()
+
+    isCdRevert t =
+        case oversimplify t of
+            ["cd", p] -> p `elem` ["..", "-"]
+            _ -> False
+
+    getCmd (T_Annotation id _ x) = getCmd x
+    getCmd (T_Pipeline id _ [x]) = getCommandName x
+    getCmd _ = Nothing
+
+    doList list =
+        let cds = filter ((== Just "cd") . getCmd) list in
+            when (length cds >= 2 && isCdRevert (last cds)) $
+               info (getId $ last cds) 2103 message
+
+    message = "Use a ( subshell ) to avoid having to cd back."
+
+prop_checkLoopKeywordScope1 = verify checkLoopKeywordScope "continue 2"
+prop_checkLoopKeywordScope2 = verify checkLoopKeywordScope "for f; do ( break; ); done"
+prop_checkLoopKeywordScope3 = verify checkLoopKeywordScope "if true; then continue; fi"
+prop_checkLoopKeywordScope4 = verifyNot checkLoopKeywordScope "while true; do break; done"
+prop_checkLoopKeywordScope5 = verify checkLoopKeywordScope "if true; then break; fi"
+prop_checkLoopKeywordScope6 = verify checkLoopKeywordScope "while true; do true | { break; }; done"
+prop_checkLoopKeywordScope7 = verifyNot checkLoopKeywordScope "#!/bin/ksh\nwhile true; do true | { break; }; done"
+checkLoopKeywordScope params t |
+        name `elem` map Just ["continue", "break"] =
+    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."
+    else case map subshellType $ filter (not . isFunction) path of
+        Just str:_ -> warn (getId t) 2106 $
+            "This only exits the subshell caused by the " ++ str ++ "."
+        _ -> return ()
+  where
+    name = getCommandName t
+    path = let p = getPath (parentMap params) t in filter relevant p
+    subshellType t = case leadType (shellType params) (parentMap params) t of
+        NoneScope -> Nothing
+        SubshellScope str -> return str
+    relevant t = isLoop t || isFunction t || isJust (subshellType t)
+checkLoopKeywordScope _ _ = return ()
+
+
+prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
+prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
+checkLocalScope params t | t `isCommand` "local" && not (isInFunction t) =
+    err (getId t) 2168 "'local' is only valid in functions."
+  where
+    isInFunction t = any isFunction $ getPath (parentMap params) t
+checkLocalScope _ _ = return ()
+
+prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }"
+prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }"
+prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }"
+checkFunctionDeclarations params
+        (T_Function id (FunctionKeyword hasKeyword) (FunctionParentheses hasParens) _ _) =
+    case shellType params of
+        Bash -> return ()
+        Ksh ->
+            when (hasKeyword && hasParens) $
+                err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."
+        Dash -> forSh
+        Sh   -> forSh
+
+    where
+        forSh = do
+            when (hasKeyword && hasParens) $
+                warn id 2112 "'function' keyword is non-standard. Delete it."
+            when (hasKeyword && not hasParens) $
+                warn id 2113 "'function' keyword is non-standard. Use 'foo()' instead of 'function foo'."
+checkFunctionDeclarations _ _ = return ()
+
+
+prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
+prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
+prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
+prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
+prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
+prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
+prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
+prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"
+prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
+prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
+prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
+prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
+prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
+prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
+checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =
+    when (any isRecursiveFlag simpleArgs) $
+        mapM_ (mapM_ checkWord . braceExpand) tokens
+  where
+    simpleArgs = oversimplify t
+
+    checkWord token =
+        case getLiteralString token of
+            Just str ->
+                when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $
+                    warn (getId token) 2114 "Warning: deletes a system directory. Use 'rm --' to disable this message."
+            Nothing ->
+                checkWord' token
+
+    checkWord' token = fromMaybe (return ()) $ do
+        filename <- getPotentialPath token
+        let path = fixPath filename
+        return . when (path `elem` importantPaths) $
+            warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."
+
+    fixPath filename =
+        let normalized = skipRepeating '/' . skipRepeating '*' $ filename in
+            if normalized == "/" then normalized else stripTrailing '/' normalized
+
+    getPotentialPath = getLiteralStringExt f
+      where
+        f (T_Glob _ str) = return str
+        f (T_DollarBraced _ word) =
+            let var = onlyLiteralString word in
+                if any (flip isInfixOf var) [":?", ":-", ":="]
+                then Nothing
+                else return ""
+        f _ = return ""
+
+    isRecursiveFlag "--recursive" = True
+    isRecursiveFlag ('-':'-':_) = False
+    isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str
+    isRecursiveFlag _ = False
+
+    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 _ [] = []
+
+    paths = [
+        "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
+        "/var", "/lib"
+        ]
+    importantPaths = filter (not . null) $
+        ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
+checkCatastrophicRm _ _ = return ()
+
+
+prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar"
+prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar"
+checkStderrPipe params =
+    case shellType params of
+        Ksh -> match
+        _ -> const $ return ()
+  where
+    match (T_Pipe id "|&") =
+        err id 2118 "Ksh does not support |&. Use 2>&1 |."
+    match _ = return ()
+
+prop_checkUnpassedInFunctions1 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo"
+prop_checkUnpassedInFunctions2 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; };"
+prop_checkUnpassedInFunctions3 = verifyNotTree checkUnpassedInFunctions "foo() { echo $lol; }; foo"
+prop_checkUnpassedInFunctions4 = verifyNotTree checkUnpassedInFunctions "foo() { echo $0; }; foo"
+prop_checkUnpassedInFunctions5 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; }; foo 'lol'; foo"
+prop_checkUnpassedInFunctions6 = verifyNotTree checkUnpassedInFunctions "foo() { set -- *; echo $1; }; foo"
+prop_checkUnpassedInFunctions7 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo; foo;"
+prop_checkUnpassedInFunctions8 = verifyNotTree checkUnpassedInFunctions "foo() { echo $((1)); }; foo;"
+prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;"
+prop_checkUnpassedInFunctions10= verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;"
+prop_checkUnpassedInFunctions11= verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;"
+checkUnpassedInFunctions params root =
+    execWriter $ mapM_ warnForGroup referenceGroups
+  where
+    functionMap :: Map.Map String Token
+    functionMap = Map.fromList $
+        map (\t@(T_Function _ _ _ name _) -> (name,t)) functions
+    functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
+
+    findFunction t@(T_Function id _ _ name body) =
+        let flow = getVariableFlow (shellType params) (parentMap params) body
+        in
+          if any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
+            then return t
+            else Nothing
+    findFunction _ = Nothing
+
+    isPositionalAssignment x =
+        case x of
+            Assignment (_, _, str, _) -> isPositional str
+            _ -> False
+    isPositionalReference function x =
+        case x of
+            Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function
+            _ -> False
+
+    isDirectChildOf child parent = fromMaybe False $ do
+        function <- find (\x -> case x of T_Function {} -> True; _ -> False) $ getPath (parentMap params) child
+        return $ getId parent == getId function
+
+    referenceList :: [(String, Bool, Token)]
+    referenceList = execWriter $
+        doAnalysis (fromMaybe (return ()) . checkCommand) root
+    checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())
+    checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do
+        str <- getLiteralString cmd
+        unless (Map.member str functionMap) $ fail "irrelevant"
+        return $ tell [(str, null args, t)]
+    checkCommand _ = Nothing
+
+    isPositional str = str == "*" || str == "@"
+        || (all isDigit str && str /= "0")
+
+    isArgumentless (_, b, _) = b
+    referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList
+    updateWith x@(name, _, _) = Map.insertWith (++) name [x]
+
+    warnForGroup group =
+        when (all isArgumentless group) $ do
+            mapM_ suggestParams group
+            warnForDeclaration group
+
+    suggestParams (name, _, thing) =
+        info (getId thing) 2119 $
+            "Use " ++ name ++ " \"$@\" if function's $1 should mean script's $1."
+    warnForDeclaration ((name, _, _):_) =
+        warn (getId . fromJust $ Map.lookup name functionMap) 2120 $
+            name ++ " references arguments, but none are ever passed."
+
+
+prop_checkOverridingPath1 = verify checkOverridingPath "PATH=\"$var/$foo\""
+prop_checkOverridingPath2 = verify checkOverridingPath "PATH=\"mydir\""
+prop_checkOverridingPath3 = verify checkOverridingPath "PATH=/cow/foo"
+prop_checkOverridingPath4 = verifyNot checkOverridingPath "PATH=/cow/foo/bin"
+prop_checkOverridingPath5 = verifyNot checkOverridingPath "PATH='/bin:/sbin'"
+prop_checkOverridingPath6 = verifyNot checkOverridingPath "PATH=\"$var/$foo\" cmd"
+prop_checkOverridingPath7 = verifyNot checkOverridingPath "PATH=$OLDPATH"
+prop_checkOverridingPath8 = verifyNot checkOverridingPath "PATH=$PATH:/stuff"
+checkOverridingPath _ (T_SimpleCommand _ vars []) =
+    mapM_ checkVar vars
+  where
+    checkVar (T_Assignment id Assign "PATH" Nothing word) =
+        let string = concat $ oversimplify word
+        in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do
+            when ('/' `elem` string && ':' `notElem` string) $ notify id
+            when (isLiteral word && ':' `notElem` string && '/' `notElem` string) $ notify id
+    checkVar _ = return ()
+    notify id = warn id 2123 "PATH is the shell search path. Use another name."
+checkOverridingPath _ _ = return ()
+
+prop_checkTildeInPath1 = verify checkTildeInPath "PATH=\"$PATH:~/bin\""
+prop_checkTildeInPath2 = verify checkTildeInPath "PATH='~foo/bin'"
+prop_checkTildeInPath3 = verifyNot checkTildeInPath "PATH=~/bin"
+checkTildeInPath _ (T_SimpleCommand _ vars _) =
+    mapM_ checkVar vars
+  where
+    checkVar (T_Assignment id Assign "PATH" Nothing (T_NormalWord _ parts)) =
+        when (any (\x -> isQuoted x && hasTilde x) parts) $
+            warn id 2147 "Literal tilde in PATH works poorly across programs."
+    checkVar _ = return ()
+
+    hasTilde t = fromMaybe False (liftM2 elem (return '~') (getLiteralStringExt (const $ return "") t))
+    isQuoted (T_DoubleQuoted {}) = True
+    isQuoted (T_SingleQuoted {}) = True
+    isQuoted _ = False
+checkTildeInPath _ _ = return ()
+
+prop_checkUnsupported3 = verify checkUnsupported "#!/bin/sh\ncase foo in bar) baz ;& esac"
+prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac"
+prop_checkUnsupported5 = verify checkUnsupported "#!/bin/bash\necho \"${ ls; }\""
+checkUnsupported params t =
+    when (not (null support) && (shellType params `notElem` support)) $
+        report name
+ where
+    (name, support) = shellSupport t
+    report s = err (getId t) 2127 $
+        "To use " ++ s ++ ", specify #!/usr/bin/env " ++
+            (map toLower . intercalate " or " . map show $ support)
+
+-- TODO: Move more of these checks here
+shellSupport t =
+  case t of
+    T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list)
+    T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Ksh])
+    otherwise -> ("", [])
+  where
+    forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash])
+    forCase seps | CaseFallThrough `elem` seps = ("cases with ;&", [Bash, Ksh])
+    forCase _ = ("", [])
+
+
+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;"
+checkMultipleAppends params t =
+    mapM_ checkList $ getCommandSequences t
+  where
+    checkList list =
+        mapM_ checkGroup (groupWith (liftM fst) $ map getTarget list)
+    checkGroup (f:_:_:_) | isJust f =
+        style (snd $ fromJust f) 2129
+            "Consider using { cmd1; cmd2; } >> file instead of individual redirects."
+    checkGroup _ = return ()
+    getTarget (T_Annotation _ _ t) = getTarget t
+    getTarget (T_Pipeline _ _ args@(_:_)) = getTarget (last args)
+    getTarget (T_Redirecting id list _) = do
+        file <- mapMaybe getAppend list !!! 0
+        return (file, id)
+    getTarget _ = Nothing
+    getAppend (T_FdRedirect _ _ (T_IoFile _ (T_DGREAT {}) f)) = return f
+    getAppend _ = Nothing
+
+
+prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\""
+prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'"
+checkSuspiciousIFS params (T_Assignment id Assign "IFS" Nothing value) =
+    potentially $ do
+        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'"
+    check value =
+        case value of
+            "\\n" -> suggest n
+            "/n" -> suggest n
+            "\\t" -> suggest t
+            "/t" -> suggest t
+            _ -> return ()
+    suggest r = warn id 2141 $ "Did you mean IFS=" ++ r ++ " ?"
+checkSuspiciousIFS _ _ = return ()
+
+
+prop_checkGrepQ1= verify checkShouldUseGrepQ "[[ $(foo | grep bar) ]]"
+prop_checkGrepQ2= verify checkShouldUseGrepQ "[ -z $(fgrep lol) ]"
+prop_checkGrepQ3= verify checkShouldUseGrepQ "[ -n \"$(foo | zgrep lol)\" ]"
+prop_checkGrepQ4= verifyNot checkShouldUseGrepQ "[ -z $(grep bar | cmd) ]"
+prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"
+prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]"
+checkShouldUseGrepQ params t =
+    potentially $ case t of
+        TC_Noary id _ token -> check id True token
+        TC_Unary id _ "-n" token -> check id True token
+        TC_Unary id _ "-z" token -> check id False token
+        _ -> fail "not check"
+  where
+    check id bool token = do
+        name <- getFinalGrep token
+        let op = if bool then "-n" else "-z"
+        let flip = if bool then "" else "! "
+        return . style id 2143 $
+            "Use " ++ flip ++ name ++ " -q instead of " ++
+                "comparing output with [ " ++ op ++ " .. ]."
+
+    getFinalGrep t = do
+        cmds <- getPipeline t
+        guard . not . null $ cmds
+        name <- getCommandBasename $ last cmds
+        guard . isGrep $ name
+        return name
+    getPipeline t =
+        case t of
+            T_NormalWord _ [x] -> getPipeline x
+            T_DoubleQuoted _ [x] -> getPipeline x
+            T_DollarExpansion _ [x] -> getPipeline x
+            T_Pipeline _ _ cmds -> return cmds
+            _ -> fail "unknown"
+    isGrep = (`elem` ["grep", "egrep", "fgrep", "zgrep"])
+
+prop_checkTestGlobs1 = verify checkTestGlobs "[ -e *.mp3 ]"
+prop_checkTestGlobs2 = verifyNot checkTestGlobs "[[ $a == *b* ]]"
+checkTestGlobs params (TC_Unary _ _ op token) | isGlob token =
+    err (getId token) 2144 $
+       op ++ " doesn't work with globs. Use a for loop."
+checkTestGlobs _ _ = return ()
+
+
+prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }"
+prop_checkMaskedReturns2 = verify checkMaskedReturns "declare a=$(false)"
+prop_checkMaskedReturns3 = verify checkMaskedReturns "declare a=\"`false`\""
+prop_checkMaskedReturns4 = verifyNot checkMaskedReturns "declare a; a=$(false)"
+prop_checkMaskedReturns5 = verifyNot checkMaskedReturns "f() { local -r a=$(false); }"
+checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = potentially $ do
+    name <- getCommandName t
+    guard $ name `elem` ["declare", "export"]
+        || name == "local" && "r" `notElem` map snd (getAllFlags t)
+    return $ mapM_ checkArgs rest
+  where
+    checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
+        warn id 2155 "Declare and assign separately to avoid masking return values."
+    checkArgs _ = return ()
+
+    hasReturn t = case t of
+        T_Backticked {} -> True
+        T_DollarExpansion {} -> True
+        _ -> False
+checkMaskedReturns _ _ = return ()
+
+
+prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
+prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
+checkReadWithoutR _ t@(T_SimpleCommand {}) | t `isUnqualifiedCommand` "read" =
+    unless ("r" `elem` map snd (getAllFlags t)) $
+        info (getId t) 2162 "read without -r will mangle backslashes."
+checkReadWithoutR _ _ = return ()
+
+prop_checkUncheckedCd1 = verifyTree checkUncheckedCd "cd ~/src; rm -r foo"
+prop_checkUncheckedCd2 = verifyNotTree checkUncheckedCd "cd ~/src || exit; rm -r foo"
+prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCd "set -e; cd ~/src; rm -r foo"
+prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi"
+prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi"
+prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .."
+prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar"
+prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar"
+checkUncheckedCd params root =
+    if hasSetE then [] else execWriter $ doAnalysis checkElement root
+  where
+    checkElement t@(T_SimpleCommand {}) =
+        when(t `isUnqualifiedCommand` "cd"
+            && not (isCdDotDot t)
+            && not (isCondition $ getPath (parentMap params) t)) $
+                warn (getId t) 2164 "Use cd ... || exit in case cd fails."
+    checkElement _ = return ()
+    isCdDotDot t = oversimplify t == ["cd", ".."]
+    hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root
+    isSetE t =
+        case t of
+            T_Script _ str _ -> str `matches` re
+            T_SimpleCommand {}  ->
+                t `isUnqualifiedCommand` "set" &&
+                    ("errexit" `elem` oversimplify t || "e" `elem` map snd (getAllFlags t))
+            _ -> False
+    re = mkRegex "[[:space:]]-[^-]*e"
+
+prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done"
+prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"
+prop_checkLoopVariableReassignment3 = verifyNot checkLoopVariableReassignment "for i in *; do for j in *.bar; do true; done; done"
+checkLoopVariableReassignment params token =
+    potentially $ case token of
+        T_ForIn {} -> check
+        T_ForArithmetic {} -> check
+        _ -> Nothing
+  where
+    check = do
+        str <- loopVariable token
+        next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path
+        return $ do
+            warn (getId token) 2165 "This nested loop overrides the index variable of its parent."
+            warn (getId next)  2167 "This parent loop has its index variable overridden."
+    path = drop 1 $ getPath (parentMap params) token
+    loopVariable :: Token -> Maybe String
+    loopVariable t =
+        case t of
+            T_ForIn _ s _ _ -> return s
+            T_ForArithmetic _
+                (TA_Sequence _
+                    [TA_Assignment _ "="
+                        (TA_Expansion _ [T_Literal _ var]) _])
+                            _ _ _ -> return var
+            _ -> fail "not loop"
+
+prop_checkTrailingBracket1 = verify checkTrailingBracket "if -z n ]]; then true; fi "
+prop_checkTrailingBracket2 = verifyNot checkTrailingBracket "if [[ -z n ]]; then true; fi "
+prop_checkTrailingBracket3 = verify checkTrailingBracket "a || b ] && thing"
+prop_checkTrailingBracket4 = verifyNot checkTrailingBracket "run [ foo ]"
+prop_checkTrailingBracket5 = verifyNot checkTrailingBracket "run bar ']'"
+checkTrailingBracket _ token =
+    case token of
+        T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token
+        otherwise -> return ()
+  where
+    check t command =
+        case t of
+            T_NormalWord id [T_Literal _ str] -> potentially $ do
+                guard $ str `elem` [ "]]", "]" ]
+                let opposite = invert str
+                    parameters = oversimplify command
+                guard $ opposite `notElem` parameters
+                return $ warn id 2171 $
+                    "Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?"
+            otherwise -> return ()
+    invert s =
+        case s of
+            "]]" -> "[["
+            "]" -> "["
+            x -> x
 
 return []
 runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/ShellCheck/Analyzer.hs b/ShellCheck/Analyzer.hs
--- a/ShellCheck/Analyzer.hs
+++ b/ShellCheck/Analyzer.hs
@@ -19,9 +19,18 @@
 -}
 module ShellCheck.Analyzer (analyzeScript) where
 
-import ShellCheck.Interface
 import ShellCheck.Analytics
+import ShellCheck.AnalyzerLib
+import ShellCheck.Interface
+import Data.List
+import qualified ShellCheck.Checks.Commands
 
+
 -- TODO: Clean up the cruft this is layered on
 analyzeScript :: AnalysisSpec -> AnalysisResult
-analyzeScript = runAnalytics
+analyzeScript spec = AnalysisResult {
+    arComments =
+        filterByAnnotation (asScript spec) . nub $
+            runAnalytics spec
+            ++ ShellCheck.Checks.Commands.runChecks spec
+}
diff --git a/ShellCheck/AnalyzerLib.hs b/ShellCheck/AnalyzerLib.hs
new file mode 100644
--- /dev/null
+++ b/ShellCheck/AnalyzerLib.hs
@@ -0,0 +1,632 @@
+{-
+    Copyright 2012-2015 Vidar Holen
+
+    This file is part of ShellCheck.
+    http://www.vidarholen.net/contents/shellcheck
+
+    ShellCheck is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    ShellCheck is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE FlexibleContexts #-}
+module ShellCheck.AnalyzerLib where
+import ShellCheck.AST
+import ShellCheck.ASTLib
+import ShellCheck.Data
+import ShellCheck.Interface
+import ShellCheck.Parser
+import ShellCheck.Regex
+
+import Control.Arrow (first)
+import Control.Monad.Identity
+import Control.Monad.Reader
+import Control.Monad.State
+import Control.Monad.Writer
+import Data.Char
+import Data.List
+import Data.Maybe
+import qualified Data.Map as Map
+
+import Test.QuickCheck.All (forAllProperties)
+import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
+
+type Analysis = ReaderT Parameters (Writer [TokenComment]) ()
+
+
+data Parameters = Parameters {
+    variableFlow :: [StackData],
+    parentMap :: Map.Map Id Token,
+    shellType :: Shell,
+    shellTypeSpecified :: Bool
+    }
+
+data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
+data StackData =
+    StackScope Scope
+    | StackScopeEnd
+    -- (Base expression, specific position, var name, assigned values)
+    | Assignment (Token, Token, String, DataType)
+    | Reference (Token, Token, String)
+  deriving (Show)
+
+data DataType = DataString DataSource | DataArray DataSource
+  deriving (Show)
+
+data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
+  deriving (Show)
+
+data VariableState = Dead Token String | Alive deriving (Show)
+
+defaultSpec root = AnalysisSpec {
+    asScript = root,
+    asShellType = Nothing,
+    asExecutionMode = Executed
+}
+
+pScript s =
+  let
+    pSpec = ParseSpec {
+        psFilename = "script",
+        psScript = s
+    }
+  in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
+
+makeComment :: Severity -> Id -> Code -> String -> TokenComment
+makeComment severity id code note =
+    TokenComment id $ Comment severity code note
+
+addComment note = tell [note]
+
+warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
+warn  id code str = addComment $ makeComment WarningC id code str
+err   id code str = addComment $ makeComment ErrorC id code str
+info  id code str = addComment $ makeComment InfoC id code str
+style id code str = addComment $ makeComment StyleC id code str
+
+makeParameters spec =
+    let params = Parameters {
+        shellType = fromMaybe (determineShell root) $ asShellType spec,
+        shellTypeSpecified = isJust $ asShellType spec,
+        parentMap = getParentTree root,
+        variableFlow =
+            getVariableFlow (shellType params) (parentMap params) root
+    } in params
+  where root = asScript spec
+
+prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
+prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
+prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
+prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
+prop_determineShell4 = determineShell (fromJust $ pScript
+    "#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
+prop_determineShell5 = determineShell (fromJust $ pScript
+    "#shellcheck shell=sh\nfoo") == Sh
+prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
+determineShell t = fromMaybe Bash $ do
+    shellString <- foldl mplus Nothing $ getCandidates t
+    shellForExecutable shellString
+  where
+    forAnnotation t =
+        case t of
+            (ShellOverride s) -> return s
+            _ -> fail ""
+    getCandidates :: Token -> [Maybe String]
+    getCandidates t@(T_Script {}) = [Just $ fromShebang t]
+    getCandidates (T_Annotation _ annotations s) =
+        map forAnnotation annotations ++
+           [Just $ fromShebang s]
+    fromShebang (T_Script _ s t) = shellFor s
+
+    shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
+    shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
+    shellFor s = reverse . takeWhile (/= '/') . reverse $ s
+
+
+--- Context seeking
+
+getParentTree t =
+    snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
+  where
+    pre t = modify (first ((:) t))
+    post t = do
+        (_:rest, map) <- get
+        case rest of [] -> put (rest, map)
+                     (x:_) -> put (rest, Map.insert (getId t) x map)
+
+getTokenMap t =
+    execState (doAnalysis f t) Map.empty
+  where
+    f t = modify (Map.insert (getId t) t)
+
+
+-- Is this node self quoting for a regular element?
+isQuoteFree = isQuoteFreeNode False
+
+-- Is this node striclty self quoting, for array expansions
+isStrictlyQuoteFree = isQuoteFreeNode True
+
+
+isQuoteFreeNode strict tree t =
+    (isQuoteFreeElement t == Just True) ||
+        head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
+  where
+    -- Is this node self-quoting in itself?
+    isQuoteFreeElement t =
+        case t of
+            T_Assignment {} -> return True
+            T_FdRedirect {} -> return True
+            _ -> Nothing
+
+    -- Are any subnodes inherently self-quoting?
+    isQuoteFreeContext t =
+        case t of
+            TC_Noary _ DoubleBracket _ -> return True
+            TC_Unary _ DoubleBracket _ _ -> return True
+            TC_Binary _ DoubleBracket _ _ _ -> return True
+            TA_Sequence {} -> return True
+            T_Arithmetic {} -> return True
+            T_Assignment {} -> return True
+            T_Redirecting {} -> return $
+                if strict then False else
+                    -- Not true, just a hack to prevent warning about non-expansion refs
+                    any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
+            T_DoubleQuoted _ _ -> return True
+            T_DollarDoubleQuoted _ _ -> return True
+            T_CaseExpression {} -> return True
+            T_HereDoc {} -> return True
+            T_DollarBraced {} -> return True
+            -- When non-strict, pragmatically assume it's desirable to split here
+            T_ForIn {} -> return (not strict)
+            T_SelectIn {} -> return (not strict)
+            _ -> Nothing
+
+isParamTo tree cmd =
+    go
+  where
+    go x = case Map.lookup (getId x) tree of
+                Nothing -> False
+                Just parent -> check parent
+    check t =
+        case t of
+            T_SingleQuoted _ _ -> go t
+            T_DoubleQuoted _ _ -> go t
+            T_NormalWord _ _ -> go t
+            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 _ = 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 _ _ = False
+
+-- A list of the element and all its parents
+getPath tree t = t :
+    case Map.lookup (getId t) tree of
+        Nothing -> []
+        Just parent -> getPath tree parent
+
+isParentOf tree parent child =
+    elem (getId parent) . map getId $ getPath tree child
+
+parents params = getPath (parentMap params)
+
+pathTo t = do
+    parents <- reader parentMap
+    return $ getPath parents t
+
+-- Check whether a word is entirely output from a single command
+tokenIsJustCommandOutput t = case t of
+    T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
+    T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
+    T_NormalWord id [T_Backticked _ cmds] -> check cmds
+    T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
+    _ -> False
+  where
+    check [x] = not $ isOnlyRedirection x
+    check _ = False
+
+-- TODO: Replace this with a proper Control Flow Graph
+getVariableFlow shell parents t =
+    let (_, stack) = runState (doStackAnalysis startScope endScope t) []
+    in reverse stack
+  where
+    startScope t =
+        let scopeType = leadType shell parents t
+        in do
+            when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
+            when (assignFirst t) $ setWritten t
+
+    endScope t =
+        let scopeType = leadType shell parents t
+        in do
+            setRead t
+            unless (assignFirst t) $ setWritten t
+            when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
+
+    assignFirst (T_ForIn {}) = True
+    assignFirst (T_SelectIn {}) = True
+    assignFirst _ = False
+
+    setRead t =
+        let read    = getReferencedVariables parents t
+        in mapM_ (\v -> modify (Reference v:)) read
+
+    setWritten t =
+        let written = getModifiedVariables t
+        in mapM_ (\v -> modify (Assignment v:)) written
+
+
+leadType shell parents t =
+    case t of
+        T_DollarExpansion _ _  -> SubshellScope "$(..) expansion"
+        T_Backticked _ _  -> SubshellScope "`..` expansion"
+        T_Backgrounded _ _  -> SubshellScope "backgrounding &"
+        T_Subshell _ _  -> SubshellScope "(..) group"
+        T_CoProcBody _ _  -> SubshellScope "coproc"
+        T_Redirecting {}  ->
+            if fromMaybe False causesSubshell
+            then SubshellScope "pipeline"
+            else NoneScope
+        _ -> NoneScope
+  where
+    parentPipeline = do
+        parent <- Map.lookup (getId t) parents
+        case parent of
+            T_Pipeline {} -> return parent
+            _ -> Nothing
+
+    causesSubshell = do
+        (T_Pipeline _ _ list) <- parentPipeline
+        if length list <= 1
+            then return False
+            else if lastCreatesSubshell
+                then return True
+                else return . not $ (getId . head $ reverse list) == getId t
+
+    lastCreatesSubshell =
+        case shell of
+            Bash -> True
+            Dash -> True
+            Sh -> True
+            Ksh -> False
+
+getModifiedVariables t =
+    case t of
+        T_SimpleCommand _ vars [] ->
+            concatMap (\x -> case x of
+                                T_Assignment id _ name _ w  ->
+                                    [(x, x, name, dataTypeFrom DataString w)]
+                                _ -> []
+                      ) vars
+        c@(T_SimpleCommand {}) ->
+            getModifiedVariableCommand c
+
+        TA_Unary _ "++|" var -> maybeToList $ do
+            name <- getLiteralString var
+            return (t, t, name, DataString $ SourceFrom [t])
+        TA_Unary _ "|++" var -> maybeToList $ do
+            name <- getLiteralString var
+            return (t, t, name, DataString $ SourceFrom [t])
+        TA_Assignment _ op lhs rhs -> maybeToList $ do
+            guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
+            name <- getLiteralString lhs
+            return (t, t, name, DataString $ SourceFrom [rhs])
+
+        t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
+            [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
+
+        t@(T_CoProc _ name _) ->
+            [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
+
+        --Points to 'for' rather than variable
+        T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
+        T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
+        _ -> []
+
+isClosingFileOp op =
+    case op of
+        T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
+        T_IoFile _ (T_LESSAND  _) (T_NormalWord _ [T_Literal _ "-"]) -> True
+        _ -> False
+
+
+-- Consider 'export/declare -x' a reference, since it makes the var available
+getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
+    case x of
+        "export" -> if "f" `elem` flags
+            then []
+            else concatMap getReference rest
+        "declare" -> if any (`elem` flags) ["x", "p"]
+            then concatMap getReference rest
+            else []
+        "readonly" -> concatMap getReference rest
+        "trap" ->
+            case rest of
+                head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
+                _ -> []
+        _ -> []
+  where
+    getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
+    getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
+    getReference _ = []
+    flags = map snd $ getAllFlags base
+
+getReferencedVariableCommand _ = []
+
+getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
+   filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
+    case x of
+        "read" ->
+            let params = map getLiteral rest in
+                catMaybes . takeWhile isJust . reverse $ params
+        "getopts" ->
+            case rest of
+                opts:var:_ -> maybeToList $ getLiteral var
+                _ -> []
+
+        "let" -> concatMap letParamToLiteral rest
+
+        "export" ->
+            if "f" `elem` flags then [] else concatMap getModifierParamString rest
+
+        "declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
+        "typeset" -> declaredVars
+
+        "local" -> concatMap getModifierParamString rest
+        "readonly" -> concatMap getModifierParamString rest
+        "set" -> maybeToList $ do
+            params <- getSetParams rest
+            return (base, base, "@", DataString $ SourceFrom params)
+
+        "printf" -> maybeToList $ getPrintfVariable rest
+
+        "mapfile" -> maybeToList $ getMapfileArray base rest
+        "readarray" -> maybeToList $ getMapfileArray base rest
+
+        _ -> []
+  where
+    flags = map snd $ getAllFlags base
+    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_DoubleQuoted id2 [T_Literal id3 s]]) =
+        T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
+    stripEqualsFrom t = t
+
+    declaredVars = concatMap (getModifierParam defaultType) rest
+      where
+        defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
+
+    getLiteral t = do
+        s <- getLiteralString t
+        when ("-" `isPrefixOf` s) $ fail "argument"
+        return (base, t, s, DataString SourceExternal)
+
+    getModifierParamString = getModifierParam DataString
+
+    getModifierParam def t@(T_Assignment _ _ name _ value) =
+        [(base, t, name, dataTypeFrom def value)]
+    getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
+        name <- getLiteralString t
+        guard $ isVariableName name
+        return (base, t, name, def SourceDeclaration)
+    getModifierParam _ _ = []
+
+    letParamToLiteral token =
+          if var == ""
+            then []
+            else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
+        where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
+
+    getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
+    getSetParams (t:rest) =
+        let s = getLiteralString t in
+            case s of
+                Just "--" -> return rest
+                Just ('-':_) -> getSetParams rest
+                _ -> return (t:fromMaybe [] (getSetParams rest))
+    getSetParams [] = Nothing
+
+    getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
+      where
+        f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
+        f (_:rest) = f rest
+        f [] = fail "not found"
+
+    -- mapfile has some curious syntax allowing flags plus 0..n variable names
+    -- where only the first non-option one is used if any. Here we cheat and
+    -- just get the last one, if it's a variable name.
+    getMapfileArray base arguments = do
+        lastArg <- listToMaybe (reverse arguments)
+        name <- getLiteralString lastArg
+        guard $ isVariableName name
+        return (base, lastArg, name, DataArray SourceExternal)
+
+getModifiedVariableCommand _ = []
+
+getIndexReferences s = fromMaybe [] $ do
+    match <- matchRegex re s
+    index <- match !!! 0
+    return $ matchAllStrings variableNameRegex index
+  where
+    re = mkRegex "(\\[.*\\])"
+
+getReferencedVariables parents t =
+    case t of
+        T_DollarBraced id l -> let str = bracedString t in
+            (t, t, getBracedReference str) :
+                map (\x -> (l, l, x)) (getIndexReferences str)
+        TA_Expansion id _ ->
+            if isArithmeticAssignment t
+            then []
+            else getIfReference t t
+        T_Assignment id mode str _ word ->
+            [(t, t, str) | mode == Append] ++ specialReferences str t word
+
+        TC_Unary id _ "-v" token -> getIfReference t token
+        TC_Unary id _ "-R" token -> getIfReference t token
+        TC_Binary id DoubleBracket op lhs rhs ->
+            if isDereferencing op
+            then concatMap (getIfReference t) [lhs, rhs]
+            else []
+
+        t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
+            [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
+        x -> getReferencedVariableCommand x
+  where
+    -- Try to reduce false positives for unused vars only referenced from evaluated vars
+    specialReferences name base word =
+        if name `elem` [
+            "PS1", "PS2", "PS3", "PS4",
+            "PROMPT_COMMAND"
+          ]
+        then
+            map (\x -> (base, base, x)) $
+                getVariablesFromLiteralToken word
+        else []
+
+    literalizer (TA_Index {}) = return ""  -- x[0] becomes a reference of x
+    literalizer _ = Nothing
+
+    getIfReference context token = maybeToList $ do
+            str <- getLiteralStringExt literalizer token
+            guard . not $ null str
+            when (isDigit $ head str) $ fail "is a number"
+            return (context, token, getBracedReference str)
+
+    isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
+
+    isArithmeticAssignment t = case getPath parents t of
+        this: TA_Assignment _ "=" _ _ :_ -> True
+        _ -> False
+
+dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
+
+
+--- Command specific checks
+
+isCommand token str = isCommandMatch token (\cmd -> cmd  == str || ('/' : str) `isSuffixOf` cmd)
+isUnqualifiedCommand token str = isCommandMatch token (== str)
+
+isCommandMatch token matcher = fromMaybe False $ do
+    cmd <- getCommandName token
+    return $ matcher cmd
+
+isConfusedGlobRegex ('*':_) = True
+isConfusedGlobRegex [x,'*'] | x /= '\\' = True
+isConfusedGlobRegex _ = False
+
+isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
+isVariableChar x = isVariableStartChar x || isDigit x
+variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
+
+prop_isVariableName1 = isVariableName "_fo123"
+prop_isVariableName2 = not $ isVariableName "4"
+prop_isVariableName3 = not $ isVariableName "test: "
+isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
+isVariableName _ = False
+
+getVariablesFromLiteralToken token =
+    getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
+
+-- Try to get referenced variables from a literal string like "$foo"
+-- Ignores tons of cases like arithmetic evaluation and array indices.
+prop_getVariablesFromLiteral1 =
+    getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
+getVariablesFromLiteral string =
+    map (!! 0) $ matchAllSubgroups variableRegex string
+  where
+    variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
+
+prop_getBracedReference1 = getBracedReference "foo" == "foo"
+prop_getBracedReference2 = getBracedReference "#foo" == "foo"
+prop_getBracedReference3 = getBracedReference "#" == "#"
+prop_getBracedReference4 = getBracedReference "##" == "#"
+prop_getBracedReference5 = getBracedReference "#!" == "!"
+prop_getBracedReference6 = getBracedReference "!#" == "#"
+prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
+prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
+prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
+prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
+prop_getBracedReference11= getBracedReference "!os*" == ""
+prop_getBracedReference12= getBracedReference "!os?bar**" == ""
+getBracedReference s = fromMaybe s $
+    nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
+  where
+    noPrefix = dropPrefix s
+    dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
+    dropPrefix "" = ""
+    takeName s = do
+        let name = takeWhile isVariableChar s
+        guard . not $ null name
+        return name
+    getSpecial (c:_) =
+        if c `elem` "*@#?-$!" then return [c] else fail "not special"
+    getSpecial _ = fail "empty"
+
+    nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
+        let suffix = dropWhile isVariableChar rest
+        guard $ suffix /= rest -- e.g. ${!@}
+        first <- suffix !!! 0
+        guard $ first `elem` "*?"
+        return ""
+    nameExpansion _ = Nothing
+
+
+-- Useful generic functions
+potentially :: Monad m => Maybe (m ()) -> m ()
+potentially = fromMaybe (return ())
+
+headOrDefault _ (a:_) = a
+headOrDefault def _ = def
+
+(!!!) list i =
+    case drop i list of
+        [] -> Nothing
+        (r:_) -> Just r
+
+
+
+filterByAnnotation token =
+    filter (not . shouldIgnore)
+  where
+    idFor (TokenComment id _) = id
+    shouldIgnore note =
+        any (shouldIgnoreFor (getCode note)) $
+            getPath parents (T_Bang $ idFor note)
+    shouldIgnoreFor num (T_Annotation _ anns _) =
+        any hasNum anns
+      where
+        hasNum (DisableComment ts) = num == ts
+        hasNum _ = False
+    shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
+    shouldIgnoreFor _ _ = False
+    parents = getParentTree token
+    getCode (TokenComment _ (Comment _ c _)) = c
+
+
+return [] 
+runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/ShellCheck/Checks/Commands.hs b/ShellCheck/Checks/Commands.hs
new file mode 100644
--- /dev/null
+++ b/ShellCheck/Checks/Commands.hs
@@ -0,0 +1,560 @@
+{-
+    Copyright 2012-2015 Vidar Holen
+
+    This file is part of ShellCheck.
+    http://www.vidarholen.net/contents/shellcheck
+
+    ShellCheck is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    ShellCheck is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE FlexibleContexts #-}
+
+-- This module contains checks that examine specific commands by name.
+module ShellCheck.Checks.Commands (runChecks
+    , ShellCheck.Checks.Commands.runTests
+) where
+
+import ShellCheck.AST
+import ShellCheck.ASTLib
+import ShellCheck.AnalyzerLib
+import ShellCheck.Data
+import ShellCheck.Interface
+import ShellCheck.Parser
+import ShellCheck.Regex
+
+import Control.Monad
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Data.Char
+import Data.List
+import Data.Maybe
+import qualified Data.Map as Map
+import Test.QuickCheck.All (forAllProperties)
+import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
+
+data CommandName = Exactly String | Basename String
+    deriving (Eq, Ord)
+
+data CommandCheck =
+    CommandCheck CommandName (Token -> Analysis)
+
+nullCheck :: Token -> Analysis
+nullCheck _ = return ()
+
+
+verify :: CommandCheck -> String -> Bool
+verify f s = producesComments f s == Just True
+verifyNot f s = producesComments f s == Just False
+
+producesComments :: CommandCheck -> String -> Maybe Bool
+producesComments f s = do
+        root <- pScript s
+        return . not . null $ runList (defaultSpec root) [f]
+
+composeChecks f g t = do
+    f t
+    g t
+
+arguments (T_SimpleCommand _ _ (cmd:args)) = args
+
+commandChecks :: [CommandCheck]
+commandChecks = [
+    checkTr
+    ,checkFindNameGlob
+    ,checkNeedlessExpr
+    ,checkGrepRe
+    ,checkTrapQuotes
+    ,checkReturn
+    ,checkFindExecWithSingleArgument
+    ,checkUnusedEchoEscapes
+    ,checkInjectableFindSh
+    ,checkFindActionPrecedence
+    ,checkMkdirDashPM
+    ,checkNonportableSignals
+    ,checkInteractiveSu
+    ,checkSshCommandString
+    ,checkPrintfVar
+    ,checkUuoeCmd
+    ,checkSetAssignment
+    ,checkExportedExpansions
+    ,checkAliasesUsesArgs
+    ,checkAliasesExpandEarly
+    ]
+
+buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
+buildCommandMap = foldl' addCheck Map.empty
+  where
+    addCheck map (CommandCheck name function) =
+        Map.insertWith' composeChecks name function map
+
+
+checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
+checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
+    name <- getLiteralString cmd
+    return $
+        if '/' `elem` name
+        then
+            Map.findWithDefault nullCheck (Basename $ basename name) map t
+        else do
+            Map.findWithDefault nullCheck (Exactly name) map t
+            Map.findWithDefault nullCheck (Basename name) map t
+
+  where
+    basename = reverse . takeWhile (/= '/') . reverse
+checkCommand _ _ = return ()
+
+runList spec list = notes
+    where
+        root = asScript spec
+        params = makeParameters spec
+        notes = execWriter $ runReaderT (doAnalysis (checkCommand map) root) params
+        map = buildCommandMap list
+
+runChecks spec = runList spec commandChecks
+
+
+prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
+prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
+prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
+prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
+prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
+prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
+prop_checkTr4 = verifyNot checkTr "ls [a-z]"
+prop_checkTr5 = verify checkTr "tr foo bar"
+prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
+prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
+prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
+prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
+prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
+checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
+  where
+    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) $
+            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) && ('*' `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 relevant /= nub relevant
+
+prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
+prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
+prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
+checkFindNameGlob = CommandCheck (Basename "find") (f . arguments)  where
+    acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
+    acceptsGlob _ = False
+    f [] = return ()
+    f [x] = return ()
+    f (a:b:r) = do
+        when (acceptsGlob (getLiteralString a) && isGlob b) $ do
+            let (Just s) = getLiteralString a
+            warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
+        f (b:r)
+
+
+prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
+prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
+prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
+prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
+checkNeedlessExpr = CommandCheck (Basename "expr") f where
+    f t =
+        when (all (`notElem` exceptions) (words $ arguments t)) $
+            style (getId t) 2003
+                "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
+    -- These operators are hard to replicate in POSIX
+    exceptions = [ ":", "<", ">", "<=", ">=" ]
+    words = mapMaybe getLiteralString
+
+
+prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
+prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
+prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
+prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
+prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v  --regex=moo *"
+prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
+prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
+prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
+prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
+prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
+prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
+
+checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where
+    -- --regex=*(extglob) doesn't work. Fixme?
+    skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
+    skippable _ = False
+    f [] = return ()
+    f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
+    f (re:_) = do
+        when (isGlob re) $
+            warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
+        let string = concat $ oversimplify re
+        if isConfusedGlobRegex string then
+            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 ++ "'."
+
+    wordStartingWith c =
+        head . filter ([c] `isPrefixOf`) $ candidates
+      where
+        candidates =
+            sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
+
+    getSuspiciousRegexWildcard str =
+        if not $ str `matches` contra
+        then do
+            match <- matchRegex suspicious str
+            str <- match !!! 0
+            str !!! 0
+        else
+            fail "looks good"
+      where
+        suspicious = mkRegex "([A-Za-z1-9])\\*"
+        contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
+
+
+prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
+prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
+prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
+prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
+checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
+    f (x:_) = checkTrap x
+    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."
+    checkExpansions (T_DollarExpansion id _) = warning id
+    checkExpansions (T_Backticked id _) = warning id
+    checkExpansions (T_DollarBraced id _) = warning id
+    checkExpansions (T_DollarArithmetic id _) = warning id
+    checkExpansions _ = return ()
+
+
+prop_checkReturn1 = verifyNot checkReturn "return"
+prop_checkReturn2 = verifyNot checkReturn "return 1"
+prop_checkReturn3 = verifyNot checkReturn "return $var"
+prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
+prop_checkReturn5 = verify checkReturn "return -1"
+prop_checkReturn6 = verify checkReturn "return 1000"
+prop_checkReturn7 = verify checkReturn "return 'hello world'"
+checkReturn = CommandCheck (Exactly "return") (f . arguments)
+  where
+    f (first:second:_) =
+        err (getId second) 2151
+            "Only one integer 0-255 can be returned. Use stdout for other data."
+    f [value] =
+        when (isInvalid $ literal value) $
+            err (getId value) 2152
+                "Can only return 0-255. Other data should be written to stdout."
+    f _ = return ()
+
+    isInvalid s = s == "" || any (not . isDigit) s || length s > 5
+        || let value = (read s :: Integer) in value > 255
+
+    literal token = fromJust $ getLiteralStringExt lit token
+    lit (T_DollarBraced {}) = return "0"
+    lit (T_DollarArithmetic {}) = return "0"
+    lit (T_DollarExpansion {}) = return "0"
+    lit (T_Backticked {}) = return "0"
+    lit _ = return "WTF"
+
+
+prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
+prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
+prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
+checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
+  where
+    f = void . sequence . mapMaybe check . tails
+    check (exec:arg:term:_) = do
+        execS <- getLiteralString exec
+        termS <- getLiteralString term
+        cmdS <- getLiteralStringExt (const $ return " ") arg
+
+        guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
+        guard $ cmdS `matches` commandRegex
+        return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
+    check _ = Nothing
+    commandRegex = mkRegex "[ |;]"
+
+
+prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
+prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
+prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
+prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
+prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
+checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
+  where
+    isDashE = mkRegex "^-.*e"
+    hasEscapes = mkRegex "\\\\[rnt]"
+    f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
+        return ()
+      where allButLast = reverse . drop 1 . reverse $ args
+    f args = mapM_ checkEscapes args
+
+    checkEscapes (T_NormalWord _ args) =
+        mapM_ checkEscapes args
+    checkEscapes (T_DoubleQuoted id args) =
+        mapM_ checkEscapes args
+    checkEscapes (T_Literal id str) = examine id str
+    checkEscapes (T_SingleQuoted id str) = examine id str
+    checkEscapes _ = return ()
+
+    examine id str =
+        when (str `matches` hasEscapes) $
+            info id 2028 "echo won't expand escape sequences. Consider printf."
+
+
+prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
+prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
+prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
+checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
+  where
+    check args = do
+        let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
+        match pattern idStrings
+
+    match _ [] = return ()
+    match [] (next:_) = action next
+    match (p:tests) ((id, arg):args) = do
+        when (p arg) $ match tests args
+        match (p:tests) args
+
+    pattern = [
+        (`elem` ["-exec", "-execdir"]),
+        (`elem` ["sh", "bash", "ksh"]),
+        (== "-c")
+        ]
+    action (id, arg) =
+        when ("{}" `isInfixOf` arg) $
+            warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
+
+
+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 = CommandCheck (Basename "find") (f . arguments)
+  where
+    pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
+    f list | length list < length pattern = return ()
+    f list@(_:rest) =
+        if and (zipWith ($) pattern list)
+        then warnFor (list !! (length pattern - 1))
+        else f rest
+    isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
+    isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
+    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."
+
+
+prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
+prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
+prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
+prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
+prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
+prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
+prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
+prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
+prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
+prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
+prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
+prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
+prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
+prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
+prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
+checkMkdirDashPM = CommandCheck (Basename "mkdir") check
+  where
+    check t = potentially $ do
+        let flags = getAllFlags t
+        dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
+        dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
+        guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir  is fine, but dir/subdir is not.
+        return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
+    couldHaveSubdirs t = fromMaybe True $ do
+        name <- getLiteralString t
+        return $ '/' `elem` name
+
+
+prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
+prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
+prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
+prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
+prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
+prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
+checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
+  where
+    f = mapM_ check
+    check param = potentially $ do
+        str <- getLiteralString param
+        let id = getId param
+        return $ sequence_ $ mapMaybe (\f -> f id str) [
+            checkNumeric,
+            checkUntrappable
+            ]
+
+    checkNumeric id str = do
+        guard $ not (null str)
+        guard $ all isDigit str
+        guard $ str /= "0" -- POSIX exit trap
+        guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
+        return $ warn id 2172
+            "Trapping signals by number is not well defined. Prefer signal names."
+
+    checkUntrappable id str = do
+        guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
+        return $ err id 2173
+            "SIGKILL/SIGSTOP can not be trapped."
+
+
+prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
+prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
+prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
+prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
+checkInteractiveSu = CommandCheck (Basename "su") f
+  where
+    f cmd = when (length (arguments cmd) <= 1) $ do
+        path <- pathTo cmd
+        when (all undirected path) $
+            info (getId cmd) 2117
+                "To run commands as another user, use su -c or sudo."
+
+    undirected (T_Pipeline _ _ l) = length l <= 1
+    -- This should really just be modifications to stdin, but meh
+    undirected (T_Redirecting _ list _) = null list
+    undirected _ = True
+
+
+-- This is hard to get right without properly parsing ssh args
+prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
+prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
+prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
+checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
+  where
+    nonOptions =
+        filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
+    f args =
+        case nonOptions args of
+            (hostport:r@(_:_)) -> checkArg $ last r
+            _ -> return ()
+    checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
+        case filter (not . isConstant) parts of
+            [] -> return ()
+            (x:_) -> info (getId x) 2029
+                "Note that, unescaped, this expands on the client side."
+    checkArg _ = return ()
+
+
+prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
+prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
+prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
+prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
+checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
+    f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
+    f (format:params) = check format
+    f _ = return ()
+    check format =
+        unless ('%' `elem` concat (oversimplify 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`"
+prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
+prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
+prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
+prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
+checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
+    msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
+    f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
+    f _ = return ()
+
+
+prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
+prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
+prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
+prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
+prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
+prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
+checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
+  where
+    f (var:value:rest) =
+        let str = literal var in
+            when (isVariableName str || isAssignment str) $
+                msg (getId var)
+    f (var:_) =
+        when (isAssignment $ literal var) $
+            msg (getId var)
+    f _ = return ()
+
+    msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
+
+    isAssignment str = '=' `elem` str
+    literal (T_NormalWord _ l) = concatMap literal l
+    literal (T_Literal _ str) = str
+    literal _ = "*"
+
+
+prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
+prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
+prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
+checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
+  where
+    check = mapM_ checkForVariables
+    checkForVariables f =
+        case getWordParts f of
+            [t@(T_DollarBraced {})] ->
+                warn (getId t) 2163 "Exporting an expansion rather than a variable."
+            _ -> return ()
+
+
+prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
+prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
+prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
+checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
+  where
+    re = mkRegex "\\$\\{?[0-9*@]"
+    f = mapM_ checkArg
+    checkArg arg =
+        let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
+            when ('=' `elem` string && string `matches` re) $
+                err (getId arg) 2142
+                    "Aliases can't use positional parameters. Use a function."
+
+
+prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
+prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
+prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
+checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
+  where
+    f = mapM_ checkArg
+    checkArg arg | '=' `elem` concat (oversimplify 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 ()
+
+
+return []
+runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/ShellCheck/Parser.hs b/ShellCheck/Parser.hs
--- a/ShellCheck/Parser.hs
+++ b/ShellCheck/Parser.hs
@@ -52,20 +52,24 @@
 
 backslash :: Monad m => SCParser m Char
 backslash = char '\\'
-linefeed = optional carriageReturn >> char '\n'
+linefeed :: Monad m => SCParser m Char
+linefeed = do
+    optional carriageReturn
+    c <- char '\n'
+    readPendingHereDocs
+    return c
 singleQuote = char '\'' <|> unicodeSingleQuote
 doubleQuote = char '"' <|> unicodeDoubleQuote
 variableStart = upper <|> lower <|> oneOf "_"
 variableChars = upper <|> lower <|> digit <|> oneOf "_"
 functionChars = variableChars <|> oneOf ":+-.?"
 specialVariable = oneOf "@*#?-$!"
-tokenDelimiter = oneOf "&|;<> \t\n\r" <|> almostSpace
 quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
 quotable = almostSpace <|> unicodeDoubleQuote <|> oneOf quotableChars
 bracedQuotable = oneOf "}\"$`'"
 doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars
 doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars
-whitespace = oneOf " \t\n" <|> carriageReturn <|> almostSpace
+whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed
 linewhitespace = oneOf " \t" <|> almostSpace
 
 suspectCharAfterQuotes = variableChars <|> char '%'
@@ -138,15 +142,24 @@
         | ContextSource String
     deriving (Show)
 
+data HereDocContext =
+        HereDocPending Token -- on linefeed, read this T_HereDoc
+        | HereDocBoundary -- but don't consider heredocs before this
+    deriving (Show)
+
 data UserState = UserState {
     lastId :: Id,
     positionMap :: Map.Map Id SourcePos,
-    parseNotes :: [ParseNote]
+    parseNotes :: [ParseNote],
+    hereDocMap :: Map.Map Id [Token],
+    pendingHereDocs :: [HereDocContext]
 }
 initialUserState = UserState {
     lastId = Id $ -1,
     positionMap = Map.empty,
-    parseNotes = []
+    parseNotes = [],
+    hereDocMap = Map.empty,
+    pendingHereDocs = []
 }
 
 codeForParseNote (ParseNote _ _ code _) = code
@@ -155,7 +168,6 @@
     where
         pos = fromJust $ Map.lookup id map
 
-
 getLastId = lastId <$> getState
 
 getNextIdAt sourcepos = do
@@ -173,6 +185,58 @@
     pos <- getPosition
     getNextIdAt pos
 
+addToHereDocMap id list = do
+    state <- getState
+    let map = hereDocMap state
+    putState $ state {
+        hereDocMap = Map.insert id list map
+    }
+
+withHereDocBoundary p = do
+    pushBoundary
+    do
+        v <- p
+        popBoundary
+        return v
+     <|> do
+        popBoundary
+        fail ""
+  where
+    pushBoundary = do
+        state <- getState
+        let docs = pendingHereDocs state
+        putState $ state {
+            pendingHereDocs = HereDocBoundary : docs
+        }
+    popBoundary = do
+        state <- getState
+        let docs = tail $ dropWhile (not . isHereDocBoundary) $
+                    pendingHereDocs state
+        putState $ state {
+            pendingHereDocs = docs
+        }
+
+addPendingHereDoc t = do
+    state <- getState
+    let docs = pendingHereDocs state
+    putState $ state {
+        pendingHereDocs = HereDocPending t : docs
+    }
+
+popPendingHereDocs = do
+    state <- getState
+    let (pending, boundary) = break isHereDocBoundary $ pendingHereDocs state
+    putState $ state {
+        pendingHereDocs = boundary
+    }
+    return . map extract . reverse $ pendingHereDocs state
+  where
+    extract (HereDocPending t) = t
+
+isHereDocBoundary x = case x of
+    HereDocBoundary -> True
+    otherwise -> False
+
 getMap = positionMap <$> getState
 getParseNotes = parseNotes <$> getState
 
@@ -522,8 +586,8 @@
       where
         readGlobLiteral = do
             id <- getNextId
-            s <- many1 (extglobStart <|> oneOf "{}[]$")
-            return $ T_Literal id s
+            s <- extglobStart <|> oneOf "{}[]$"
+            return $ T_Literal id [s]
         readGroup = called "regex grouping" $ do
             id <- getNextId
             char '('
@@ -644,7 +708,9 @@
         l <- readAssignment `sepBy` (char ',' >> spacing)
         return $ TA_Sequence id l
 
-    readAssignment = readTrinary `splitBy` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
+    readAssignment = chainr1 readTrinary readAssignmentOp
+    readAssignmentOp = readComboOp ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] TA_Assignment
+
     readTrinary = do
         x <- readLogicalOr
         do
@@ -772,7 +838,7 @@
 readAnnotation = called "shellcheck annotation" $ do
     try readAnnotationPrefix
     many1 linewhitespace
-    values <- many1 (readDisable <|> readSourceOverride)
+    values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride)
     linefeed
     many linewhitespace
     return $ concat values
@@ -789,6 +855,14 @@
         filename <- many1 $ noneOf " \n"
         return [SourceOverride filename]
 
+    readShellOverride = forKey "shell" $ do
+        pos <- getPosition
+        shell <- many1 $ noneOf " \n"
+        when (isNothing $ shellForExecutable shell) $
+            parseNoteAt pos ErrorC 1103
+                "This shell type is unknown. Use e.g. sh or bash."
+        return [ShellOverride shell]
+
     forKey s p = do
         try $ string s
         char '='
@@ -811,6 +885,9 @@
 prop_readNormalWord4 = isOk readNormalWord "$\"foo\"$'foo\nbar'"
 prop_readNormalWord5 = isWarning readNormalWord "${foo}}"
 prop_readNormalWord6 = isOk readNormalWord "foo/{}"
+prop_readNormalWord7 = isOk readNormalWord "foo\\\nbar"
+prop_readNormalWord8 = isWarning readSubshell "(foo\\ \nbar)"
+prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)"
 readNormalWord = readNormalishWord ""
 
 readNormalishWord end = do
@@ -1090,6 +1167,7 @@
     backslash
     do
         next <- quotable <|> oneOf "?*@!+[]{}.,~#"
+        when (next == ' ') $ checkTrailingSpaces pos <|> return ()
         return $ if next == '\n' then "" else [next]
       <|>
         do
@@ -1106,7 +1184,12 @@
     escapedChar 'r' = Just "carriage return"
     escapedChar _ = Nothing
 
+    checkTrailingSpaces pos = lookAhead . try $ do
+        many linewhitespace
+        void linefeed <|> eof
+        parseProblemAt pos ErrorC 1101 "Delete trailing spaces after \\ to break line (or use quotes for literal space)."
 
+
 prop_readExtglob1 = isOk readExtglob "!(*.mp3)"
 prop_readExtglob2 = isOk readExtglob "!(*.mp3|*.wmv)"
 prop_readExtglob4 = isOk readExtglob "+(foo \\) bar)"
@@ -1217,8 +1300,11 @@
 
 readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
 readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
-readDollarExpression = readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable
 
+prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))"
+prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)"
+readDollarExpression = readTripleParenthesis "$" readDollarArithmetic readDollarExpansion <|> readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable
+
 prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
 readDollarSingleQuote = called "$'..' expression" $ do
     id <- getNextId
@@ -1243,7 +1329,9 @@
     id <- getNextId
     try (string "$((")
     c <- readArithmeticContents
-    string "))"
+    pos <- getPosition
+    char ')'
+    char ')' <|> fail "Expected a double )) to end the $((..))"
     return (T_DollarArithmetic id c)
 
 readDollarBracket = called "$[..] expression" $ do
@@ -1261,6 +1349,25 @@
     string "))"
     return (T_Arithmetic id c)
 
+-- Check if maybe ((( was intended as ( (( rather than (( (
+readTripleParenthesis prefix expected alternative = do
+    pos <- try . lookAhead $ do
+        string prefix
+        p <- getPosition
+        string "(((" -- should optimally be "((" but it's noisy and rarely helpful
+        return p
+
+    -- If the expected parser fails, try the alt.
+    -- If the alt fails, run the expected one again for the errors.
+    try expected <|> tryAlt pos <|> expected
+  where
+    tryAlt pos = do
+        t <- try alternative
+        parseNoteAt pos WarningC 1102 $
+            "Shells differ in parsing ambiguous " ++ prefix ++ "(((. Use spaces: " ++ prefix ++ "( (( ."
+        return t
+
+
 prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }"
 prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}"
 readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
@@ -1343,14 +1450,17 @@
     n <- lookAhead (anyChar <|> (eof >> return '_'))
     return $ T_Literal id "$"
 
-prop_readHereDoc = isOk readHereDoc "<< foo\nlol\ncow\nfoo"
-prop_readHereDoc2 = isWarning readHereDoc "<<- EOF\n  cow\n  EOF"
-prop_readHereDoc3 = isOk readHereDoc "<< foo\n$\"\nfoo"
-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"
-prop_readHereDoc8 = isOk readHereDoc "<<foo>>bar\netc\nfoo"
+prop_readHereDoc = isOk readScript "cat << foo\nlol\ncow\nfoo"
+prop_readHereDoc2 = isWarning readScript "cat <<- EOF\n  cow\n  EOF"
+prop_readHereDoc3 = isOk readScript "cat << foo\n$\"\nfoo"
+prop_readHereDoc4 = isOk readScript "cat << foo\n`\nfoo"
+prop_readHereDoc5 = isOk readScript "cat <<- !foo\nbar\n!foo"
+prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
+prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
+prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
+prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
+prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
+prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
 readHereDoc = called "here document" $ do
     fid <- getNextId
     pos <- getPosition
@@ -1367,24 +1477,11 @@
             liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
             <|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
             <|> (readToken >>= (\x -> return (Unquoted, x)))
-    spacing
 
-    startPos <- getPosition
-    hereData <- anyChar `reluctantlyTill` do
-                    linefeed
-                    spacing
-                    string endToken
-                    disregard linefeed  <|> eof
-
-    do
-        linefeed
-        spaces <- spacing
-        verifyHereDoc dashed quoted spaces hereData
-        string endToken
-        parsedData <- parseHereData quoted startPos hereData
-        return $ T_FdRedirect fid "" $ T_HereDoc hid dashed quoted endToken parsedData
-     `attempting` (eof >> debugHereDoc tokenPosition endToken hereData)
-
+    -- add empty tokens for now, read the rest in readPendingHereDocs
+    let doc = T_HereDoc hid dashed quoted endToken []
+    addPendingHereDoc doc
+    return $ T_FdRedirect fid "" doc
   where
     stripLiteral (T_Literal _ x) = x
     stripLiteral (T_SingleQuoted _ x) = x
@@ -1399,6 +1496,27 @@
             c <- anyChar
             return [c]
 
+
+readPendingHereDocs = do
+    docs <- popPendingHereDocs
+    mapM_ readDoc docs
+  where
+    readDoc (T_HereDoc id dashed quoted endToken _) = do
+        pos <- getPosition
+        hereData <- anyChar `reluctantlyTill` do
+                        spacing
+                        string endToken
+                        disregard (char '\n') <|> eof
+        do
+            spaces <- spacing
+            verifyHereDoc dashed quoted spaces hereData
+            string endToken
+            parsedData <- parseHereData quoted pos hereData
+            list <- parseHereData quoted pos hereData
+            addToHereDocMap id list
+
+         `attempting` (eof >> debugHereDoc pos endToken hereData)
+
     parseHereData Quoted startPos hereData = do
         id <- getNextIdAt startPos
         return [T_Literal id hereData]
@@ -1483,7 +1601,7 @@
     word <- readNormalWord
     return $ T_FdRedirect id "" $ T_HereString id2 word
 
-readNewlineList = many1 ((newline <|> carriageReturn) `thenSkip` spacing)
+readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing)
 readLineBreak = optional readNewlineList
 
 prop_readSeparator1 = isWarning readScript "a &; b"
@@ -1657,6 +1775,7 @@
         eof
         return []
 
+prop_readTerm = isOk readTerm "time ( foo; bar; )"
 readTerm = do
     allspacing
     m <- readAndOr
@@ -2010,6 +2129,7 @@
         readWithoutFunction = try $ do
             id <- getNextId
             name <- readFunctionName
+            guard $ name /= "time"  -- Interfers with time ( foo )
             spacing
             readParens
             return $ T_Function id (FunctionKeyword False) (FunctionParentheses True) name
@@ -2054,7 +2174,20 @@
 prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
 readCompoundCommand = do
     id <- getNextId
-    cmd <- choice [ readBraceGroup, readArithmeticExpression, readSubshell, readCondition, readWhileClause, readUntilClause, readIfClause, readForClause, readSelectClause, readCaseClause, readFunctionDefinition]
+    cmd <- choice [
+        readBraceGroup,
+        readTripleParenthesis "" readArithmeticExpression readSubshell,
+        readArithmeticExpression,
+        readSubshell,
+        readCondition,
+        readWhileClause,
+        readUntilClause,
+        readIfClause,
+        readForClause,
+        readSelectClause,
+        readCaseClause,
+        readFunctionDefinition
+        ]
     spacing
     redirs <- many readIoRedirect
     unless (null redirs) $ optional $ do
@@ -2083,14 +2216,30 @@
         lookAhead $ char '-'
         readCmdWord
 
--- Fixme: this is a hack that doesn't handle let '++c' or let a\>b
+-- Fixme: this is a hack that doesn't handle let c='4'"5" or let a\>b
+readLetSuffix :: Monad m => SCParser m [Token]
 readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
   where
+    readLetExpression :: Monad m => SCParser m Token
     readLetExpression = do
         startPos <- getPosition
         expression <- readStringForParser readCmdWord
-        subParse startPos readArithmeticContents expression
+        let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
+        subParse newPos readArithmeticContents unQuoted
 
+    kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
+    kludgeAwayQuotes s p =
+        case s of
+            first:rest@(_:_) ->
+                let (last:backwards) = reverse rest
+                    middle = reverse backwards
+                in
+                    if first `elem` "'\"" && first == last
+                    then (middle, updatePosChar p first)
+                    else (s, p)
+            x -> (s, p)
+
+
 -- bash allows a=(b), ksh allows $a=(b). dash allows neither. Let's warn.
 readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
   where
@@ -2288,8 +2437,11 @@
 
 prop_readShebang1 = isOk readShebang "#!/bin/sh\n"
 prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
+prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n"
+prop_readShebang4 = isWarning readShebang "! /bin/sh"
 readShebang = do
-    try readCorrect <|> try readSwapped
+    try readCorrect <|> try readSwapped <|> try readMissingHash
+    many linewhitespace
     str <- many $ noneOf "\r\n"
     optional carriageReturn
     optional linefeed
@@ -2302,6 +2454,15 @@
         parseProblemAt pos ErrorC 1084
             "Use #!, not !#, for the shebang."
 
+    readMissingHash = do
+        pos <- getPosition
+        char '!'
+        lookAhead $ do
+            many linewhitespace
+            char '/'
+        parseProblemAt pos ErrorC 1104
+            "Use #!, not just !, for the shebang."
+
 verifyEof = eof <|> choice [
         ifParsable g_Lparen $
             parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
@@ -2332,9 +2493,11 @@
     verifyShell pos (getShell sb)
     if isValidShell (getShell sb) /= Just False
       then do
-            commands <- readCompoundListOrEmpty
+            annotationId <- getNextId
+            annotations <- readAnnotations
+            commands <- withAnnotations annotations readCompoundListOrEmpty
             verifyEof
-            return $ T_Script id sb commands
+            return $ T_Annotation annotationId annotations $  T_Script id sb commands
         else do
             many anyChar
             return $ T_Script id sb []
@@ -2405,9 +2568,8 @@
 
 parseWithNotes parser = do
     item <- parser
-    map <- getMap
-    parseNotes <- getParseNotes
-    return (item, map, nub . sortNotes $ parseNotes)
+    state <- getState
+    return (item, state)
 
 compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2)
 sortNotes = sortBy compareNotes
@@ -2447,11 +2609,12 @@
 parseShell sys name contents = do
     (result, state) <- runParser sys (parseWithNotes readScript) name contents
     case result of
-        Right (script, tokenMap, notes) ->
+        Right (script, userstate) ->
             return ParseResult {
-                prComments = map toPositionedComment $ nub $ notes ++ parseProblems state,
-                prTokenPositions = Map.map posToPos tokenMap,
-                prRoot = Just script
+                prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
+                prTokenPositions = Map.map posToPos (positionMap userstate),
+                prRoot = Just $
+                    reattachHereDocs script (hereDocMap userstate)
             }
         Left err ->
             return ParseResult {
@@ -2472,6 +2635,13 @@
     second (ContextName pos str) = ParseNote pos InfoC 1009 $
         "The mentioned parser error was in this " ++ str ++ "."
 
+reattachHereDocs root map =
+    doTransform f root
+  where
+    f t@(T_HereDoc id dash quote string []) = fromMaybe t $ do
+        list <- Map.lookup id map
+        return $ T_HereDoc id dash quote string list
+    f t = t
 
 toPositionedComment :: ParseNote -> PositionedComment
 toPositionedComment (ParseNote pos severity code message) =
diff --git a/shellcheck.1 b/shellcheck.1
--- a/shellcheck.1
+++ b/shellcheck.1
@@ -36,7 +36,7 @@
 .SH OPTIONS
 .TP
 .B \f[B]\-C\f[][\f[I]WHEN\f[]],\ \f[B]\-\-color\f[][=\f[I]WHEN\f[]]
-For TTY outut, enable colors \f[I]always\f[], \f[I]never\f[] or
+For TTY output, enable colors \f[I]always\f[], \f[I]never\f[] or
 \f[I]auto\f[].
 The default is \f[I]auto\f[].
 \f[B]\-\-color\f[] without an argument is equivalent to
diff --git a/shellcheck.1.md b/shellcheck.1.md
--- a/shellcheck.1.md
+++ b/shellcheck.1.md
@@ -34,7 +34,7 @@
 
 **-C**[*WHEN*],\ **--color**[=*WHEN*]
 
-:   For TTY outut, enable colors *always*, *never* or *auto*. The default
+:   For TTY output, enable colors *always*, *never* or *auto*. The default
     is *auto*. **--color** without an argument is equivalent to
     **--color=always**.
 
diff --git a/test/shellcheck.hs b/test/shellcheck.hs
--- a/test/shellcheck.hs
+++ b/test/shellcheck.hs
@@ -4,13 +4,17 @@
 import System.Exit
 import qualified ShellCheck.Checker
 import qualified ShellCheck.Analytics
+import qualified ShellCheck.AnalyzerLib
 import qualified ShellCheck.Parser
+import qualified ShellCheck.Checks.Commands
 
 main = do
     putStrLn "Running ShellCheck tests..."
     results <- sequence [
         ShellCheck.Checker.runTests,
+        ShellCheck.Checks.Commands.runTests,
         ShellCheck.Analytics.runTests,
+        ShellCheck.AnalyzerLib.runTests,
         ShellCheck.Parser.runTests
       ]
     if and results
