diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -109,7 +109,8 @@
 * [Codacy](https://www.codacy.com/)
 * [Code Climate](https://codeclimate.com/)
 * [Code Factor](https://www.codefactor.io/)
-* [Github](https://github.com/features/actions)(only Linux)
+* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
+* [Github](https://github.com/features/actions) (only Linux)
 
 Services and platforms with third party plugins:
 
@@ -148,7 +149,7 @@
 
     pacman -S shellcheck
 
-or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
+or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
 
 On Gentoo based distros:
 
@@ -167,10 +168,14 @@
 
     pkg install hs-ShellCheck
 
-On OS X with homebrew:
+On macOS (OS X) with Homebrew:
 
     brew install shellcheck
 
+Or with MacPorts:
+
+    sudo port install shellcheck
+
 On OpenBSD:
 
     pkg_add shellcheck
@@ -197,6 +202,10 @@
 C:\> scoop install shellcheck
 ```
 
+From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
+
+    conda install -c conda-forge shellcheck
+
 From Snap Store:
 
     snap install --channel=edge shellcheck
@@ -220,7 +229,7 @@
 * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
 * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
 * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
-* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
+* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
 * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
 
 or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
@@ -264,7 +273,7 @@
 
 ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
 
-On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
+On macOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
 
     $ brew install cabal-install
 
diff --git a/ShellCheck.cabal b/ShellCheck.cabal
--- a/ShellCheck.cabal
+++ b/ShellCheck.cabal
@@ -1,5 +1,5 @@
 Name:             ShellCheck
-Version:          0.7.1
+Version:          0.7.2
 Synopsis:         Shell script analysis tool
 License:          GPL-3
 License-file:     LICENSE
@@ -8,7 +8,7 @@
 Maintainer:       vidar@vidarholen.net
 Homepage:         https://www.shellcheck.net/
 Build-Type:       Simple
-Cabal-Version:    >= 1.8
+Cabal-Version:    >= 1.10
 Bug-reports:      https://github.com/koalaman/shellcheck/issues
 Description:
   The goals of ShellCheck are:
@@ -83,6 +83,7 @@
       ShellCheck.Regex
     other-modules:
       Paths_ShellCheck
+    default-language: Haskell98
 
 executable shellcheck
     if impl(ghc < 8.0)
@@ -103,6 +104,7 @@
       QuickCheck >= 2.7.4,
       regex-tdfa,
       ShellCheck
+    default-language: Haskell98
     main-is: shellcheck.hs
 
 test-suite test-shellcheck
@@ -122,5 +124,6 @@
       QuickCheck >= 2.7.4,
       regex-tdfa,
       ShellCheck
+    default-language: Haskell98
     main-is: test/shellcheck.hs
 
diff --git a/shellcheck.1.md b/shellcheck.1.md
--- a/shellcheck.1.md
+++ b/shellcheck.1.md
@@ -232,7 +232,8 @@
 **disable**
 :   Disables a comma separated list of error codes for the following command.
     The command can be a simple command like `echo foo`, or a compound command
-    like a function definition, subshell block or loop.
+    like a function definition, subshell block or loop. A range can be
+    be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
 
 **enable**
 :   Enable an optional check by name, as listed with **--list-optional**.
@@ -254,7 +255,7 @@
 **shell**
 :   Overrides the shell detected from the shebang.  This is useful for
     files meant to be included (and thus lacking a shebang), or possibly
-    as a more targeted alternative to 'disable=2039'.
+    as a more targeted alternative to 'disable=SC2039'.
 
 # RC FILES
 
diff --git a/shellcheck.hs b/shellcheck.hs
--- a/shellcheck.hs
+++ b/shellcheck.hs
@@ -507,7 +507,7 @@
       where
         find filename deflt = do
             sources <- findM ((allowable inputs) `andM` doesFileExist) $
-                        (adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
+                        (adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
             case sources of
                 Nothing -> return deflt
                 Just first -> return first
diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs
--- a/src/ShellCheck/AST.hs
+++ b/src/ShellCheck/AST.hs
@@ -145,7 +145,7 @@
     deriving (Show, Eq, Functor, Foldable, Traversable)
 
 data Annotation =
-    DisableComment Integer
+    DisableComment Integer Integer -- [from, to)
     | EnableComment String
     | SourceOverride String
     | ShellOverride String
diff --git a/src/ShellCheck/ASTLib.hs b/src/ShellCheck/ASTLib.hs
--- a/src/ShellCheck/ASTLib.hs
+++ b/src/ShellCheck/ASTLib.hs
@@ -17,9 +17,11 @@
     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 -}
+{-# LANGUAGE TemplateHaskell #-}
 module ShellCheck.ASTLib where
 
 import ShellCheck.AST
+import ShellCheck.Regex
 
 import Control.Monad.Writer
 import Control.Monad
@@ -28,7 +30,13 @@
 import Data.Functor.Identity
 import Data.List
 import Data.Maybe
+import qualified Data.Map as Map
+import Numeric (showHex)
 
+import Test.QuickCheck
+
+arguments (T_SimpleCommand _ _ (cmd:args)) = args
+
 -- Is this a type of loop?
 isLoop t = case t of
         T_WhileExpression {} -> True
@@ -134,13 +142,95 @@
     str <- getLeadingUnquotedString token
     return $ "-" `isPrefixOf` str
 
--- Given a T_DollarBraced, return a simplified version of the string contents.
-bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
-bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
+-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
+--     -re -d : -u 3 bar
+-- into
+--     Just [("r", (-re, -re)), ("e", (-re, -re)), ("d", (-d,:)), ("u", (-u,3)), ("", (bar,bar))]
+--
+-- Each string flag maps to a tuple of (flag, argument), where argument=flag if it
+-- doesn't take a specific one.
+--
+-- Any unrecognized flag will result in Nothing. The exception is if arbitraryLongOpts
+-- is set, in which case --anything will map to "anything".
+getGnuOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
+getGnuOpts str args = getOpts (True, False) str [] args
 
+-- As above, except the first non-arg string will treat the rest as arguments
+getBsdOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
+getBsdOpts str args = getOpts (False, False) str [] args
+
+-- Tests for this are in Commands.hs where it's more frequently used
+getOpts ::
+    -- Behavioral config: gnu style, allow arbitrary long options
+    (Bool, Bool)
+    -- A getopts style string
+    -> String
+    -- List of long options and whether they take arguments
+    -> [(String, Bool)]
+    -- List of arguments (excluding command)
+    -> [Token]
+    -- List of flags to tuple of (optionToken, valueToken)
+    -> Maybe [(String, (Token, Token))]
+
+getOpts (gnu, arbitraryLongOpts) string longopts args = process args
+  where
+    flagList (c:':':rest) = ([c], True) : flagList rest
+    flagList (c:rest)     = ([c], False) : flagList rest
+    flagList []           = longopts
+    flagMap = Map.fromList $ ("", False) : flagList string
+
+    process [] = return []
+    process (token:rest) = do
+        case getLiteralStringDef "\0" token of
+            "--" -> return $ listToArgs rest
+            '-':'-':word -> do
+                let (name, arg) = span (/= '=') word
+                needsArg <-
+                    if arbitraryLongOpts
+                    then return $ Map.findWithDefault False name flagMap
+                    else Map.lookup name flagMap
+
+                if needsArg && null arg
+                  then
+                    case rest of
+                        (arg:rest2) -> do
+                            more <- process rest2
+                            return $ (name, (token, arg)) : more
+                        _ -> fail "Missing arg"
+                  else do
+                    more <- process rest
+                    -- Consider splitting up token to get arg
+                    return $ (name, (token, token)) : more
+            '-':opts -> shortToOpts opts token rest
+            arg ->
+                if gnu
+                then do
+                    more <- process rest
+                    return $ ("", (token, token)):more
+                else return $ listToArgs (token:rest)
+
+    shortToOpts opts token args =
+        case opts of
+            c:rest -> do
+                needsArg <- Map.lookup [c] flagMap
+                case () of
+                    _ | needsArg && null rest -> do
+                        (next:restArgs) <- return args
+                        more <- process restArgs
+                        return $ ([c], (token, next)):more
+                    _ | needsArg -> do
+                        more <- process args
+                        return $ ([c], (token, token)):more
+                    _ -> do
+                        more <- shortToOpts rest token args
+                        return $ ([c], (token, token)):more
+            [] -> process args
+
+    listToArgs = map (\x -> ("", (x, x)))
+
 -- Is this an expansion of multiple items of an array?
-isArrayExpansion t@(T_DollarBraced _ _ _) =
-    let string = bracedString t in
+isArrayExpansion (T_DollarBraced _ _ l) =
+    let string = concat $ oversimplify l in
         "@" `isPrefixOf` string ||
             not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
 isArrayExpansion _ = False
@@ -148,8 +238,8 @@
 -- Is it possible that this arg becomes multiple args?
 mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
   where
-    f t@(T_DollarBraced _ _ _) =
-        let string = bracedString t in
+    f (T_DollarBraced _ _ l) =
+        let string = concat $ oversimplify l in
             "!" `isPrefixOf` string
     f (T_DoubleQuoted _ parts) = any f parts
     f (T_NormalWord _ parts) = any f parts
@@ -193,6 +283,12 @@
     str _ = Nothing
 getUnquotedLiteral _ = Nothing
 
+isQuotes t =
+    case t of
+        T_DoubleQuoted {} -> True
+        T_SingleQuoted {} -> True
+        _ -> False
+
 -- Get the last unquoted T_Literal in a word like "${var}foo"THIS
 -- or nothing if the word does not end in an unquoted literal.
 getTrailingUnquotedLiteral :: Token -> Maybe Token
@@ -211,8 +307,11 @@
 getLeadingUnquotedString :: Token -> Maybe String
 getLeadingUnquotedString t =
     case t of
-        T_NormalWord _ ((T_Literal _ s) : _) -> return s
+        T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
         _ -> Nothing
+  where
+    from ((T_Literal _ s):rest) = s ++ from rest
+    from _ = ""
 
 -- Maybe get the literal string of this token and any globs in it.
 getGlobOrLiteralString = getLiteralStringExt f
@@ -273,7 +372,38 @@
 -- Is this token a string literal?
 isLiteral t = isJust $ getLiteralString t
 
+-- Escape user data for messages.
+-- Messages generally avoid repeating user data, but sometimes it's helpful.
+e4m = escapeForMessage
+escapeForMessage :: String -> String
+escapeForMessage str = concatMap f str
+  where
+    f '\\' = "\\\\"
+    f '\n' = "\\n"
+    f '\r' = "\\r"
+    f '\t' = "\\t"
+    f '\x1B' = "\\e"
+    f c =
+        if shouldEscape c
+        then
+            if ord c < 256
+            then "\\x" ++ (pad0 2 $ toHex c)
+            else "\\U" ++ (pad0 4 $ toHex c)
+        else [c]
 
+    shouldEscape c =
+        (not $ isPrint c)
+        || (not (isAscii c) && not (isLetter c))
+
+    pad0 :: Int -> String -> String
+    pad0 n s =
+        let l = length s in
+            if l < n
+            then (replicate (n-l) '0') ++ s
+            else s
+    toHex :: Char -> String
+    toHex c = map toUpper $ showHex (ord c) ""
+
 -- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
 getWordParts (T_NormalWord _ l)   = concatMap getWordParts l
 getWordParts (T_DoubleQuoted _ l) = l
@@ -301,7 +431,7 @@
 
 -- Maybe get the command name string of a token representing a command
 getCommandName :: Token -> Maybe String
-getCommandName = fst . getCommandNameAndToken
+getCommandName = fst . getCommandNameAndToken False
 
 -- Maybe get the name+arguments of a command.
 getCommandArgv t = do
@@ -311,20 +441,38 @@
 -- Get the command name token from a command, i.e.
 -- the token representing 'ls' in 'ls -la 2> foo'.
 -- If it can't be determined, return the original token.
-getCommandTokenOrThis = snd . getCommandNameAndToken
+getCommandTokenOrThis = snd . getCommandNameAndToken False
 
-getCommandNameAndToken :: Token -> (Maybe String, Token)
-getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
-    (T_SimpleCommand _ _ (w:rest)) <- getCommand t
-    s <- getLiteralString w
-    if "busybox" `isSuffixOf` s || "builtin" == s
-        then
-            case rest of
-                (applet:_) -> return (getLiteralString applet, applet)
-                _ -> return (Just s, w)
-        else
-            return (Just s, w)
+-- Given a command, get the string and token that represents the command name.
+-- If direct, return the actual command (e.g. exec in 'exec ls')
+-- If not, return the logical command (e.g. 'ls' in 'exec ls')
 
+getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
+getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
+    cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
+    s <- getLiteralString w
+    return $ fromMaybe (Just s, w) $ do
+        guard $ not direct
+        actual <- getEffectiveCommandToken s cmd rest
+        return (getLiteralString actual, actual)
+  where
+    getEffectiveCommandToken str cmd args =
+        let
+            firstArg = do
+                arg <- listToMaybe args
+                guard . not $ isFlag arg
+                return arg
+        in
+            case str of
+                "busybox" -> firstArg
+                "builtin" -> firstArg
+                "command" -> firstArg
+                "run" -> firstArg -- Used by bats
+                "exec" -> do
+                    opts <- getBsdOpts "cla:" args
+                    (_, (t, _)) <- find (null . fst) opts
+                    return t
+                _ -> fail ""
 
 -- If a command substitution is a single command, get its name.
 --  $(date +%s) = Just "date"
@@ -341,9 +489,9 @@
 
 -- Get the basename of a token representing a command
 getCommandBasename = fmap basename . getCommandName
-  where
-    basename = reverse . takeWhile (/= '/') . reverse
 
+basename = reverse . takeWhile (/= '/') . reverse
+
 isAssignment t =
     case t of
         T_Redirecting _ _ w -> isAssignment w
@@ -400,10 +548,10 @@
     f t@T_SimpleCommand {} = sequence_ $ do
         name <- getCommandName t
         let assocNames = ["declare","local","typeset"]
-        guard $ elem name assocNames
+        guard $ name `elem` assocNames
         let flags = getAllFlags t
-        guard $ elem "A" $ map snd flags
-        let args = map fst . filter ((==) "" . snd) $ flags
+        guard $ "A" `elem` map snd flags
+        let args = [arg | (arg, "") <- flags]
         let names = mapMaybe (getLiteralStringExt nameAssignments) args
         return $ tell names
     f _ = return ()
@@ -421,39 +569,37 @@
 
 -- Turn a word into a PG pattern, replacing all unknown/runtime values with
 -- PGMany.
-wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
-wordToPseudoGlob word =
-    simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
-  where
-    f x = case x of
-        T_Literal _ s -> return $ map PGChar s
-        T_SingleQuoted _ s -> return $ map PGChar s
-
-        T_DollarBraced {} -> return [PGMany]
-        T_DollarExpansion {} -> return [PGMany]
-        T_Backticked {} -> return [PGMany]
-
-        T_Glob _ "?" -> return [PGAny]
-        T_Glob _ ('[':_)  -> return [PGAny]
-        T_Glob {} -> return [PGMany]
-
-        T_Extglob {} -> return [PGMany]
-
-        _ -> return [PGMany]
+wordToPseudoGlob :: Token -> [PseudoGlob]
+wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
 
 -- Turn a word into a PG pattern, but only if we can preserve
 -- exact semantics.
 wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
-wordToExactPseudoGlob word =
-    simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
+wordToExactPseudoGlob = wordToPseudoGlob' True
+
+wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
+wordToPseudoGlob' exact word =
+    simplifyPseudoGlob <$> toGlob word
   where
+    toGlob :: Token -> Maybe [PseudoGlob]
+    toGlob word =
+        case word of
+            T_NormalWord _ (T_Literal _ ('~':str):rest) -> do
+                guard $ not exact
+                let this = (PGMany : (map PGChar $ dropWhile (/= '/') str))
+                tail <- concat <$> (mapM f $ concatMap getWordParts rest)
+                return $ this ++ tail
+            _ -> concat <$> (mapM f $ getWordParts word)
+
     f x = case x of
-        T_Literal _ s -> return $ map PGChar s
+        T_Literal _ s      -> return $ map PGChar s
         T_SingleQuoted _ s -> return $ map PGChar s
-        T_Glob _ "?" -> return [PGAny]
-        T_Glob _ "*" -> return [PGMany]
-        _ -> fail "Unknown token type"
+        T_Glob _ "?"       -> return [PGAny]
+        T_Glob _ "*"       -> return [PGMany]
+        T_Glob _ ('[':_) | not exact -> return [PGAny]
+        _ -> if exact then fail "" else return [PGMany]
 
+
 -- Reorder a PseudoGlob for more efficient matching, e.g.
 -- f?*?**g -> f??*g
 simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
@@ -502,8 +648,7 @@
     matchable (PGMany : rest) [] = matchable rest []
     matchable _ _ = False
 
-wordsCanBeEqual x y = fromMaybe True $
-    liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
+wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
 
 -- Is this an expansion that can be quoted,
 -- e.g. $(foo) `foo` $foo (but not {foo,})?
@@ -517,6 +662,11 @@
     T_Backticked {} -> True
     _ -> False
 
+-- Is this an expansion that results in a simple string?
+isStringExpansion t = isCommandSubstitution t || case t of
+    T_DollarArithmetic {} -> True
+    T_DollarBraced {} -> not (isArrayExpansion t)
+    _ -> False
 
 -- Is this a T_Annotation that ignores a specific code?
 isAnnotationIgnoringCode code t =
@@ -524,5 +674,45 @@
         T_Annotation _ anns _ -> any hasNum anns
         _ -> False
   where
-    hasNum (DisableComment ts) = code == ts
+    hasNum (DisableComment from to) = code >= from && code < to
     hasNum _                   = False
+
+prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
+prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
+prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
+prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
+prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
+prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
+prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
+prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
+prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
+prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
+prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
+
+-- Get the shell executable from a string like '/usr/bin/env bash'
+executableFromShebang :: String -> String
+executableFromShebang = shellFor
+  where
+    re = mkRegex "/env +(-S|--split-string=?)? *(.*)"
+    shellFor s | s `matches` re =
+        case matchRegex re s of
+            Just [flag, shell] -> fromEnvArgs (words shell)
+            _ -> ""
+    shellFor sb =
+        case words sb of
+            [] -> ""
+            [x] -> basename x
+            (first:second:args) | basename first == "busybox" ->
+                case basename second of
+                   "sh" -> "ash" -- busybox sh is ash
+                   x -> x
+            (first:args) | basename first == "env" ->
+                fromEnvArgs args
+            (first:_) -> basename first
+
+    fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args
+    basename s = reverse . takeWhile (/= '/') . reverse $ s
+    skipFlags = dropWhile ("-" `isPrefixOf`)
+
+return []
+runTests = $quickCheckAll
diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs
--- a/src/ShellCheck/Analytics.hs
+++ b/src/ShellCheck/Analytics.hs
@@ -64,6 +64,7 @@
     ,checkUncheckedCdPushdPopd
     ,checkArrayAssignmentIndices
     ,checkUseBeforeDefinition
+    ,checkAliasUsedInSameParsingUnit
     ]
 
 runAnalytics :: AnalysisSpec -> [TokenComment]
@@ -190,6 +191,12 @@
     ,checkUselessBang
     ,checkTranslatedStringVariable
     ,checkModifiedArithmeticInRedirection
+    ,checkBlatantRecursion
+    ,checkBadTestAndOr
+    ,checkAssignToSelf
+    ,checkEqualsInCommand
+    ,checkSecondArgIsComparison
+    ,checkComparisonWithLeadingX
     ]
 
 optionalChecks = map fst optionalTreeChecks
@@ -266,15 +273,23 @@
     | t `isUnqualifiedCommand` str = f cmd rest
 checkUnqualifiedCommand _ _ _ = return ()
 
+verifyCodes :: (Parameters -> Token -> Writer [TokenComment] ()) -> [Code] -> String -> Bool
+verifyCodes f l s = codes == Just l
+  where
+    treeCheck = runNodeAnalysis f
+    comments = runAndGetComments treeCheck s
+    codes = map (cCode . tcComment) <$> comments
 
 checkNode f = producesComments (runNodeAnalysis f)
 producesComments :: (Parameters -> Token -> [TokenComment]) -> String -> Maybe Bool
-producesComments f s = do
+producesComments f s = not . null <$> runAndGetComments f s
+
+runAndGetComments f s = do
         let pr = pScript s
         prRoot pr
         let spec = defaultSpec pr
         let params = makeParameters spec
-        return . not . null $
+        return $
             filterByAnnotation spec params $
                 runList spec [f]
 
@@ -353,6 +368,19 @@
         repPrecedence = depth,
         repInsertionPoint = InsertBefore
     }
+replaceToken id params r =
+    let tp = tokenPositions params
+        (start, end) = tp Map.! id
+        depth = length $ getPath (parentMap params) (T_EOF id)
+    in
+    newReplacement {
+        repStartPos = start,
+        repEndPos = end,
+        repString = r,
+        repPrecedence = depth,
+        repInsertionPoint = InsertBefore
+    }
+
 surroundWidth id params s = fixWith [replaceStart id params 0 s, replaceEnd id params 0 s]
 fixWith fixes = newFix { fixReplacements = fixes }
 
@@ -383,7 +411,7 @@
 prop_checkAssignAteCommand5 = verify checkAssignAteCommand "PAGER=cat grep bar"
 prop_checkAssignAteCommand6 = verifyNot checkAssignAteCommand "PAGER=\"cat\" grep bar"
 prop_checkAssignAteCommand7 = verify checkAssignAteCommand "here=pwd"
-checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) list) =
+checkAssignAteCommand _ (T_SimpleCommand id [T_Assignment _ _ _ _ assignmentTerm] list) =
     -- Check if first word is intended as an argument (flag or glob).
     if firstWordIsArg list
     then
@@ -415,7 +443,7 @@
 
 prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1"
 prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2"
-checkWrongArithmeticAssignment params (T_SimpleCommand id (T_Assignment _ _ _ _ val:[]) []) =
+checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ val] []) =
   sequence_ $ do
     str <- getNormalString val
     match <- matchRegex regex str
@@ -535,9 +563,14 @@
 
 prop_checkShebangParameters1 = verifyTree checkShebangParameters "#!/usr/bin/env bash -x\necho cow"
 prop_checkShebangParameters2 = verifyNotTree checkShebangParameters "#! /bin/sh  -l "
+prop_checkShebangParameters3 = verifyNotTree checkShebangParameters "#!/usr/bin/env -S bash -x\necho cow"
+prop_checkShebangParameters4 = verifyNotTree checkShebangParameters "#!/usr/bin/env --split-string bash -x\necho cow"
 checkShebangParameters p (T_Annotation _ _ t) = checkShebangParameters p t
 checkShebangParameters _ (T_Script _ (T_Literal id sb) _) =
-    [makeComment ErrorC id 2096 "On most OS, shebangs can only specify a single parameter." | length (words sb) > 2]
+    [makeComment ErrorC id 2096 "On most OS, shebangs can only specify a single parameter." | isMultiWord]
+  where
+    isMultiWord = length (words sb) > 2 && not (sb `matches` re)
+    re = mkRegex "env +(-S|--split-string)"
 
 prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho cow"
 prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh  -l "
@@ -551,6 +584,12 @@
 prop_checkShebang10= verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"
 prop_checkShebang11= verifyTree checkShebang "#!/bin/sh/\ntrue"
 prop_checkShebang12= verifyTree checkShebang "#!/bin/sh/ -xe\ntrue"
+prop_checkShebang13= verifyTree checkShebang "#!/bin/busybox sh"
+prop_checkShebang14= verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=sh\n"
+prop_checkShebang15= verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=dash\n"
+prop_checkShebang16= verifyTree checkShebang "#!/bin/busybox ash"
+prop_checkShebang17= verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=dash\n"
+prop_checkShebang18= verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=sh\n"
 checkShebang params (T_Annotation _ list t) =
     if any isOverride list then [] else checkShebang params t
   where
@@ -781,8 +820,8 @@
 prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done"
 prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"
 prop_checkDollarStar3 = verifyNot checkDollarStar "[[ $* = 'a b' ]]"
-checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _ _)])
-      | bracedString b == "*" &&
+checkDollarStar p t@(T_NormalWord _ [T_DollarBraced id _ l])
+      | concat (oversimplify l) == "*" &&
         not (isStrictlyQuoteFree (parentMap p) t) =
             warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."
 checkDollarStar _ _ = return ()
@@ -848,7 +887,8 @@
 prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
 prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
 prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\""
-prop_checkArrayWithoutIndex10= verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\""
+prop_checkArrayWithoutIndex10 = verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\""
+prop_checkArrayWithoutIndex11 = verifyNotTree checkArrayWithoutIndex "read -rpfoobar r; r=42"
 checkArrayWithoutIndex params _ =
     doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
   where
@@ -941,8 +981,10 @@
 prop_checkSingleQuotedVariables19= verifyNot checkSingleQuotedVariables "echo '```'"
 prop_checkSingleQuotedVariables20= verifyNot checkSingleQuotedVariables "mumps -run %XCMD 'W $O(^GLOBAL(5))'"
 prop_checkSingleQuotedVariables21= verifyNot checkSingleQuotedVariables "mumps -run LOOP%XCMD --xec 'W $O(^GLOBAL(6))'"
-
-
+prop_checkSingleQuotedVariables22= verifyNot checkSingleQuotedVariables "jq '$__loc__'"
+prop_checkSingleQuotedVariables23= verifyNot checkSingleQuotedVariables "command jq '$__loc__'"
+prop_checkSingleQuotedVariables24= verifyNot checkSingleQuotedVariables "exec jq '$__loc__'"
+prop_checkSingleQuotedVariables25= verifyNot checkSingleQuotedVariables "exec -c -a foo jq '$__loc__'"
 
 
 checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
@@ -973,6 +1015,7 @@
                 ,"alias"
                 ,"sudo" -- covering "sudo sh" and such
                 ,"docker" -- like above
+                ,"podman"
                 ,"dpkg-query"
                 ,"jq"  -- could also check that user provides --arg
                 ,"rename"
@@ -1174,8 +1217,8 @@
   where
     error t =
         unless (isConstantNonRe t) $
-            err (getId t) 2076
-                "Don't quote right-hand side of =~, it'll match literally rather than as a regex."
+            warn (getId t) 2076
+                "Remove quotes from right-hand side of =~ to match as a regex rather than literally."
     re = mkRegex "[][*.+()|]"
     hasMetachars s = s `matches` re
     isConstantNonRe t = fromMaybe False $ do
@@ -1208,6 +1251,9 @@
 prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"
 prop_checkConstantIfs8 = verifyNot checkConstantIfs "[[ ~foo == '~foo' ]]"
 prop_checkConstantIfs9 = verify checkConstantIfs "[[ *.png == [a-z] ]]"
+prop_checkConstantIfs10 = verifyNot checkConstantIfs "[[ ~me == ~+ ]]"
+prop_checkConstantIfs11 = verifyNot checkConstantIfs "[[ ~ == ~+ ]]"
+prop_checkConstantIfs12 = verify checkConstantIfs "[[ '~' == x ]]"
 checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
     if isConstant lhs && isConstant rhs
         then  warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
@@ -1309,8 +1355,8 @@
 prop_checkArithmeticDeref14= verifyNot checkArithmeticDeref "(( $! ))"
 prop_checkArithmeticDeref15= verifyNot checkArithmeticDeref "(( ${!var} ))"
 prop_checkArithmeticDeref16= verifyNot checkArithmeticDeref "(( ${x+1} + ${x=42} ))"
-checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _ _)]) =
-    unless (isException $ bracedString b) getWarning
+checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id _ l]) =
+    unless (isException $ concat $ oversimplify l) getWarning
   where
     isException [] = True
     isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h
@@ -1676,30 +1722,25 @@
         doList tail True
     doList' _ _ = return ()
 
-    commentIfExec (T_Pipeline id _ list) =
-      mapM_ commentIfExec $ take 1 list
-    commentIfExec (T_Redirecting _ _ f@(
-      T_SimpleCommand id _ (cmd:arg:_)))
-        | f `isUnqualifiedCommand` "exec" =
-          warn id 2093
-            "Remove \"exec \" if script should continue after this command."
+    commentIfExec (T_Pipeline id _ [c]) = commentIfExec c
+    commentIfExec (T_Redirecting _ _ (T_SimpleCommand id _ (cmd:additionalArg:_))) |
+        getLiteralString cmd == Just "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."
+            warn id 2091 "Remove surrounding $() to avoid executing output (or use eval if intentional)."
         T_Backticked id _ ->
-            warn id 2092 "Remove backticks to avoid executing output."
+            warn id 2092 "Remove backticks to avoid executing output (or use eval if intentional)."
         T_DollarArithmetic id _ ->
             err id 2084 "Remove '$' or use '_=$((expr))' to avoid executing output."
-        T_DoubleQuoted id [subword] -> check subword
         _ -> return ()
 checkSpuriousExpansion _ _ = return ()
 
@@ -1719,7 +1760,7 @@
     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."
+        warn id 2087 $ "Quote '" ++ (e4m token) ++ "' to make here document expansions happen on the server side rather than on the client."
     checkHereDoc _ = return ()
 checkSshHereDoc _ _ = return ()
 
@@ -1836,6 +1877,9 @@
 prop_checkSpacefulness38= verifyTree checkSpacefulness "a=; echo $a"
 prop_checkSpacefulness39= verifyNotTree checkSpacefulness "a=''\"\"''; b=x$a; echo $b"
 prop_checkSpacefulness40= verifyNotTree checkSpacefulness "a=$((x+1)); echo $a"
+prop_checkSpacefulness41= verifyNotTree checkSpacefulness "exec $1 --flags"
+prop_checkSpacefulness42= verifyNotTree checkSpacefulness "run $1 --flags"
+prop_checkSpacefulness43= verifyNotTree checkSpacefulness "$foo=42"
 
 data SpaceStatus = SpaceSome | SpaceNone | SpaceEmpty deriving (Eq)
 instance Semigroup SpaceStatus where
@@ -1860,27 +1904,43 @@
                 emit $ makeComment InfoC (getId token) 2223
                          "This default assignment may cause DoS due to globbing. Quote it."
             else
-                emit $ makeCommentWithFix InfoC (getId token) 2086
-                         "Double quote to prevent globbing and word splitting."
-                         (addDoubleQuotesAround params token)
+                unless (quotesMayConflictWithSC2281 params token) $
+                    emit $ makeCommentWithFix InfoC (getId token) 2086
+                             "Double quote to prevent globbing and word splitting."
+                                (addDoubleQuotesAround params token)
 
     isDefaultAssignment parents token =
         let modifier = getBracedModifier $ bracedString token in
             any (`isPrefixOf` modifier) ["=", ":="]
             && isParamTo parents ":" token
 
+    -- Given a T_DollarBraced, return a simplified version of the string contents.
+    bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
+    bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
+
 prop_checkSpacefulness4v= verifyTree checkVerboseSpacefulness "foo=3; foo=$(echo $foo)"
 prop_checkSpacefulness8v= verifyTree checkVerboseSpacefulness "a=foo\\ bar; a=foo; rm $a"
 prop_checkSpacefulness28v = verifyTree checkVerboseSpacefulness "exec {n}>&1; echo $n"
 prop_checkSpacefulness36v = verifyTree checkVerboseSpacefulness "arg=$#; echo $arg"
+prop_checkSpacefulness44v = verifyNotTree checkVerboseSpacefulness "foo=3; $foo=4"
 checkVerboseSpacefulness params = checkSpacefulness' onFind params
   where
     onFind spaces token name =
-        when (spaces == SpaceNone && name `notElem` specialVariablesWithoutSpaces) $
+        when (spaces == SpaceNone
+                && name `notElem` specialVariablesWithoutSpaces
+                && not (quotesMayConflictWithSC2281 params token)) $
             tell [makeCommentWithFix StyleC (getId token) 2248
                     "Prefer double quoting even when variables don't contain special characters."
                     (addDoubleQuotesAround params token)]
 
+-- Don't suggest quotes if this will instead be autocorrected
+-- from $foo=bar to foo=bar. This is not pretty but ok.
+quotesMayConflictWithSC2281 params t =
+    case getPath (parentMap params) t of
+        _ : T_NormalWord parentId (me:T_Literal _ ('=':_):_) : T_SimpleCommand _ _ (cmd:_) : _ ->
+            (getId t) == (getId me) && (parentId == getId cmd)
+        _ -> False
+
 addDoubleQuotesAround params token = (surroundWidth (getId token) params "\"")
 checkSpacefulness'
     :: (SpaceStatus -> Token -> String -> Writer [TokenComment] ()) ->
@@ -1940,7 +2000,7 @@
           T_DollarArithmetic _ _ -> SpaceNone
           T_Literal _ s      -> fromLiteral s
           T_SingleQuoted _ s -> fromLiteral s
-          T_DollarBraced _ _ _ -> spacefulF $ getBracedReference $ bracedString x
+          T_DollarBraced _ _ l -> spacefulF $ getBracedReference $ concat $ oversimplify l
           T_NormalWord _ w   -> isSpacefulWord spacefulF w
           T_DoubleQuoted _ w -> isSpacefulWord spacefulF w
           _ -> SpaceEmpty
@@ -1955,19 +2015,17 @@
 prop_CheckVariableBraces2 = verifyNot checkVariableBraces "a='123'; echo ${a}"
 prop_CheckVariableBraces3 = verifyNot checkVariableBraces "#shellcheck disable=SC2016\necho '$a'"
 prop_CheckVariableBraces4 = verifyNot checkVariableBraces "echo $* $1"
-checkVariableBraces params t =
-    case t of
-        T_DollarBraced id False _
-            | name `notElem` unbracedVariables ->
-                styleWithFix id 2250
-                    "Prefer putting braces around variable references even when not strictly required."
-                    (fixFor t)
-
-        _ -> return ()
+prop_CheckVariableBraces5 = verifyNot checkVariableBraces "$foo=42"
+checkVariableBraces params t@(T_DollarBraced id False l)
+    | name `notElem` unbracedVariables && not (quotesMayConflictWithSC2281 params t) =
+        styleWithFix id 2250
+            "Prefer putting braces around variable references even when not strictly required."
+            (fixFor t)
   where
-    name = getBracedReference $ bracedString t
+    name = getBracedReference $ concat $ oversimplify l
     fixFor token = fixWith [replaceStart (getId token) params 1 "${"
                            ,replaceEnd (getId token) params 0 "}"]
+checkVariableBraces _ _ = return ()
 
 prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
 prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
@@ -2013,7 +2071,7 @@
 
     squashesQuotes t =
         case t of
-            T_DollarBraced id _ _ -> "#" `isPrefixOf` bracedString t
+            T_DollarBraced id _ l -> "#" `isPrefixOf` concat (oversimplify l)
             _ -> False
 
     readF _ expr name = do
@@ -2054,18 +2112,27 @@
   verifyNotTree checkFunctionsUsedExternally "foo() { :; }; ssh host echo foo"
 prop_checkFunctionsUsedExternally7 =
   verifyNotTree checkFunctionsUsedExternally "install() { :; }; sudo apt-get install foo"
+prop_checkFunctionsUsedExternally8 =
+  verifyTree checkFunctionsUsedExternally "foo() { :; }; command sudo foo"
+prop_checkFunctionsUsedExternally9 =
+  verifyTree checkFunctionsUsedExternally "foo() { :; }; exec -c sudo foo"
 checkFunctionsUsedExternally params t =
     runNodeAnalysis checkCommand params t
   where
-    checkCommand _ t@(T_SimpleCommand _ _ (cmd:args)) =
-        case getCommandBasename t of
-            Just name -> do
+    checkCommand _ t@(T_SimpleCommand _ _ argv) =
+        case getCommandNameAndToken False t of
+            (Just str, t) -> do
+                let name = basename str
+                let args = skipOver t argv
                 let argStrings = map (\x -> (fromMaybe "" $ getLiteralString x, x)) args
                 let candidates = getPotentialCommands name argStrings
                 mapM_ (checkArg name) candidates
             _ -> return ()
     checkCommand _ _ = return ()
 
+    skipOver t list = drop 1 $ dropWhile (\c -> getId c /= id) $ list
+      where id = getId t
+
     -- Try to pick out the argument[s] that may be commands
     getPotentialCommands name argAndString =
         case name of
@@ -2121,6 +2188,7 @@
 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_checkUnused16b= verifyNotTree checkUnusedAssignments "f() { local -x foo; foo=42; bar; }; f"
 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"
@@ -2214,6 +2282,15 @@
 prop_checkUnassignedReferences38= verifyTree (checkUnassignedReferences' True) "echo $VAR"
 prop_checkUnassignedReferences39= verifyNotTree checkUnassignedReferences "builtin export var=4; echo $var"
 prop_checkUnassignedReferences40= verifyNotTree checkUnassignedReferences ": ${foo=bar}"
+prop_checkUnassignedReferences41= verifyNotTree checkUnassignedReferences "mapfile -t files 123; echo \"${files[@]}\""
+prop_checkUnassignedReferences42= verifyNotTree checkUnassignedReferences "mapfile files -t; echo \"${files[@]}\""
+prop_checkUnassignedReferences43= verifyNotTree checkUnassignedReferences "mapfile --future files; echo \"${files[@]}\""
+prop_checkUnassignedReferences_minusNPlain   = verifyNotTree checkUnassignedReferences "if [ -n \"$x\" ]; then echo $x; fi"
+prop_checkUnassignedReferences_minusZPlain   = verifyNotTree checkUnassignedReferences "if [ -z \"$x\" ]; then echo \"\"; fi"
+prop_checkUnassignedReferences_minusNBraced  = verifyNotTree checkUnassignedReferences "if [ -n \"${x}\" ]; then echo $x; fi"
+prop_checkUnassignedReferences_minusZBraced  = verifyNotTree checkUnassignedReferences "if [ -z \"${x}\" ]; then echo \"\"; fi"
+prop_checkUnassignedReferences_minusNDefault = verifyNotTree checkUnassignedReferences "if [ -n \"${x:-}\" ]; then echo $x; fi"
+prop_checkUnassignedReferences_minusZDefault = verifyNotTree checkUnassignedReferences "if [ -z \"${x:-}\" ]; then echo \"\"; fi"
 
 checkUnassignedReferences = checkUnassignedReferences' False
 checkUnassignedReferences' includeGlobals params t = warnings
@@ -2274,7 +2351,7 @@
     isInArray var t = any isArray $ getPath (parentMap params) t
       where
         isArray T_Array {} = True
-        isArray b@(T_DollarBraced _ _ _) | var /= getBracedReference (bracedString b) = True
+        isArray (T_DollarBraced _ _ l) | var /= getBracedReference (concat $ oversimplify l) = True
         isArray _ = False
 
     isGuarded (T_DollarBraced _ _ v) =
@@ -2296,8 +2373,11 @@
 prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*"
 prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"
 prop_checkGlobsAsOptions4 = verifyNot checkGlobsAsOptions "*.txt"
-checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =
-    mapM_ check $ takeWhile (not . isEndOfArgs) (drop 1 args)
+prop_checkGlobsAsOptions5 = verifyNot checkGlobsAsOptions "echo 'Files:' *.txt"
+prop_checkGlobsAsOptions6 = verifyNot checkGlobsAsOptions "printf '%s\\n' *"
+checkGlobsAsOptions _ cmd@(T_SimpleCommand _ _ args) =
+    unless ((fromMaybe "" $ getCommandBasename cmd) `elem` ["echo", "printf"]) $
+        mapM_ check $ takeWhile (not . isEndOfArgs) (drop 1 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."
@@ -2327,6 +2407,7 @@
 prop_checkWhileReadPitfalls12 = verifyNot checkWhileReadPitfalls "while read foo\ndo\nmplayer foo.ogv << EOF\nq\nEOF\ndone"
 prop_checkWhileReadPitfalls13 = verify checkWhileReadPitfalls "while read foo; do x=$(ssh host cmd); done"
 prop_checkWhileReadPitfalls14 = verify checkWhileReadPitfalls "while read foo; do echo $(ssh host cmd) < /dev/null; done"
+prop_checkWhileReadPitfalls15 = verify checkWhileReadPitfalls "while read foo; do ssh $foo cmd & done"
 
 checkWhileReadPitfalls params (T_WhileExpression id [command] contents)
         | isStdinReadCommand command =
@@ -2377,6 +2458,7 @@
                     warnWithFix (getId cmd) 2095
                         ("Use " ++ name ++ " " ++ flag ++ " to prevent " ++ name ++ " from swallowing stdin.")
                         (fix flag cmd)
+    checkMuncher (T_Backgrounded _ t) = checkMuncher t
     checkMuncher _ = return ()
 
     stdinRedirect (T_FdRedirect _ fd op)
@@ -2402,7 +2484,7 @@
 checkPrefixAssignmentReference params t@(T_DollarBraced id _ value) =
     check path
   where
-    name = getBracedReference $ bracedString t
+    name = getBracedReference $ concat $ oversimplify value
     path = getPath (parentMap params) t
     idPath = map getId path
 
@@ -2437,7 +2519,7 @@
   where
     isCharClass str = "[" `isPrefixOf` str && "]" `isSuffixOf` str
     contents = dropNegation . drop 1 . take (length str - 1) $ str
-    hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents
+    hasDupes = any ((>1) . length) . group . sort . filter (/= '-') $ contents
     dropNegation s =
         case s of
             '!':rest -> rest
@@ -2554,6 +2636,7 @@
 prop_checkUnpassedInFunctions11= verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;"
 prop_checkUnpassedInFunctions12= verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;"
 prop_checkUnpassedInFunctions13= verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n"
+prop_checkUnpassedInFunctions14= verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo"
 checkUnpassedInFunctions params root =
     execWriter $ mapM_ warnForGroup referenceGroups
   where
@@ -2595,7 +2678,7 @@
         return $ tell [(str, null args, t)]
     checkCommand _ = Nothing
 
-    isPositional str = str == "*" || str == "@"
+    isPositional str = str == "*" || str == "@" || str == "#"
         || (all isDigit str && str /= "0" && str /= "")
 
     isArgumentless (_, b, _) = b
@@ -2612,7 +2695,7 @@
 
     suggestParams (name, _, thing) =
         info (getId thing) 2119 $
-            "Use " ++ name ++ " \"$@\" if function's $1 should mean script's $1."
+            "Use " ++ (e4m name) ++ " \"$@\" if function's $1 should mean script's $1."
     warnForDeclaration func name =
         warn (getId func) 2120 $
             name ++ " references arguments, but none are ever passed."
@@ -2799,7 +2882,7 @@
                 then
                     -- Ksh appears to stop processing after unrecognized tokens, so operators
                     -- will effectively work with globs, but only the first match.
-                    when (op `elem` ['-':c:[] | c <- "bcdfgkprsuwxLhNOGRS" ]) $
+                    when (op `elem` [['-', c] | c <- "bcdfgkprsuwxLhNOGRS" ]) $
                         warn (getId token) 2245 $
                             op ++ " only applies to the first expansion of this glob. Use a loop to check any/all."
                 else
@@ -2867,11 +2950,14 @@
 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); }"
+prop_checkMaskedReturns4 = verify checkMaskedReturns "readonly a=$(false)"
+prop_checkMaskedReturns5 = verify checkMaskedReturns "readonly a=\"`false`\""
+prop_checkMaskedReturns6 = verifyNot checkMaskedReturns "declare a; a=$(false)"
+prop_checkMaskedReturns7 = verifyNot checkMaskedReturns "f() { local -r a=$(false); }"
+prop_checkMaskedReturns8 = verifyNot checkMaskedReturns "a=$(false); readonly a"
 checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = sequence_ $ do
     name <- getCommandName t
-    guard $ name `elem` ["declare", "export"]
+    guard $ name `elem` ["declare", "export", "readonly"]
         || name == "local" && "r" `notElem` map snd (getAllFlags t)
     return $ mapM_ checkArgs rest
   where
@@ -2898,8 +2984,8 @@
   where
     flags = getAllFlags t
     has_t0 = Just "0" == do
-        parsed <- getOpts flagsForRead flags
-        t <- lookup "t" parsed
+        parsed <- getGnuOpts flagsForRead $ arguments t
+        (_, t) <- lookup "t" parsed
         getLiteralString t
 
 checkReadWithoutR _ _ = return ()
@@ -3035,7 +3121,7 @@
     isZero t = getLiteralString t == Just "0"
     isExitCode t =
         case getWordParts t of
-            [exp@T_DollarBraced {}] -> bracedString exp == "?"
+            [T_DollarBraced _ _ l] -> concat (oversimplify l) == "?"
             _ -> False
     message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?."
 
@@ -3137,9 +3223,7 @@
             if isConstant word
                 then warn (getId word) 2194
                         "This word is constant. Did you forget the $ on a variable?"
-                else  sequence_ $ do
-                    pg <- wordToPseudoGlob word
-                    return $ mapM_ (check pg) allpatterns
+                else mapM_ (check $ wordToPseudoGlob word) allpatterns
 
             let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns
             let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns
@@ -3152,15 +3236,13 @@
     fst3 (x,_,_) = x
     snd3 (_,x,_) = x
     tp = tokenPositions params
-    check target candidate = sequence_ $ do
-        candidateGlob <- wordToPseudoGlob candidate
-        guard . not $ pseudoGlobsCanOverlap target candidateGlob
-        return $ warn (getId candidate) 2195
-                    "This pattern will never match the case statement's word. Double check them."
+    check target candidate = unless (pseudoGlobsCanOverlap target $ wordToPseudoGlob candidate) $
+        warn (getId candidate) 2195
+            "This pattern will never match the case statement's word. Double check them."
 
     tupMap f l = map (\x -> (x, f x)) l
     checkDoms ((glob, Just x), rest) =
-        forM_ (find (\(_, p) -> x `pseudoGlobIsSuperSetof` p) valids) $
+        forM_ (find (\(_, p) -> x `pseudoGlobIsSuperSetof` p) rest) $
             \(first,_) -> do
                 warn (getId glob) 2221 $ "This pattern always overrides a later one" <> patternContext (getId first)
                 warn (getId first) 2222 $ "This pattern never matches because of a previous pattern" <> patternContext (getId glob)
@@ -3170,8 +3252,6 @@
             case posLine . fst <$> Map.lookup id tp of
               Just l -> " on line " <> show l <> "."
               _      -> "."
-
-        valids = [(x,y) | (x, Just y) <- rest]
     checkDoms _ = return ()
 
 
@@ -3226,7 +3306,7 @@
         T_DollarBraced id _ str |
             not (isCountingReference part)
             && not (isQuotedAlternativeReference part)
-            && getBracedReference (bracedString part) `notElem` variablesWithoutSpaces
+            && getBracedReference (concat $ oversimplify str) `notElem` variablesWithoutSpaces
             -> warn id 2206 $
                 if shellType params == Ksh
                 then "Quote to prevent word splitting/globbing, or split robustly with read -A or while read."
@@ -3287,26 +3367,101 @@
 prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
 prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
 prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin"
+prop_checkPipeToNowhere10 = verify checkPipeToNowhere "ls > file | grep foo"
+prop_checkPipeToNowhere11 = verify checkPipeToNowhere "ls | grep foo < file"
+prop_checkPipeToNowhere12 = verify checkPipeToNowhere "ls > foo > bar"
+prop_checkPipeToNowhere13 = verify checkPipeToNowhere "ls > foo 2> bar > baz"
+prop_checkPipeToNowhere14 = verify checkPipeToNowhere "ls > foo &> bar"
+prop_checkPipeToNowhere15 = verifyNot checkPipeToNowhere "ls > foo 2> bar |& grep 'No space left'"
+prop_checkPipeToNowhere16 = verifyNot checkPipeToNowhere "echo World | cat << EOF\nhello $(cat)\nEOF\n"
+prop_checkPipeToNowhere17 = verify checkPipeToNowhere "echo World | cat << 'EOF'\nhello $(cat)\nEOF\n"
+prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l"
+prop_checkPipeToNowhere19 = verifyNot checkPipeToNowhere "find . -print0 | du --files0-from=/dev/stdin"
+prop_checkPipeToNowhere20 = verifyNot checkPipeToNowhere "find . | du --exclude-from=/dev/fd/0"
+
+data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq)
 checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
-checkPipeToNowhere _ t =
+checkPipeToNowhere params t =
     case t of
-        T_Pipeline _ _ (first:rest) -> mapM_ checkPipe rest
+        T_Pipeline _ pipes cmds ->
+            mapM_ checkPipe $ commandsWithContext pipes cmds
         T_Redirecting _ redirects cmd | any redirectsStdin redirects -> checkRedir cmd
         _ -> return ()
   where
-    checkPipe redir = sequence_ $ do
-        cmd <- getCommand redir
-        name <- getCommandBasename cmd
-        guard $ name `elem` nonReadingCommands
-        guard . not $ hasAdditionalConsumers cmd
-        -- Confusing echo for cat is so common that it's worth a special case
-        let suggestion =
-                if name == "echo"
-                then "Did you want 'cat' instead?"
-                else "Wrong command or missing xargs?"
-        return $ warn (getId cmd) 2216 $
-            "Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
+    checkPipe (input, stage, output) = do
+        let hasConsumers = hasAdditionalConsumers stage
+        let hasProducers = hasAdditionalProducers stage
 
+        sequence_ $ do
+            cmd <- getCommand stage
+            name <- getCommandBasename cmd
+            guard $ name `elem` nonReadingCommands
+            guard $ not hasConsumers && input /= NoPipe
+            guard . not $ commandSpecificException name cmd
+
+            -- Confusing echo for cat is so common that it's worth a special case
+            let suggestion =
+                    if name == "echo"
+                    then "Did you want 'cat' instead?"
+                    else "Wrong command or missing xargs?"
+            return $ warn (getId cmd) 2216 $
+                "Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
+
+        sequence_ $ do
+            T_Redirecting _ redirs cmd <- return stage
+            fds <- mapM getRedirectionFds redirs
+
+            let fdAndToken :: [(Integer, Token)]
+                fdAndToken =
+                  concatMap (\(list, redir) -> map (\n -> (n, redir)) list) $
+                    zip fds redirs
+
+            let fdMap =
+                  Map.fromListWith (++) $
+                    map (\(a,b) -> (a,[b])) fdAndToken
+
+            let inputWarning = sequence_ $ do
+                    guard $ input /= NoPipe && not hasConsumers
+                    (override:_) <- Map.lookup 0 fdMap
+                    return $ err (getOpId override) 2259 $
+                        "This redirection overrides piped input. To use both, merge or pass filenames."
+
+            -- Only produce output warnings for regular pipes, since these are
+            -- way more common, and  `foo > out 2> err |& foo` can still write
+            -- to stderr if the files fail to open
+            let outputWarning = sequence_ $ do
+                    guard $ output == StdoutPipe && not hasProducers
+                    (override:_) <- Map.lookup 1 fdMap
+                    return $ err (getOpId override) 2260 $
+                        "This redirection overrides the output pipe. Use 'tee' to output to both."
+
+            return $ do
+                inputWarning
+                outputWarning
+                mapM_ warnAboutDupes $ Map.assocs fdMap
+
+    commandSpecificException name cmd =
+        case name of
+            "du" -> any ((`elem` ["exclude-from", "files0-from"]) . snd) $ getAllFlags cmd
+            _ -> False
+
+    warnAboutDupes (n, list@(_:_:_)) =
+        forM_ list $ \c -> err (getOpId c) 2261 $
+            "Multiple redirections compete for " ++ str n ++ ". Use cat, tee, or pass filenames instead."
+    warnAboutDupes _ = return ()
+
+    alternative =
+        if shellType params `elem` [Bash, Ksh]
+        then "process substitutions or temp files"
+        else "temporary files"
+
+    str n =
+        case n of
+            0 -> "stdin"
+            1 -> "stdout"
+            2 -> "stderr"
+            _ -> "FD " ++ show n
+
     checkRedir cmd = sequence_ $ do
         name <- getCommandBasename cmd
         guard $ name `elem` nonReadingCommands
@@ -3320,23 +3475,74 @@
             "Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
 
     -- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
-    hasAdditionalConsumers t = isNothing $
-        doAnalysis (guard . not . mayConsume) t
+    hasAdditionalConsumers = treeContains mayConsume
+    -- Could any words in a SimpleCommand produce stdout? E.g. >(tee foo)
+    hasAdditionalProducers = treeContains mayProduce
+    treeContains pred t = isNothing $
+        doAnalysis (guard . not . pred) t
 
     mayConsume t =
         case t of
-            T_ProcSub {} -> True
+            T_ProcSub _ "<" _ -> True
             T_Backticked {} -> True
             T_DollarExpansion {} -> True
             _ -> False
 
-    redirectsStdin t =
+    mayProduce t =
         case t of
-            T_FdRedirect _ _ (T_IoFile _ T_Less {} _) -> True
-            T_FdRedirect _ _ T_HereDoc {} -> True
-            T_FdRedirect _ _ T_HereString {} -> True
+            T_ProcSub _ ">" _ -> True
             _ -> False
 
+    getOpId t =
+        case t of
+            T_FdRedirect _ _ x -> getOpId x
+            T_IoFile _ op _ -> getId op
+            _ -> getId t
+
+    getRedirectionFds t =
+        case t of
+            T_FdRedirect _ "" x -> getDefaultFds x
+            T_FdRedirect _ "&" _ -> return [1, 2]
+            T_FdRedirect _ num x | all isDigit num ->
+                -- Don't report the number unless we know what it is.
+                -- This avoids triggering on 3>&1 1>&3
+                getDefaultFds x *> return [read num]
+            -- Don't bother with {fd}>42 and such
+            _ -> Nothing
+
+    getDefaultFds redir =
+        case redir of
+            T_HereDoc {} -> return [0]
+            T_HereString {} -> return [0]
+            T_IoFile _ op _ ->
+                case op of
+                    T_Less {} -> return [0]
+                    T_Greater {} -> return [1]
+                    T_DGREAT {} -> return [1]
+                    T_GREATAND {} -> return [1, 2]
+                    T_CLOBBER {} -> return [1]
+                    T_IoDuplicate _ op "-" -> getDefaultFds op
+                    _ -> Nothing
+            _ -> Nothing
+
+    redirectsStdin t =
+        fromMaybe False $ do
+            fds <- getRedirectionFds t
+            return $ 0 `elem` fds
+
+    pipeType t =
+        case t of
+            T_Pipe _ "|" -> StdoutPipe
+            T_Pipe _ "|&" -> StdoutStderrPipe
+            _ -> NoPipe
+
+    commandsWithContext pipes cmds =
+        let pipeTypes = map pipeType pipes
+            inputs = NoPipe : pipeTypes
+            outputs = pipeTypes ++ [NoPipe]
+        in
+            zip3 inputs cmds outputs
+
 prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { true; }"
 prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f"
 prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi"
@@ -3596,7 +3802,8 @@
 prop_checkModifiedArithmeticInRedirection3 = verifyNot checkModifiedArithmeticInRedirection "while true; do true; done > $((i++))"
 prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))"
 prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n"
-checkModifiedArithmeticInRedirection _ t =
+prop_checkModifiedArithmeticInRedirection6 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/dash\nls > $((i=i+1))"
+checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash) $
     case t of
         T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs
         _ -> return ()
@@ -3625,6 +3832,427 @@
     warnFor id =
         warn id 2257 "Arithmetic modifications in command redirections may be discarded. Do them separately."
 
+prop_checkAliasUsedInSameParsingUnit1 = verifyTree checkAliasUsedInSameParsingUnit "alias x=y; x"
+prop_checkAliasUsedInSameParsingUnit2 = verifyNotTree checkAliasUsedInSameParsingUnit "alias x=y\nx"
+prop_checkAliasUsedInSameParsingUnit3 = verifyTree checkAliasUsedInSameParsingUnit "{ alias x=y\nx\n}"
+prop_checkAliasUsedInSameParsingUnit4 = verifyNotTree checkAliasUsedInSameParsingUnit "alias x=y; 'x';"
+prop_checkAliasUsedInSameParsingUnit5 = verifyNotTree checkAliasUsedInSameParsingUnit ":\n{\n#shellcheck disable=SC2262\nalias x=y\nx\n}"
+prop_checkAliasUsedInSameParsingUnit6 = verifyNotTree checkAliasUsedInSameParsingUnit ":\n{\n#shellcheck disable=SC2262\nalias x=y\nalias x=z\nx\n}" -- Only consider the first instance
+checkAliasUsedInSameParsingUnit :: Parameters -> Token -> [TokenComment]
+checkAliasUsedInSameParsingUnit params root =
+    let
+        -- Get all root commands
+        commands = concat $ getCommandSequences root
+        -- Group them by whether they start on the same line where the previous one ended
+        units = groupByLink followsOnLine commands
+    in
+        execWriter $ mapM_ checkUnit units
+  where
+    lineSpan t =
+        let m = tokenPositions params in do
+            (start, end) <- Map.lookup t m
+            return $ (posLine start, posLine end)
+
+    followsOnLine a b = fromMaybe False $ do
+        (_, end) <- lineSpan (getId a)
+        (start, _) <- lineSpan (getId b)
+        return $ end == start
+
+    checkUnit :: [Token] -> Writer [TokenComment] ()
+    checkUnit unit = evalStateT (mapM_ (doAnalysis findCommands) unit) (Map.empty)
+
+    isSourced t =
+        let
+            f (T_SourceCommand {}) = True
+            f _ = False
+        in
+            any f $ getPath (parentMap params) t
+
+    findCommands :: Token -> StateT (Map.Map String Token) (Writer [TokenComment]) ()
+    findCommands t = case t of
+            T_SimpleCommand _ _ (cmd:args) ->
+                case getUnquotedLiteral cmd of
+                    Just "alias" ->
+                        mapM_ addAlias args
+                    Just name | '/' `notElem` name -> do
+                        cmd <- gets (Map.lookup name)
+                        case cmd of
+                            Just alias ->
+                                unless (isSourced t || shouldIgnoreCode params 2262 alias) $ do
+                                    warn (getId alias) 2262 "This alias can't be defined and used in the same parsing unit. Use a function instead."
+                                    info (getId t) 2263 "Since they're in the same parsing unit, this command will not refer to the previously mentioned alias."
+                            _ -> return ()
+                    _ -> return ()
+            _ -> return ()
+    addAlias arg = do
+        let (name, value) = break (== '=') $ getLiteralStringDef "-" arg
+        when (isVariableName name && not (null value)) $
+            modify (Map.insertWith (\new old -> old) name arg)
+
+-- Like groupBy, but compares pairs of adjacent elements, rather than against the first of the span
+prop_groupByLink1 = groupByLink (\a b -> a+1 == b) [1,2,3,2,3,7,8,9] == [[1,2,3], [2,3], [7,8,9]]
+prop_groupByLink2 = groupByLink (==) ([] :: [()]) == []
+groupByLink :: (a -> a -> Bool) -> [a] -> [[a]]
+groupByLink f list =
+    case list of
+        [] -> []
+        (x:xs) -> foldr c n xs x []
+  where
+    c next rest current span =
+        if f current next
+        then rest next (current:span)
+        else (reverse $ current:span) : rest next []
+    n current span = [reverse (current:span)]
+
+
+prop_checkBlatantRecursion1 = verify checkBlatantRecursion ":(){ :|:& };:"
+prop_checkBlatantRecursion2 = verify checkBlatantRecursion "f() { f; }"
+prop_checkBlatantRecursion3 = verifyNot checkBlatantRecursion "f() { command f; }"
+prop_checkBlatantRecursion4 = verify checkBlatantRecursion "cd() { cd \"$lol/$1\" || exit; }"
+prop_checkBlatantRecursion5 = verifyNot checkBlatantRecursion "cd() { [ -z \"$1\" ] || cd \"$1\"; }"
+prop_checkBlatantRecursion6 = verifyNot checkBlatantRecursion "cd() { something; cd $1; }"
+prop_checkBlatantRecursion7 = verifyNot checkBlatantRecursion "cd() { builtin cd $1; }"
+checkBlatantRecursion :: Parameters -> Token -> Writer [TokenComment] ()
+checkBlatantRecursion params t =
+    case t of
+        T_Function _ _ _ name body ->
+            case getCommandSequences body of
+                    [first : _] -> checkList name first
+                    _ -> return ()
+        _ -> return ()
+  where
+    checkList :: String -> Token -> Writer [TokenComment] ()
+    checkList name t =
+        case t of
+            T_Backgrounded _ t -> checkList name t
+            T_AndIf _ lhs _ -> checkList name lhs
+            T_OrIf _ lhs _ -> checkList name lhs
+            T_Pipeline _ _ cmds -> mapM_ (checkCommand name) cmds
+            _ -> return ()
+
+    checkCommand :: String -> Token -> Writer [TokenComment] ()
+    checkCommand name cmd = sequence_ $ do
+        let (invokedM, t) = getCommandNameAndToken True cmd
+        invoked <- invokedM
+        guard $ name == invoked
+        return $
+            errWithFix (getId t) 2264
+                ("This function unconditionally re-invokes itself. Missing 'command'?")
+                (fixWith [replaceStart (getId t) params 0 $ "command "])
+
+
+prop_checkBadTestAndOr1 = verify checkBadTestAndOr "[ x ] & [ y ]"
+prop_checkBadTestAndOr2 = verify checkBadTestAndOr "test -e foo & [ y ]"
+prop_checkBadTestAndOr3 = verify checkBadTestAndOr "[ x ] | [ y ]"
+checkBadTestAndOr params t =
+    case t of
+        T_Pipeline _ seps cmds@(_:_:_) -> checkOrs seps cmds
+        T_Backgrounded id cmd -> checkAnds id cmd
+        _ -> return ()
+  where
+    checkOrs seps cmds =
+        let maybeSeps = map Just seps
+            commandWithSeps = zip3 (Nothing:maybeSeps) cmds (maybeSeps ++ [Nothing])
+        in
+            mapM_ checkTest commandWithSeps
+    checkTest (before, cmd, after) =
+        when (isTest cmd) $ do
+            checkPipe before
+            checkPipe after
+
+    checkPipe t =
+        case t of
+            Just (T_Pipe id "|") ->
+                warnWithFix id 2266 "Use || for logical OR. Single | will pipe." $
+                    fixWith [replaceEnd id params 0 "|"]
+            _ -> return ()
+
+    checkAnds id t =
+        case t of
+            T_AndIf _ _ rhs -> checkAnds id rhs
+            T_OrIf _ _ rhs -> checkAnds id rhs
+            T_Pipeline _ _ list | not (null list) -> checkAnds id (last list)
+            cmd -> when (isTest cmd) $
+                errWithFix id 2265 "Use && for logical AND. Single & will background and return true." $
+                    (fixWith [replaceEnd id params 0 "&"])
+
+    isTest t =
+        case t of
+            T_Condition {} -> True
+            T_SimpleCommand {} -> t `isCommand` "test"
+            T_Redirecting _ _ t -> isTest t
+            T_Annotation _ _ t -> isTest t
+            _ -> False
+
+prop_checkComparisonWithLeadingX1 = verify checkComparisonWithLeadingX "[ x$foo = xlol ]"
+prop_checkComparisonWithLeadingX2 = verify checkComparisonWithLeadingX "test x$foo = xlol"
+prop_checkComparisonWithLeadingX3 = verifyNot checkComparisonWithLeadingX "[ $foo = xbar ]"
+prop_checkComparisonWithLeadingX4 = verifyNot checkComparisonWithLeadingX "test $foo = xbar"
+prop_checkComparisonWithLeadingX5 = verify checkComparisonWithLeadingX "[ \"x$foo\" = 'xlol' ]"
+prop_checkComparisonWithLeadingX6 = verify checkComparisonWithLeadingX "[ x\"$foo\" = x'lol' ]"
+checkComparisonWithLeadingX params t =
+    case t of
+        TC_Binary id typ op lhs rhs | op == "=" || op == "==" ->
+            check lhs rhs
+        T_SimpleCommand _ _ [cmd, lhs, op, rhs] |
+            getLiteralString cmd == Just "test" &&
+                getLiteralString op `elem` [Just "=", Just "=="] ->
+                    check lhs rhs
+        _ -> return ()
+  where
+    msg = "Avoid x-prefix in comparisons as it no longer serves a purpose."
+    check lhs rhs = sequence_ $ do
+        l <- fixLeadingX lhs
+        r <- fixLeadingX rhs
+        return $ styleWithFix (getId lhs) 2268 msg $ fixWith [l, r]
+
+    fixLeadingX token =
+         case getWordParts token of
+            T_Literal id ('x':_):_ ->
+                case token of
+                    -- The side is a single, unquoted x, so we have to quote
+                    T_NormalWord _ [T_Literal id "x"] ->
+                        return $ replaceStart id params 1 "\"\""
+                    -- Otherwise we can just delete it
+                    _ -> return $ replaceStart id params 1 ""
+            T_SingleQuoted id ('x':_):_ ->
+                -- Replace the single quote and x
+                return $ replaceStart id params 2 "'"
+            _ -> Nothing
+
+prop_checkAssignToSelf1 = verify checkAssignToSelf "x=$x"
+prop_checkAssignToSelf2 = verify checkAssignToSelf "x=${x}"
+prop_checkAssignToSelf3 = verify checkAssignToSelf "x=\"$x\""
+prop_checkAssignToSelf4 = verifyNot checkAssignToSelf "x=$x mycmd"
+checkAssignToSelf _ t =
+    case t of
+        T_SimpleCommand _ vars [] -> mapM_ check vars
+        _ -> return ()
+  where
+    check t =
+        case t of
+            T_Assignment id Assign name [] t ->
+                case getWordParts t of
+                    [T_DollarBraced _ _ b] -> do
+                        when (Just name == getLiteralString b) $
+                            msg id
+                    _ -> return ()
+            _ -> return ()
+    msg id = info id 2269 "This variable is assigned to itself, so the assignment does nothing."
+
+
+prop_checkEqualsInCommand1a = verifyCodes checkEqualsInCommand [2277] "#!/bin/bash\n0='foo'"
+prop_checkEqualsInCommand2a = verifyCodes checkEqualsInCommand [2278] "#!/bin/ksh \n$0='foo'"
+prop_checkEqualsInCommand3a = verifyCodes checkEqualsInCommand [2279] "#!/bin/dash\n${0}='foo'"
+prop_checkEqualsInCommand4a = verifyCodes checkEqualsInCommand [2280] "#!/bin/sh  \n0='foo'"
+
+prop_checkEqualsInCommand1b = verifyCodes checkEqualsInCommand [2270] "1='foo'"
+prop_checkEqualsInCommand2b = verifyCodes checkEqualsInCommand [2270] "${2}='foo'"
+
+prop_checkEqualsInCommand1c = verifyCodes checkEqualsInCommand [2271] "var$((n+1))=value"
+prop_checkEqualsInCommand2c = verifyCodes checkEqualsInCommand [2271] "var${x}=value"
+prop_checkEqualsInCommand3c = verifyCodes checkEqualsInCommand [2271] "var$((cmd))x='foo'"
+prop_checkEqualsInCommand4c = verifyCodes checkEqualsInCommand [2271] "$(cmd)='foo'"
+
+prop_checkEqualsInCommand1d = verifyCodes checkEqualsInCommand [2273] "======="
+prop_checkEqualsInCommand2d = verifyCodes checkEqualsInCommand [2274] "======= Here ======="
+prop_checkEqualsInCommand3d = verifyCodes checkEqualsInCommand [2275] "foo\n=42"
+
+prop_checkEqualsInCommand1e = verifyCodes checkEqualsInCommand [] "--foo=bar"
+prop_checkEqualsInCommand2e = verifyCodes checkEqualsInCommand [] "$(cmd)'=foo'"
+prop_checkEqualsInCommand3e = verifyCodes checkEqualsInCommand [2276] "var${x}/=value"
+prop_checkEqualsInCommand4e = verifyCodes checkEqualsInCommand [2276] "${}=value"
+prop_checkEqualsInCommand5e = verifyCodes checkEqualsInCommand [2276] "${#x}=value"
+
+prop_checkEqualsInCommand1f = verifyCodes checkEqualsInCommand [2281] "$var=foo"
+prop_checkEqualsInCommand2f = verifyCodes checkEqualsInCommand [2281] "$a=$b"
+prop_checkEqualsInCommand3f = verifyCodes checkEqualsInCommand [2281] "${var}=foo"
+prop_checkEqualsInCommand4f = verifyCodes checkEqualsInCommand [2281] "${var[42]}=foo"
+prop_checkEqualsInCommand5f = verifyCodes checkEqualsInCommand [2281] "$var+=foo"
+
+prop_checkEqualsInCommand1g = verifyCodes checkEqualsInCommand [2282] "411toppm=true"
+
+checkEqualsInCommand params originalToken =
+    case originalToken of
+        T_SimpleCommand _ _ (word:_) -> check word
+        _ -> return ()
+  where
+    hasEquals t =
+        case t of
+            T_Literal _ s -> '=' `elem` s
+            _ -> False
+
+    check t@(T_NormalWord _ list) | any hasEquals list =
+        case break hasEquals list of
+            (leading, (eq:_)) -> msg t (stripSinglePlus leading) eq
+            _ -> return ()
+    check _ = return ()
+
+    -- This is a workaround for the parser adding + and = as separate literals
+    stripSinglePlus l =
+        case reverse l of
+            (T_Literal _ "+"):rest -> reverse rest
+            _ -> l
+
+    positionalAssignmentRe = mkRegex "^[0-9][0-9]?="
+    positionalMsg id =
+        err id 2270 "To assign positional parameters, use 'set -- first second ..' (or use [ ] to compare)."
+    indirectionMsg id =
+        err id 2271 "For indirection, use arrays, declare \"var$n=value\", or (for sh) read/eval."
+    badComparisonMsg id =
+        err id 2272 "Command name contains ==. For comparison, use [ \"$var\" = value ]."
+    conflictMarkerMsg id =
+        err id 2273 "Sequence of ===s found. Merge conflict or intended as a commented border?"
+    borderMsg id =
+        err id 2274 "Command name starts with ===. Intended as a commented border?"
+    prefixMsg id =
+        err id 2275 "Command name starts with =. Bad line break?"
+    genericMsg id =
+        err id 2276 "This is interpreted as a command name containing '='. Bad assignment or comparison?"
+    assign0Msg id bashfix =
+        case shellType params of
+            Bash -> errWithFix id 2277 "Use BASH_ARGV0 to assign to $0 in bash (or use [ ] to compare)." bashfix
+            Ksh -> err id 2278 "$0 can't be assigned in Ksh (but it does reflect the current function)."
+            Dash -> err id 2279 "$0 can't be assigned in Dash. This becomes a command name."
+            _ -> err id 2280 "$0 can't be assigned this way, and there is no portable alternative."
+    leadingNumberMsg id =
+        err id 2282 "Variable names can't start with numbers, so this is interpreted as a command."
+
+    isExpansion t =
+        case t of
+            T_Arithmetic {} -> True
+            _ -> isQuoteableExpansion t
+
+    isConflictMarker cmd = fromMaybe False $ do
+        str <- getUnquotedLiteral cmd
+        guard $ all (== '=') str
+        guard $ length str >= 4 && length str <= 12 -- Git uses 7 but who knows
+        return True
+
+    mayBeVariableName l = fromMaybe False $ do
+        guard . not $ any isQuotes l
+        guard . not $ any willBecomeMultipleArgs l
+        str <- getLiteralStringExt (\_ -> Just "x") (T_NormalWord (Id 0) l)
+        return $ isVariableName str
+
+    isLeadingNumberVar s =
+        let lead = takeWhile (/= '=') s
+        in not (null lead) && isDigit (head lead)
+            && all isVariableChar lead && not (all isDigit lead)
+
+    msg cmd leading (T_Literal litId s) = do
+        -- There are many different cases, and the order of the branches matter.
+        case leading of
+            -- --foo=42
+            [] | "-" `isPrefixOf` s -> -- There's SC2215 for these
+                return ()
+
+            -- ======Hello======
+            [] | "=" `isPrefixOf` s ->
+                case originalToken of
+                    T_SimpleCommand _ [] [word] | isConflictMarker word ->
+                        conflictMarkerMsg (getId originalToken)
+                    _ | "===" `isPrefixOf` s -> borderMsg (getId originalToken)
+                    _ -> prefixMsg (getId cmd)
+
+            -- $var==42
+            _ | "==" `isInfixOf` s ->
+                badComparisonMsg (getId cmd)
+
+            -- ${foo[x]}=42 and $foo=42
+            [T_DollarBraced id braced l] | "=" `isPrefixOf` s -> do
+                let variableStr = concat $ oversimplify l
+                let variableReference = getBracedReference variableStr
+                let variableModifier = getBracedModifier variableStr
+                let isPlain = isVariableName variableStr
+                let isPositional = all isDigit variableStr
+
+                let isArray = variableReference /= ""
+                                && "[" `isPrefixOf` variableModifier
+                                && "]" `isSuffixOf` variableModifier
+
+                case () of
+                    -- $foo=bar should already have caused a parse-time SC1066
+                    -- _ | not braced && isPlain ->
+                    --    return ()
+
+                    _ | variableStr == "" -> -- Don't try to fix ${}=foo
+                        genericMsg (getId cmd)
+
+                    -- $#=42 or ${#var}=42
+                    _ | "#" `isPrefixOf` variableStr ->
+                        genericMsg (getId cmd)
+
+                    -- ${0}=42
+                    _ | variableStr == "0" ->
+                        assign0Msg id $ fixWith [replaceToken id params "BASH_ARGV0"]
+
+                    -- $2=2
+                    _ | isPositional ->
+                        positionalMsg id
+
+                    _ | isArray || isPlain ->
+                        errWithFix id 2281
+                            ("Don't use " ++ (if braced then "${}" else "$") ++ " on the left side of assignments.") $
+                                fixWith $
+                                    if braced
+                                      then [ replaceStart id params 2 "", replaceEnd id params 1 "" ]
+                                      else [ replaceStart id params 1 "" ]
+
+                    _ -> indirectionMsg id
+
+            -- 2=42
+            [] | s `matches` positionalAssignmentRe ->
+                if "0=" `isPrefixOf` s
+                then
+                    assign0Msg litId $ fixWith [replaceStart litId params 1 "BASH_ARGV0"]
+                else
+                    positionalMsg litId
+
+            -- 9foo=42
+            [] | isLeadingNumberVar s ->
+                leadingNumberMsg (getId cmd)
+
+            -- var${foo}x=42
+            (_:_) | mayBeVariableName leading && (all isVariableChar $ takeWhile (/= '=') s) ->
+                indirectionMsg (getId cmd)
+
+            _ -> genericMsg (getId cmd)
+
+
+prop_checkSecondArgIsComparison1 = verify checkSecondArgIsComparison "foo = $bar"
+prop_checkSecondArgIsComparison2 = verify checkSecondArgIsComparison "$foo = $bar"
+prop_checkSecondArgIsComparison3 = verify checkSecondArgIsComparison "2f == $bar"
+prop_checkSecondArgIsComparison4 = verify checkSecondArgIsComparison "'var' =$bar"
+prop_checkSecondArgIsComparison5 = verify checkSecondArgIsComparison "foo ='$bar'"
+prop_checkSecondArgIsComparison6 = verify checkSecondArgIsComparison "$foo =$bar"
+prop_checkSecondArgIsComparison7 = verify checkSecondArgIsComparison "2f ==$bar"
+prop_checkSecondArgIsComparison8 = verify checkSecondArgIsComparison "'var' =$bar"
+prop_checkSecondArgIsComparison9 = verify checkSecondArgIsComparison "var += $(foo)"
+prop_checkSecondArgIsComparison10 = verify checkSecondArgIsComparison "var +=$(foo)"
+checkSecondArgIsComparison _ t =
+    case t of
+        T_SimpleCommand _ _ (lhs:arg:_) -> sequence_ $ do
+            argString <- getLeadingUnquotedString arg
+            case argString of
+                '=':'=':'=':'=':_ -> Nothing -- Don't warn about `echo ======` and such
+                '+':'=':_ ->
+                        return $ err (headId t) 2285 $
+                            "Remove spaces around += to assign (or quote '+=' if literal)."
+                '=':'=':_ ->
+                        return $ err (getId t) 2284 $
+                            "Use [ x = y ] to compare values (or quote '==' if literal)."
+                '=':_ ->
+                        return $ err (headId arg) 2283 $
+                            "Remove spaces around = to assign (or use [ ] to compare, or quote '=' if literal)."
+                _ -> Nothing
+        _ -> return ()
+  where
+    -- We don't pinpoint exactly, but this helps cases like foo =$bar
+    headId t =
+        case t of
+            T_NormalWord _ (x:_) -> getId x
+            _ -> getId t
 
 return []
 runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs
--- a/src/ShellCheck/AnalyzerLib.hs
+++ b/src/ShellCheck/AnalyzerLib.hs
@@ -163,6 +163,8 @@
 info  id code str = addComment $ makeComment InfoC id code str
 style id code str = addComment $ makeComment StyleC id code str
 
+errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
+errWithFix  = addCommentWithFix ErrorC
 warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
 warnWithFix  = addCommentWithFix WarningC
 styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
@@ -178,7 +180,7 @@
         withFix = comment {
             tcFix = Just fix
         }
-    in withFix `deepseq` withFix
+    in force withFix
 
 makeParameters spec =
     let params = Parameters {
@@ -236,6 +238,10 @@
 prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
 prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
 prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
+prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
+prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
+prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh
+prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
 
 determineShellTest = determineShellTest' Nothing
 determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@@ -249,22 +255,11 @@
         headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
     fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
 
--- Given a string like "/bin/bash" or "/usr/bin/env dash",
--- return the shell basename like "bash" or "dash"
-executableFromShebang :: String -> String
-executableFromShebang = shellFor
-  where
-    shellFor s | "/env " `isInfixOf` s = headOrDefault "" (drop 1 $ words s)
-    shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
-    shellFor s = reverse . takeWhile (/= '/') . reverse $ s
-
-
-
 -- Given a root node, make a map from Id to parent Token.
 -- This is used to populate parentMap in Parameters
 getParentTree :: Token -> Map.Map Id Token
 getParentTree t =
-    snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
+    snd $ execState (doStackAnalysis pre post t) ([], Map.empty)
   where
     pre t = modify (first ((:) t))
     post t = do
@@ -293,15 +288,15 @@
 
 
 isQuoteFreeNode strict tree t =
-    (isQuoteFreeElement t == Just True) ||
+    isQuoteFreeElement t ||
         headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
   where
     -- Is this node self-quoting in itself?
     isQuoteFreeElement t =
         case t of
-            T_Assignment {} -> return True
-            T_FdRedirect {} -> return True
-            _               -> Nothing
+            T_Assignment {} -> True
+            T_FdRedirect {} -> True
+            _               -> False
 
     -- Are any subnodes inherently self-quoting?
     isQuoteFreeContext t =
@@ -354,8 +349,8 @@
 
 -- Like above, if koala_man knew Haskell when starting this project.
 getClosestCommandM t = do
-    tree <- asks parentMap
-    return $ getClosestCommand tree t
+    params <- ask
+    return $ getClosestCommand (parentMap params) t
 
 -- Is the token used as a command name (the first word in a T_SimpleCommand)?
 usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
@@ -364,8 +359,8 @@
         | currentId == getId word = go id rest
     go currentId (T_DoubleQuoted id [word]:rest)
         | currentId == getId word = go id rest
-    go currentId (T_SimpleCommand _ _ (word:_):_)
-        | currentId == getId word = True
+    go currentId (t@(T_SimpleCommand _ _ (word:_)):_) =
+        getId word == currentId || getId (getCommandTokenOrThis t) == currentId
     go _ _ = False
 
 -- A list of the element and all its parents up to the root node.
@@ -377,8 +372,8 @@
 -- Version of the above taking the map from the current context
 -- Todo: give this the name "getPath"
 getPathM t = do
-    map <- asks parentMap
-    return $ getPath map t
+    params <- ask
+    return $ getPath (parentMap params) t
 
 isParentOf tree parent child =
     elem (getId parent) . map getId $ getPath tree child
@@ -388,14 +383,13 @@
 -- Find the first match in a list where the predicate is Just True.
 -- Stops if it's Just False and ignores Nothing.
 findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
-findFirst p l =
-    case l of
-        [] -> Nothing
-        (x:xs) ->
-            case p x of
-                Just True  -> return x
-                Just False -> Nothing
-                Nothing    -> findFirst p xs
+findFirst p = foldr go Nothing
+  where
+    go x acc =
+      case p x of
+        Just True  -> return x
+        Just False -> Nothing
+        Nothing    -> acc
 
 -- Check whether a word is entirely output from a single command
 tokenIsJustCommandOutput t = case t of
@@ -410,8 +404,7 @@
 
 -- TODO: Replace this with a proper Control Flow Graph
 getVariableFlow params t =
-    let (_, stack) = runState (doStackAnalysis startScope endScope t) []
-    in reverse stack
+    reverse $ execState (doStackAnalysis startScope endScope t) []
   where
     startScope t =
         let scopeType = leadType params t
@@ -462,28 +455,22 @@
 
     causesSubshell = do
         (T_Pipeline _ _ list) <- parentPipeline
-        if length list <= 1
-            then return False
-            else if not $ hasLastpipe params
-                then return True
-                else return . not $ (getId . head $ reverse list) == getId t
+        return $ case list of
+            _:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t
+            _ -> 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
+            [(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars]
+        T_SimpleCommand {} ->
+            getModifiedVariableCommand t
 
         TA_Unary _ "++|" v@(TA_Variable _ name _)  ->
             [(t, v, name, DataString $ SourceFrom [v])]
         TA_Unary _ "|++" v@(TA_Variable _ name _)  ->
             [(t, v, name, DataString $ SourceFrom [v])]
-        TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
+        TA_Assignment _ op (TA_Variable _ name _) rhs -> do
             guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
             return (t, t, name, DataString $ SourceFrom [rhs])
 
@@ -495,26 +482,30 @@
 
         -- Count [[ -v foo ]] as an "assignment".
         -- This is to prevent [ -v foo ] being unassigned or unused.
-        TC_Unary id _ "-v" token -> maybeToList $ do
+        TC_Unary id _ "-v" token -> do
             str <- fmap (takeWhile (/= '[')) $ -- Quoted index
                     flip getLiteralStringExt token $ \x ->
                 case x of
                     T_Glob _ s -> return s -- Unquoted index
-                    _          -> Nothing
+                    _          -> []
 
             guard . not . null $ str
             return (t, token, str, DataString SourceChecked)
 
+        TC_Unary _ _ "-n" token -> markAsChecked t token
+        TC_Unary _ _ "-z" token -> markAsChecked t token
+        TC_Nullary _ _ token -> markAsChecked t token
+
         T_DollarBraced _ _ l -> maybeToList $ do
-            let string = bracedString t
+            let string = concat $ oversimplify l
             let modifier = getBracedModifier string
             guard $ any (`isPrefixOf` modifier) ["=", ":="]
             return (t, t, getBracedReference string, DataString $ SourceFrom [l])
 
-        t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
+        T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
             [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
 
-        t@(T_CoProc _ name _) ->
+        T_CoProc _ name _ ->
             [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
 
         --Points to 'for' rather than variable
@@ -522,6 +513,14 @@
         T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
         T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
         _ -> []
+  where
+    markAsChecked place token = mapMaybe (f place) $ getWordParts token
+    f place t = case t of
+            T_DollarBraced _ _ l ->
+                let str = getBracedReference $ concat $ oversimplify l in do
+                    guard $ isVariableName str
+                    return (place, t, str, DataString SourceChecked)
+            _ -> Nothing
 
 isClosingFileOp op =
     case op of
@@ -541,6 +540,9 @@
                     (not $ any (`elem` flags) ["f", "F"])
             then concatMap getReference rest
             else []
+        "local" -> if "x" `elem` flags
+            then concatMap getReference rest
+            else []
         "trap" ->
             case rest of
                 head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
@@ -569,10 +571,14 @@
         "builtin" ->
             getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
         "read" ->
-            let params = map getLiteral rest
-                readArrayVars = getReadArrayVariables rest
-            in
-                catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
+            let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
+            in fromMaybe fallback $ do
+                parsed <- getGnuOpts flagsForRead rest
+                case lookup "a" parsed of
+                    Just (_, var) -> (:[]) <$> getLiteralArray var
+                    Nothing -> return $ catMaybes $
+                        map (getLiteral . snd . snd) $ filter (null . fst) parsed
+
         "getopts" ->
             case rest of
                 opts:var:_ -> maybeToList $ getLiteral var
@@ -664,25 +670,28 @@
         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)
-
-    -- get all the array variables used in read, e.g. read -a arr
-    getReadArrayVariables args =
-        map (getLiteralArray . snd)
-            (filter (isArrayFlag . fst) (zip args (tail args)))
-
-    isArrayFlag x = fromMaybe False $ do
-        str <- getLiteralString x
-        return $ case str of
-                    '-':'-':_ -> False
-                    '-':str -> 'a' `elem` str
-                    _ -> False
+    -- where only the first non-option one is used if any.
+    getMapfileArray base rest = parseArgs `mplus` fallback
+      where
+        parseArgs :: Maybe (Token, Token, String, DataType)
+        parseArgs = do
+            args <- getGnuOpts "d:n:O:s:u:C:c:t" rest
+            case [y | ("",(_,y)) <- args] of
+                [] ->
+                    return (base, base, "MAPFILE", DataArray SourceExternal)
+                first:_ -> do
+                    name <- getLiteralString first
+                    guard $ isVariableName name
+                    return (base, first, name, DataArray SourceExternal)
+        -- If arg parsing fails (due to bad or new flags), get the last variable name
+        fallback :: Maybe (Token, Token, String, DataType)
+        fallback = do
+            (name, token) <- listToMaybe . mapMaybe f $ reverse rest
+            return (base, token, name, DataArray SourceExternal)
+        f arg = do
+            name <- getLiteralString arg
+            guard $ isVariableName name
+            return (name, arg)
 
     -- get the FLAGS_ variable created by a shflags DEFINE_ call
     getFlagVariable (n:v:_) = do
@@ -713,7 +722,7 @@
 
 getReferencedVariables parents t =
     case t of
-        T_DollarBraced id _ l -> let str = bracedString t in
+        T_DollarBraced id _ l -> let str = concat $ oversimplify l in
             (t, t, getBracedReference str) :
                 map (\x -> (l, l, x)) (
                     getIndexReferences str
@@ -738,7 +747,7 @@
             (t, t, "output")
             ]
 
-        t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
+        T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo
             [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
         x -> getReferencedVariableCommand x
   where
@@ -755,9 +764,9 @@
 
     literalizer t = case t of
         T_Glob _ s -> return s    -- Also when parsed as globs
-        _          -> Nothing
+        _          -> []
 
-    getIfReference context token = maybeToList $ do
+    getIfReference context token = do
             str@(h:_) <- getLiteralStringExt literalizer token
             when (isDigit h) $ fail "is a number"
             return (context, token, getBracedReference str)
@@ -808,7 +817,7 @@
 prop_getVariablesFromLiteral1 =
     getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
 getVariablesFromLiteral string =
-    map (!! 0) $ matchAllSubgroups variableRegex string
+    map head $ matchAllSubgroups variableRegex string
   where
     variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
 
@@ -824,27 +833,26 @@
 prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
 prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
 prop_getBracedReference11= getBracedReference "!os*" == ""
+prop_getBracedReference11b= getBracedReference "!os@" == ""
 prop_getBracedReference12= getBracedReference "!os?bar**" == ""
 prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
 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 ""       = ""
+    dropPrefix (c:rest) | c `elem` "!#" = rest
+    dropPrefix cs = cs
     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"
+    getSpecial (c:_) | c `elem` "*@#?-$!" = return [c]
+    getSpecial _ = fail "empty or not special"
 
-    nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
-        let suffix = dropWhile isVariableChar rest
-        guard $ suffix /= rest -- e.g. ${!@}
-        first <- suffix !!! 0
-        guard $ first `elem` "*?"
+    nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
+        guard $ isVariableChar next -- e.g. ${!@}
+        first <- find (not . isVariableChar) rest
+        guard $ first `elem` "*?@"
         return ""
     nameExpansion _ = Nothing
 
@@ -877,8 +885,8 @@
 
 -- Run a command if the shell is in the given list
 whenShell l c = do
-    shell <- asks shellType
-    when (shell `elem` l ) c
+    params <- ask
+    when (shellType params `elem` l ) c
 
 
 filterByAnnotation asSpec params =
@@ -907,45 +915,15 @@
 -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
 isQuotedAlternativeReference t =
     case t of
-        T_DollarBraced _ _ _ ->
-            getBracedModifier (bracedString t) `matches` re
+        T_DollarBraced _ _ l ->
+            getBracedModifier (concat $ oversimplify l) `matches` re
         _ -> False
   where
     re = mkRegex "(^|\\]):?\\+"
 
--- getGnuOpts "erd:u:" will parse a SimpleCommand like
---     read -re -d : -u 3 bar
--- into
---     Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
--- where flags with arguments map to arguments, while others map to themselves.
--- Any unrecognized flag will result in Nothing.
-getGnuOpts str t = getOpts str $ getAllFlags t
-getBsdOpts str t = getOpts str $ getLeadingFlags t
-getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
-getOpts string flags = process flags
-  where
-    flagList (c:':':rest) = ([c], True) : flagList rest
-    flagList (c:rest)     = ([c], False) : flagList rest
-    flagList []           = []
-    flagMap = Map.fromList $ ("", False) : flagList string
-
-    process [] = return []
-    process [(token, flag)] = do
-        takesArg <- Map.lookup flag flagMap
-        guard $ not takesArg
-        return [(flag, token)]
-    process ((token1, flag1):rest2@((token2, flag2):rest)) = do
-        takesArg <- Map.lookup flag1 flagMap
-        if takesArg
-            then do
-                guard $ null flag2
-                more <- process rest
-                return $ (flag1, token2) : more
-            else do
-                more <- process rest2
-                return $ (flag1, token1) : more
-
-supportsArrays shell = shell == Bash || shell == Ksh
+supportsArrays Bash = True
+supportsArrays Ksh = True
+supportsArrays _ = False
 
 -- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
 isBashLike :: Parameters -> Bool
diff --git a/src/ShellCheck/Checker.hs b/src/ShellCheck/Checker.hs
--- a/src/ShellCheck/Checker.hs
+++ b/src/ShellCheck/Checker.hs
@@ -229,6 +229,12 @@
 prop_cantSourceDynamic2 =
     [1090] == checkWithIncludes [("lib", "")] "source ~/foo"
 
+prop_canStripPrefixAndSource =
+    null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
+
+prop_canStripPrefixAndSource2 =
+    null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
+
 prop_canSourceDynamicWhenRedirected =
     null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
 
@@ -270,7 +276,7 @@
     check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
 
 prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
-    2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
+    3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
 
 prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
 
@@ -293,6 +299,13 @@
     result = checkWithSpec [] emptyCheckSpec {
         csFilename = "file.sh",
         csScript = "#shellcheck disable=SC2148\nfoo"
+    }
+
+prop_canDisableParseErrors = null $ result
+  where
+    result = checkWithSpec [] emptyCheckSpec {
+        csFilename = "file.sh",
+        csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
     }
 
 prop_shExtensionDoesntMatter = result == [2148]
diff --git a/src/ShellCheck/Checks/Commands.hs b/src/ShellCheck/Checks/Commands.hs
--- a/src/ShellCheck/Checks/Commands.hs
+++ b/src/ShellCheck/Checks/Commands.hs
@@ -19,6 +19,7 @@
 -}
 {-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiWayIf #-}
 
 -- This module contains checks that examine specific commands by name.
 module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
@@ -52,8 +53,6 @@
 verify f s = producesComments (getChecker [f]) s == Just True
 verifyNot f s = producesComments (getChecker [f]) s == Just False
 
-arguments (T_SimpleCommand _ _ (cmd:args)) = args
-
 commandChecks :: [CommandCheck]
 commandChecks = [
     checkTr
@@ -95,6 +94,7 @@
     ,checkSudoArgs
     ,checkSourceArgs
     ,checkChmodDashr
+    ,checkXargsDashi
     ]
 
 optionalChecks = map fst optionalCommandChecks
@@ -115,6 +115,35 @@
       verify check (cdPositive desc)
       && verifyNot check (cdNegative desc)
 
+-- Run a check against the getopt parser. If it fails, the lists are empty.
+checkGetOpts str flags args f =
+    flags == actualFlags && args == actualArgs
+  where
+    toTokens = map (T_Literal (Id 0)) . words
+    opts = fromMaybe [] $ f (toTokens str)
+    actualFlags = filter (not . null) $ map fst opts
+    actualArgs = [onlyLiteralString x | ("", (_, x)) <- opts]
+
+-- Short options
+prop_checkGetOptsS1 = checkGetOpts "-f x" ["f"] [] $ getOpts (True, True) "f:" []
+prop_checkGetOptsS2 = checkGetOpts "-fx" ["f"] [] $ getOpts (True, True) "f:" []
+prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True) "fx" []
+prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" []
+prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" []
+
+-- Long options
+prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
+prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
+prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" []
+prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" []
+
+-- Know when to terminate
+prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" []
+prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" []
+prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" []
+prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" []
+
+
 buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
 buildCommandMap = foldl' addCheck Map.empty
   where
@@ -199,12 +228,11 @@
 checkFindNameGlob = CommandCheck (Basename "find") (f . arguments)  where
     acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
     f [] = return ()
-    f (x:xs) = g x xs
-    g _ [] = return ()
-    g a (b:r) = do
+    f (x:xs) = foldr g (const $ return ()) xs x
+    g b acc a = do
         forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
             warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
-        g b r
+        acc b
 
 
 prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
@@ -283,17 +311,11 @@
         candidates =
             sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
 
-    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]\\*|[][^$+\\\\]"
+    getSuspiciousRegexWildcard str = case matchRegex suspicious str of
+        Just [[c]] | not (str `matches` contra) -> Just c
+        _ -> fail "looks good"
+    suspicious = mkRegex "([A-Za-z1-9])\\*"
+    contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
 
 
 prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
@@ -462,8 +484,8 @@
   where
     check t = sequence_ $ do
         let flags = getAllFlags t
-        dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
-        dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
+        dashP <- find (\(_,f) -> f == "p" || f == "parents") flags
+        dashM <- find (\(_,f) -> f == "m" || f == "mode") flags
         -- mkdir -pm 0700 dir  is fine, so is ../dir, but dir/subdir is not.
         guard $ any couldHaveSubdirs (drop 1 $ arguments t)
         return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
@@ -483,7 +505,7 @@
 checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
   where
     f args = case args of
-        first:rest -> unless (isFlag first) $ mapM_ check rest
+        first:rest | not $ isFlag first -> mapM_ check rest
         _ -> return ()
 
     check param = sequence_ $ do
@@ -520,9 +542,9 @@
             info (getId cmd) 2117
                 "To run commands as another user, use su -c or sudo."
 
-    undirected (T_Pipeline _ _ l) = length l <= 1
+    undirected (T_Pipeline _ _ (_:_:_)) = False
     -- This should really just be modifications to stdin, but meh
-    undirected (T_Redirecting _ list _) = null list
+    undirected (T_Redirecting _ (_:_) _) = False
     undirected _ = True
 
 
@@ -539,9 +561,8 @@
             ([], 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
+        forM_ (find (not . isConstant) parts) $
+            \x -> info (getId x) 2029
                 "Note that, unescaped, this expands on the client side."
     checkArg _ = return ()
 
@@ -567,6 +588,8 @@
 prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
 prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
 prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
+prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
+
 checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
     f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
     f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@@ -580,22 +603,21 @@
             let formatCount = length formats
             let argCount = length more
 
-            return $
-                case () of
-                    () | argCount == 0 && formatCount == 0 ->
-                        return () -- This is fine
-                    () | formatCount == 0 && argCount > 0 ->
-                        err (getId format) 2182
-                            "This printf format string has no variables. Other arguments are ignored."
-                    () | any mayBecomeMultipleArgs more ->
-                        return () -- We don't know so trust the user
-                    () | argCount < formatCount && onlyTrailingTs formats argCount ->
-                        return () -- Allow trailing %()Ts since they use the current time
-                    () | argCount > 0 && argCount `mod` formatCount == 0 ->
-                        return () -- Great: a suitable number of arguments
-                    () ->
-                        warn (getId format) 2183 $
-                            "This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
+            return $ if
+                | argCount == 0 && formatCount == 0 ->
+                    return () -- This is fine
+                | formatCount == 0 && argCount > 0 ->
+                    err (getId format) 2182
+                        "This printf format string has no variables. Other arguments are ignored."
+                | any mayBecomeMultipleArgs more ->
+                    return () -- We don't know so trust the user
+                | argCount < formatCount && onlyTrailingTs formats argCount ->
+                    return () -- Allow trailing %()Ts since they use the current time
+                | argCount > 0 && argCount `mod` formatCount == 0 ->
+                    return () -- Great: a suitable number of arguments
+                | otherwise ->
+                    warn (getId format) 2183 $
+                        "This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
 
         unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
           info (getId format) 2059
@@ -610,6 +632,8 @@
 prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
 prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
 prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
+prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss"
+prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss"
 getPrintfFormats = getFormats
   where
     -- Get the arguments in the string as a string of type characters,
@@ -628,17 +652,17 @@
 
     regexBasedGetFormats rest =
         case matchRegex re rest of
-            Just [width, precision, typ, rest] ->
+            Just [width, precision, typ, rest, _] ->
                 (if width == "*" then "*" else "") ++
                 (if precision == "*" then "*" else "") ++
                 typ ++ getFormats rest
             Nothing -> take 1 rest ++ getFormats rest
       where
         -- constructed based on specifications in "man printf"
-        re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)"
-        --            \____ _____/\___ ____/   \____ ____/\_________ _________/ \ /
-        --                 V          V             V               V            V
-        --               flags    field width  precision   format character     rest
+        re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
+        --            \____ _____/\___ ____/   \____ ____/\_________ _________/ \______ /
+        --                 V          V             V               V               V
+        --               flags    field width  precision   format character        rest
         -- field width and precision can be specified with a '*' instead of a digit,
         -- in which case printf will accept one more argument for each '*' used
 
@@ -663,17 +687,12 @@
 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 (var:rest)
+        | (not (null rest) && isVariableName str) || isAssignment str =
+            warn (getId var) 2121 "To assign a variable, use just 'var=value', no 'set ..'."
+      where str = literal 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
@@ -687,8 +706,7 @@
 checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
   where
     check t = sequence_ $ do
-        var <- getSingleUnmodifiedVariable t
-        let name = bracedString var
+        name <- getSingleUnmodifiedBracedString t
         return . warn (getId t) 2163 $
             "This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
 
@@ -704,26 +722,25 @@
   where
     options = getGnuOpts flagsForRead
     getVars cmd = fromMaybe [] $ do
-        opts <- options cmd
-        return [y | (x,y) <- opts, null x || x == "a"]
+        opts <- options $ arguments cmd
+        return [y | (x,(_, y)) <- opts, null x || x == "a"]
 
     check cmd = mapM_ warning $ getVars cmd
     warning t = sequence_ $ do
-        var <- getSingleUnmodifiedVariable t
-        let name = bracedString var
+        name <- getSingleUnmodifiedBracedString t
         guard $ isVariableName name   -- e.g. not $1
         return . warn (getId t) 2229 $
             "This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
 
 -- Return the single variable expansion that makes up this word, if any.
 -- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
-getSingleUnmodifiedVariable :: Token -> Maybe Token
-getSingleUnmodifiedVariable word =
+getSingleUnmodifiedBracedString :: Token -> Maybe String
+getSingleUnmodifiedBracedString word =
     case getWordParts word of
-        [t@(T_DollarBraced {})] ->
-            let contents = bracedString t
+        [T_DollarBraced _ _ l] ->
+            let contents = concat $ oversimplify l
                 name = getBracedReference contents
-            in guard (contents == name) >> return t
+            in guard (contents == name) >> return contents
         _ -> Nothing
 
 prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
@@ -883,12 +900,12 @@
             notRequested = Map.difference handledMap requestedMap
 
     warnUnhandled optId caseId str =
-        warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."
+        warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
 
-    warnRedundant (key, expr) = sequence_ $ do
-        str <- key
-        guard $ str `notElem` ["*", ":", "?"]
-        return $ warn (getId expr) 2214 "This case is not specified by getopts."
+    warnRedundant (Just str, expr)
+        | str `notElem` ["*", ":", "?"] =
+            warn (getId expr) 2214 "This case is not specified by getopts."
+    warnRedundant _ = return ()
 
     getHandledStrings (_, globs, _) =
         map (\x -> (literal x, x)) globs
@@ -899,7 +916,7 @@
 
     fromGlob t =
         case t of
-            T_Glob _ ('[':c:']':[]) -> return [c]
+            T_Glob _ ['[', c, ']'] -> return [c]
             T_Glob _ "*" -> return "*"
             T_Glob _ "?" -> return "?"
             _ -> Nothing
@@ -934,7 +951,7 @@
     when (isRecursive t) $
         mapM_ (mapM_ checkWord . braceExpand) $ arguments t
   where
-    isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags
+    isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags
 
     checkWord token =
         case getLiteralString token of
@@ -966,9 +983,9 @@
         f _ = return ""
 
     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 _ [] = []
+    skipRepeating c = foldr go []
+      where
+        go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r
 
     paths = [
         "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
@@ -1039,7 +1056,7 @@
 
 prop_checkWhich = verify checkWhich "which '.+'"
 checkWhich = CommandCheck (Basename "which") $
-    \t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."
+    \t -> info (getId $ getCommandTokenOrThis t) 2230 "'which' is non-standard. Use builtin 'command -v' instead."
 
 prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
 prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
@@ -1081,8 +1098,8 @@
 checkSudoArgs = CommandCheck (Basename "sudo") f
   where
     f t = sequence_ $ do
-        opts <- parseOpts t
-        let nonFlags = [x | ("",x) <- opts]
+        opts <- parseOpts $ arguments t
+        let nonFlags = [x | ("",(x, _)) <- opts]
         commandArg <- nonFlags !!! 0
         command <- getLiteralString commandArg
         guard $ command `elem` builtins
@@ -1112,6 +1129,19 @@
         flag <- getLiteralString t
         guard $ flag == "-r"
         return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
+
+prop_checkXargsDashi1 = verify checkXargsDashi "xargs -i{} echo {}"
+prop_checkXargsDashi2 = verifyNot checkXargsDashi "xargs -I{} echo {}"
+prop_checkXargsDashi3 = verifyNot checkXargsDashi "xargs sed -i -e foo"
+prop_checkXargsDashi4 = verify checkXargsDashi "xargs -e sed -i foo"
+prop_checkXargsDashi5 = verifyNot checkXargsDashi "xargs -x sed -i foo"
+checkXargsDashi = CommandCheck (Basename "xargs") f
+  where
+    f t = sequence_ $ do
+        opts <- parseOpts $ arguments t
+        (option, value) <- lookup "i" opts
+        return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
+    parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
 
 return []
 runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs
--- a/src/ShellCheck/Checks/ShellSupport.hs
+++ b/src/ShellCheck/Checks/ShellSupport.hs
@@ -178,6 +178,9 @@
 prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
 prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
 prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
+prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
+prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
+prop_checkBashisms99 = verifyNot checkBashisms "#!/bin/dash\necho [^f]oo"
 checkBashisms = ForShell [Sh, Dash] $ \t -> do
     params <- ask
     kludge params t
@@ -186,102 +189,102 @@
   kludge params = bashism
    where
     isDash = shellType params == Dash
-    warnMsg id s =
+    warnMsg id code s =
         if isDash
-        then warn id 2169 $ "In dash, " ++ s ++ " not supported."
-        else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
+        then err  id code $ "In dash, " ++ s ++ " not supported."
+        else warn id code $ "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 (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
+    bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
+    bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
+    bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
+    bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
+    bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
+    bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
+    bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
+    bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
+    bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is"
+    bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
     bashism (TC_Binary id SingleBracket op _ _)
         | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
-            unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
+            unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
     bashism (TC_Binary id SingleBracket op _ _)
         | op `elem` [ "-ot", "-nt", "-ef" ] =
-            unless isDash $ warnMsg id $ op ++ " is"
+            unless isDash $ warnMsg id 3013 $ op ++ " is"
     bashism (TC_Binary id SingleBracket "==" _ _) =
-            warnMsg id "== in place of = is"
+            warnMsg id 3014 "== in place of = is"
     bashism (TC_Binary id SingleBracket "=~" _ _) =
-            warnMsg id "=~ regex matching is"
+            warnMsg id 3015 "=~ regex matching is"
     bashism (TC_Unary id SingleBracket "-v" _) =
-            warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
+            warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
     bashism (TC_Unary id _ "-a" _) =
-            warnMsg id "unary -a in place of -e is"
+            warnMsg id 3017 "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 "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
-    bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
+            warnMsg id 3018 $ filter (/= '|') op ++ " is"
+    bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
+    bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
+    bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
+    bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
     bashism (T_FdRedirect id num _)
-        | all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
+        | all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
     bashism (T_Assignment id Append _ _ _) =
-        warnMsg id "+= is"
+        warnMsg id 3024 "+= is"
     bashism (T_IoFile id _ word) | isNetworked =
-            warnMsg id "/dev/{tcp,udp} is"
+            warnMsg id 3025 "/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_Glob id str) | not isDash && "[^" `isInfixOf` str =
+            warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
 
     bashism t@(TA_Variable id str _) | isBashVariable str =
-        warnMsg id $ str ++ " is"
+        warnMsg id 3028 $ str ++ " is"
 
     bashism t@(T_DollarBraced id _ token) = do
         mapM_ check expansion
         when (isBashVariable var) $
-                    warnMsg id $ var ++ " is"
+            warnMsg id 3028 $ var ++ " is"
       where
-        str = bracedString t
+        str = concat $ oversimplify token
         var = getBracedReference str
-        check (regex, feature) =
-            when (isJust $ matchRegex regex str) $ warnMsg id feature
+        check (regex, code, feature) =
+            when (isJust $ matchRegex regex str) $ warnMsg id code feature
 
     bashism t@(T_Pipe id "|&") =
-        warnMsg id "|& in place of 2>&1 | is"
+        warnMsg id 3029 "|& in place of 2>&1 | is"
     bashism (T_Array id _) =
-        warnMsg id "arrays are"
+        warnMsg id 3030 "arrays are"
     bashism (T_IoFile id _ t) | isGlob t =
-        warnMsg id "redirecting to/from globs is"
+        warnMsg id 3031 "redirecting to/from globs is"
     bashism (T_CoProc id _ _) =
-        warnMsg id "coproc is"
+        warnMsg id 3032 "coproc is"
 
     bashism (T_Function id _ _ str _) | not (isVariableName str) =
-        warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
+        warnMsg id 3033 "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"
+        warnMsg id 3034 "$(<file) to read files is"
     bashism (T_Backticked id [x]) | isOnlyRedirection x =
-        warnMsg id "`<file` to read files is"
+        warnMsg id 3035 "`<file` to read files is"
 
     bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
         | t `isCommand` "echo" && argString `matches` flagRegex =
             if isDash
             then
                 when (argString /= "-n") $
-                    warnMsg (getId arg) "echo flags besides -n"
+                    warnMsg (getId arg) 3036 "echo flags besides -n"
             else
-                warnMsg (getId arg) "echo flags are"
+                warnMsg (getId arg) 3037 "echo flags are"
       where
           argString = concat $ oversimplify arg
           flagRegex = mkRegex "^-[eEsn]+$"
 
     bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
-        | t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
-            warnMsg (getId arg) "exec flags are"
+        | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
+            warnMsg (getId arg) 3038 "exec flags are"
     bashism t@(T_SimpleCommand id _ _)
-        | t `isCommand` "let" = warnMsg id "'let' is"
+        | t `isCommand` "let" = warnMsg id 3039 "'let' is"
     bashism t@(T_SimpleCommand _ _ (cmd:args))
         | t `isCommand` "set" = unless isDash $
             checkOptions $ getLiteralArgs args
@@ -289,16 +292,17 @@
         -- Get the literal options from a list of arguments,
         -- up until the first non-literal one
         getLiteralArgs :: [Token] -> [(Id, String)]
-        getLiteralArgs (first:rest) = fromMaybe [] $ do
-            str <- getLiteralString first
-            return $ (getId first, str) : getLiteralArgs rest
-        getLiteralArgs [] = []
+        getLiteralArgs = foldr go []
+          where
+            go first rest = case getLiteralString first of
+                Just str -> (getId first, str) : rest
+                Nothing -> []
 
         -- Check a flag-option pair (such as -o errexit)
         checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
             | flag' `matches` oFlagRegex = do
                 when (opt' `notElem` longOptions) $
-                  warnMsg oid $ "set option " <> opt' <> " is"
+                  warnMsg oid 3040 $ "set option " <> opt' <> " is"
                 checkFlags (flag:rest)
             | otherwise = checkFlags (flag:opt:rest)
         checkOptions (flag:rest) = checkFlags (flag:rest)
@@ -311,10 +315,10 @@
                 unless (flag' `matches` validFlagsRegex) $
                   forM_ (tail flag') $ \letter ->
                     when (letter `notElem` optionsSet) $
-                      warnMsg fid $ "set flag " <> ('-':letter:" is")
+                      warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
                 checkOptions rest
             | beginsWithDoubleDash flag' = do
-                warnMsg fid $ "set flag " <> flag' <> " is"
+                warnMsg fid 3042 $ "set flag " <> flag' <> " is"
                 checkOptions rest
             -- Either a word that doesn't start with a dash, or simply '--',
             -- so stop checking.
@@ -336,16 +340,19 @@
         let name = fromMaybe "" $ getCommandName t
             flags = getLeadingFlags t
         in do
+            when (name == "local" && not isDash) $
+                -- This is so commonly accepted that we'll make it a special case
+                warnMsg id 3043 $ "'local' is"
             when (name `elem` unsupportedCommands) $
-                warnMsg id $ "'" ++ name ++ "' is"
+                warnMsg id 3044 $ "'" ++ name ++ "' is"
             sequence_ $ do
                 allowed' <- Map.lookup name allowedFlags
                 allowed <- allowed'
                 (word, flag) <- find
                     (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
-                return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
+                return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
 
-            when (name == "source") $ warnMsg id "'source' in place of '.' is"
+            when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
             when (name == "trap") $
                 let
                     check token = sequence_ $ do
@@ -353,12 +360,12 @@
                         let upper = map toUpper str
                         return $ do
                             when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
-                                warnMsg (getId token) $ "trapping " ++ str ++ " is"
+                                warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
                             when ("SIG" `isPrefixOf` upper) $
-                                warnMsg (getId token)
+                                warnMsg (getId token) 3048
                                     "prefixing signal names with 'SIG' is"
                             when (not isDash && upper /= str) $
-                                warnMsg (getId token)
+                                warnMsg (getId token) 3049
                                     "using lower/mixed case for signal names is"
                 in
                     mapM_ check (drop 1 rest)
@@ -367,13 +374,13 @@
                 format <- rest !!! 0  -- flags are covered by allowedFlags
                 let literal = onlyLiteralString format
                 guard $ "%q" `isInfixOf` literal
-                return $ warnMsg (getId format) "printf %q is"
+                return $ warnMsg (getId format) 3050 "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"] else []
+            ]
         allowedFlags = Map.fromList [
             ("cd", Just ["L", "P"]),
             ("exec", Just []),
@@ -390,29 +397,35 @@
             ("unset", Just ["f", "v"]),
             ("wait", Just [])
             ]
-    bashism t@(T_SourceCommand id src _) =
-        let name = fromMaybe "" $ getCommandName src
-        in when (name == "source") $ warnMsg id "'source' in place of '.' is"
-    bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
-        when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
+    bashism t@(T_SourceCommand id src _)
+        | getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
+    bashism (TA_Expansion _ (T_Literal id str : _))
+        | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
       where
         radix = mkRegex "^[0-9]+#"
     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 $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
-        (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
+        (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
+        (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
+        (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
+        (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
+        (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
+        (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
+        (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
+        (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
         ]
     bashVars = [
+        -- This list deliberately excludes $BASH_VERSION as it's often used
+        -- for shell identification.
         "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
         "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
-        "_"
+        "_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
+        "BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
+        "BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE",
+        "BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS",
+        "FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE"
         ]
     bashDynamicVars = [ "RANDOM", "SECONDS" ]
     dashVars = [ "_" ]
@@ -506,13 +519,13 @@
         case token of
             T_Assignment _ _ name (first:second:_) _ -> about second
             T_IndexedElement _ (first:second:_) _ -> about second
-            T_DollarBraced {} ->
-                when (isMultiDim token) $ about token
+            T_DollarBraced _ _ l ->
+                when (isMultiDim l) $ about token
             _ -> return ()
     about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
 
     re = mkRegex "^\\[.*\\]\\[.*\\]"  -- Fixme, this matches ${foo:- [][]} and such as well
-    isMultiDim t = getBracedModifier (bracedString t) `matches` re
+    isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
 
 prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
 prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
diff --git a/src/ShellCheck/Fixer.hs b/src/ShellCheck/Fixer.hs
--- a/src/ShellCheck/Fixer.hs
+++ b/src/ShellCheck/Fixer.hs
@@ -273,10 +273,10 @@
   where
     f sum _ PSLeaf = sum
     f sum target (PSBranch pivot left right cumulative) =
-        case () of
-            _ | target < pivot -> f sum target left
-            _ | target > pivot -> f (sum+cumulative) target right
-            _ -> sum+cumulative
+        case target `compare` pivot of
+            LT -> f sum target left
+            GT -> f (sum+cumulative) target right
+            EQ -> sum+cumulative
 
 -- Add a value to the Prefix Sum tree at the given index.
 -- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
@@ -285,10 +285,10 @@
   where
     f PSLeaf = PSBranch key PSLeaf PSLeaf value
     f (PSBranch pivot left right sum) =
-        case () of
-            _ | key < pivot -> PSBranch pivot (f left) right (sum + value)
-            _ | key > pivot -> PSBranch pivot left (f right) sum
-            _ -> PSBranch pivot left right (sum + value)
+        case key `compare` pivot of
+            LT -> PSBranch pivot (f left) right (sum + value)
+            GT -> PSBranch pivot left (f right) sum
+            EQ -> PSBranch pivot left right (sum + value)
 
 prop_pstreeSumsCorrectly kvs targets =
   let
diff --git a/src/ShellCheck/Formatter/TTY.hs b/src/ShellCheck/Formatter/TTY.hs
--- a/src/ShellCheck/Formatter/TTY.hs
+++ b/src/ShellCheck/Formatter/TTY.hs
@@ -127,7 +127,7 @@
     let lineCount = length fileLinesList
     let fileLines = listArray (1, lineCount) fileLinesList
     let groups = groupWith lineNo comments
-    mapM_ (\commentsForLine -> do
+    forM_ groups $ \commentsForLine -> do
         let lineNum = fromIntegral $ lineNo (head commentsForLine)
         let line = if lineNum < 1 || lineNum > lineCount
                         then ""
@@ -136,10 +136,9 @@
         putStrLn $ color "message" $
            "In " ++ fileName ++" line " ++ show lineNum ++ ":"
         putStrLn (color "source" line)
-        mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
+        forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
         putStrLn ""
         showFixedString color commentsForLine (fromIntegral lineNum) fileLines
-      ) groups
 
 -- Pick out only the lines necessary to show a fix in action
 sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
diff --git a/src/ShellCheck/Interface.hs b/src/ShellCheck/Interface.hs
--- a/src/ShellCheck/Interface.hs
+++ b/src/ShellCheck/Interface.hs
@@ -73,15 +73,15 @@
 
 
 data SystemInterface m = SystemInterface {
-    -- Read a file by filename, or return an error
+    -- | Read a file by filename, or return an error
     siReadFile :: String -> m (Either ErrorMessage String),
-    -- Given:
+    -- | Given:
     --   the current script,
     --   a list of source-path annotations in effect,
     --   and a sourced file,
     --   find the sourced file
     siFindSource :: String -> [String] -> String -> m FilePath,
-    -- Get the configuration file (name, contents) for a filename
+    -- | Get the configuration file (name, contents) for a filename
     siGetConfig :: String -> m (Maybe (FilePath, String))
 }
 
diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs
--- a/src/ShellCheck/Parser.hs
+++ b/src/ShellCheck/Parser.hs
@@ -24,7 +24,7 @@
 module ShellCheck.Parser (parseScript, runTests) where
 
 import ShellCheck.AST
-import ShellCheck.ASTLib
+import ShellCheck.ASTLib hiding (runTests)
 import ShellCheck.Data
 import ShellCheck.Interface
 
@@ -87,11 +87,23 @@
 unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
 unicodeSingleQuotes = "\x2018\x2019"
 
-prop_spacing = isOk spacing "  \\\n # Comment"
+prop_spacing1 = isOk spacing "  \\\n # Comment"
+prop_spacing2 = isOk spacing "# We can continue lines with \\"
+prop_spacing3 = isWarning spacing "   \\\n #  --verbose=true \\"
 spacing = do
-    x <- many (many1 linewhitespace <|> try (string "\\\n" >> return ""))
+    x <- many (many1 linewhitespace <|> continuation)
     optional readComment
     return $ concat x
+  where
+    continuation = do
+        try (string "\\\n")
+        -- The line was continued. Warn if this next line is a comment with a trailing \
+        whitespace <- many linewhitespace
+        optional $ do
+            x <- readComment
+            when ("\\" `isSuffixOf` x) $
+                parseProblem ErrorC 1143 "This backslash is part of a comment and does not continue the line."
+        return whitespace
 
 spacing1 = do
     spacing <- spacing
@@ -123,8 +135,10 @@
     return $ T_Literal id [c]
 
 carriageReturn = do
-    parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
+    pos <- getPosition
     char '\r'
+    parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
+    return '\r'
 
 almostSpace =
     choice [
@@ -209,9 +223,14 @@
 
 endSpan (IncompleteInterval start) = do
     endPos <- getPosition
-    id <- getNextIdBetween start endPos
-    return id
+    getNextIdBetween start endPos
 
+getSpanPositionsFor m = do
+    start <- getPosition
+    m
+    end <- getPosition
+    return (start, end)
+
 addToHereDocMap id list = do
     state <- getState
     let map = hereDocMap state
@@ -253,16 +272,22 @@
 shouldIgnoreCode code = do
     context <- getCurrentContexts
     checkSourced <- Mr.asks checkSourced
-    return $ any (disabling checkSourced) context
+    return $ any (contextItemDisablesCode checkSourced code) context
+
+-- Does this item on the context stack disable warnings for 'code'?
+contextItemDisablesCode :: Bool -> Integer -> Context -> Bool
+contextItemDisablesCode alsoCheckSourced code = disabling alsoCheckSourced
   where
     disabling checkSourced item =
         case item of
             ContextAnnotation list -> any disabling' list
             ContextSource _ -> not $ checkSourced
             _ -> False
-    disabling' (DisableComment n) = code == n
+    disabling' (DisableComment n m) = code >= n && code < m
     disabling' _ = False
 
+
+
 getCurrentAnnotations includeSource =
     concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts
   where
@@ -373,16 +398,15 @@
 parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
 
 --------- Convenient combinators
-thenSkip main follow = do
-    r <- main
-    optional follow
-    return r
+thenSkip main follow = main <* optional follow
 
 unexpecting s p = try $
     (try p >> fail ("Unexpected " ++ s)) <|> return ()
 
 notFollowedBy2 = unexpecting ""
 
+isFollowedBy p = (lookAhead . try $ p $> True) <|> return False
+
 reluctantlyTill p end =
     (lookAhead (void (try end) <|> eof) >> return []) <|> do
         x <- p
@@ -420,7 +444,7 @@
 
 parsecBracket before after op = do
     val <- before
-    (op val <* optional (after val)) <|> (after val *> fail "")
+    op val `thenSkip` after val <|> (after val *> fail "")
 
 swapContext contexts p =
     parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
@@ -914,8 +938,9 @@
 prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
 prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
 prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
-prop_readCondition24 = isWarning readCondition "[[ 1 == 2 ]]]"
 prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
+prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
+prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
 readCondition = called "test expression" $ do
     opos <- getPosition
     start <- startSpan
@@ -942,15 +967,9 @@
     cpos <- getPosition
     close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
     id <- endSpan start
-    when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
-    when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
-    optional $ lookAhead $ do
-        pos <- getPosition
-        notFollowedBy2 readCmdWord <|>
-            parseProblemAt pos ErrorC 1136
-                ("Unexpected characters after terminating " ++ close ++ ". Missing semicolon/linefeed?")
+    when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Test expression was opened with double [[ but closed with single ]. Make sure they match."
+    when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Test expression was opened with single [ but closed with double ]]. Make sure they match."
     spacing
-    many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
     return $ T_Condition id typ condition
 
 readAnnotationPrefix = do
@@ -964,6 +983,7 @@
 prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
 prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
 prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
+prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
 readAnnotation = called "shellcheck directive" $ do
     try readAnnotationPrefix
     many1 linewhitespace
@@ -984,12 +1004,16 @@
         key <- many1 (letter <|> char '-')
         char '=' <|> fail "Expected '=' after directive key"
         annotations <- case key of
-            "disable" -> readCode `sepBy` char ','
+            "disable" -> readRange `sepBy` char ','
               where
+                readRange = do
+                    from <- readCode
+                    to <- choice [ char '-' *> readCode, return $ from+1 ]
+                    return $ DisableComment from to
                 readCode = do
                     optional $ string "SC"
                     int <- many1 digit
-                    return $ DisableComment (read int)
+                    return $ read int
 
             "enable" -> readName `sepBy` char ','
               where
@@ -1027,6 +1051,7 @@
     unexpecting "shellcheck annotation" readAnnotationPrefix
     readAnyComment
 
+prop_readAnyComment = isOk readAnyComment "# Comment"
 readAnyComment = do
     char '#'
     many $ noneOf "\r\n"
@@ -1392,6 +1417,8 @@
     do
         next <- quotable <|> oneOf "?*@!+[]{}.,~#"
         when (next == ' ') $ checkTrailingSpaces pos <|> return ()
+        -- Check if this line is followed by a commented line with a trailing backslash
+        when (next == '\n') $ try . lookAhead $ void spacing
         return $ if next == '\n' then "" else [next]
       <|>
         do
@@ -1554,7 +1581,7 @@
 readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
   where
     arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
-        parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")
+        parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substitution), add space after $( . For $((arithmetics)), fix parsing errors.")
 
 prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
 readDollarSingleQuote = called "$'..' expression" $ do
@@ -1603,6 +1630,7 @@
     c <- readArithmeticContents
     string "))"
     id <- endSpan start
+    spacing
     return (T_Arithmetic id c)
 
 -- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used
@@ -1749,6 +1777,8 @@
 prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
 prop_readHereDoc20= isWarning readScript "cat << foo\n  foo\n()\nfoo\n"
 prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n  foo\n()\nfoo\n"
+prop_readHereDoc22 = isWarning readScript "cat << foo\r\ncow\r\nfoo\r\n"
+prop_readHereDoc23 = isNotOk readScript "cat << foo \r\ncow\r\nfoo\r\n"
 readHereDoc = called "here document" $ do
     pos <- getPosition
     try $ string "<<"
@@ -1777,7 +1807,9 @@
     -- Fun fact: bash considers << foo"" quoted, but not << <("foo").
     readToken = do
         str <- readStringForParser readNormalWord
-        return $ unquote str
+        -- A here doc actually works with \r\n because the \r becomes part of the token
+        crstr <- (carriageReturn >> (return $ str ++ "\r")) <|> return str
+        return $ unquote crstr
 
 readPendingHereDocs = do
     docs <- popPendingHereDocs
@@ -1892,14 +1924,14 @@
     debugHereDoc tokenId endToken doc
         | endToken `isInfixOf` doc =
             let lookAt line = when (endToken `isInfixOf` line) $
-                      parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")
+                      parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ (e4m line) ++ "' (!= '" ++ (e4m endToken) ++ "').")
             in do
-                  parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.")
+                  parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ (e4m endToken) ++ "' further down, but not on a separate line.")
                   mapM_ lookAt (lines doc)
         | map toLower endToken `isInfixOf` map toLower doc =
-            parseProblemAtId tokenId ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
+            parseProblemAtId tokenId ErrorC 1043 ("Found " ++ (e4m endToken) ++ " further down, but with wrong casing.")
         | otherwise =
-            parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")
+            parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ (e4m endToken) ++ "' in the here document.")
 
 
 readFilename = readNormalWord
@@ -1955,8 +1987,6 @@
     spacing
     return $ T_FdRedirect id n redir
 
-readRedirectList = many1 readIoRedirect
-
 prop_readHereString = isOk readHereString "<<< \"Hello $world\""
 readHereString = called "here string" $ do
     start <- startSpan
@@ -2079,10 +2109,6 @@
         then action
         else getParser def cmd rest
 
-    cStyleComment cmd =
-        case cmd of
-            _ -> False
-
     validateCommand cmd =
         case cmd of
             (T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)
@@ -2119,14 +2145,14 @@
     let file = getFile file' rest'
     override <- getSourceOverride
     let literalFile = do
-        name <- override `mplus` getLiteralString file
+        name <- override `mplus` getLiteralString file `mplus` stripDynamicPrefix file
         -- Hack to avoid 'source ~/foo' trying to read from literal tilde
         guard . not $ "~/" `isPrefixOf` name
         return name
     case literalFile of
         Nothing -> do
             parseNoteAtId (getId file) WarningC 1090
-                "Can't follow non-constant source. Use a directive to specify location."
+                "ShellCheck can't follow non-constant source. Use a directive to specify location."
             return t
         Just filename -> do
             proceed <- shouldFollow filename
@@ -2179,6 +2205,16 @@
             SourcePath x -> Just x
             _ -> Nothing
 
+    -- If the word has a single expansion as the directory, try stripping it
+    -- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file`
+    stripDynamicPrefix word =
+        case getWordParts word of
+            exp : rest | isStringExpansion exp -> do
+                str <- getLiteralString (T_NormalWord (Id 0) rest)
+                guard $ "/" `isPrefixOf` str
+                return $ "." ++ str
+            _ -> Nothing
+
     subRead name script =
         withContext (ContextSource name) $
             inSeparateContext $
@@ -2276,6 +2312,7 @@
 
 readCommand = choice [
     readCompoundCommand,
+    readConditionCommand,
     readCoProc,
     readSimpleCommand
     ]
@@ -2388,6 +2425,7 @@
     allspacing
     char ')' <|> fail "Expected ) closing the subshell"
     id <- endSpan start
+    spacing
     return $ T_Subshell id list
 
 prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"
@@ -2408,6 +2446,7 @@
         parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."
         fail "Missing '}'"
     id <- endSpan start
+    spacing
     return $ T_BraceGroup id list
 
 prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n  true\n}"
@@ -2460,6 +2499,11 @@
             parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."
             parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."
             return "Expected 'done'"
+
+    optional . lookAhead $ do
+        pos <- getPosition
+        try $ string "<("
+        parseProblemAt pos ErrorC 1142 "Use 'done < <(cmd)' to redirect from process substitution (currently missing one '<')."
     return commands
 
 
@@ -2482,19 +2526,31 @@
     readArithmetic id <|> readRegular id
   where
     readArithmetic id = called "arithmetic for condition" $ do
-        try $ string "(("
+        readArithmeticDelimiter '(' "Missing second '(' to start arithmetic for ((;;)) loop"
         x <- readArithmeticContents
         char ';' >> spacing
         y <- readArithmeticContents
         char ';' >> spacing
         z <- readArithmeticContents
         spacing
-        string "))"
+        readArithmeticDelimiter ')' "Missing second ')' to terminate 'for ((;;))' loop condition"
         spacing
         optional $ readSequentialSep >> spacing
         group <- readBraced <|> readDoGroup id
         return $ T_ForArithmetic id x y z group
 
+    -- For c='(' read "((" and be lenient about spaces
+    readArithmeticDelimiter c msg = do
+        char c
+        startPos <- getPosition
+        sp <- spacing
+        endPos <- getPosition
+        char c <|> do
+            parseProblemAt startPos ErrorC 1137 msg
+            fail ""
+        unless (null sp) $
+            parseProblemAtWithEnd startPos endPos ErrorC 1138 $ "Remove spaces between " ++ [c,c] ++ " in arithmetic for loop."
+
     readBraced = do
         (T_BraceGroup _ list) <- readBraceGroup
         return list
@@ -2625,7 +2681,7 @@
 
         readWithoutFunction = try $ do
             name <- many1 functionChars
-            guard $ name /= "time"  -- Interfers with time ( foo )
+            guard $ name /= "time"  -- Interferes with time ( foo )
             spacing
             readParens
             return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name
@@ -2665,9 +2721,38 @@
         id <- endSpan start
         return $ T_CoProcBody id body
 
-
 readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
 
+prop_readConditionCommand = isOk readConditionCommand "[[ x ]] > foo 2>&1"
+readConditionCommand = do
+    cmd <- readCondition
+    redirs <- many readIoRedirect
+    id <- getNextIdSpanningTokenList (cmd:redirs)
+
+    pos <- getPosition
+    hasDashAo <- isFollowedBy $ do
+        c <- choice $ try . string <$> ["-o", "-a", "or", "and"]
+        posEnd <- getPosition
+        parseProblemAtWithEnd pos posEnd ErrorC 1139 $
+            "Use " ++ alt c ++ " instead of '" ++ c ++ "' between test commands."
+
+    -- If the next word is a keyword, readNormalWord will trigger a warning
+    hasKeyword <- isFollowedBy readKeyword
+    hasWord <- isFollowedBy readNormalWord
+
+    when (hasWord && not (hasKeyword || hasDashAo)) $ do
+        -- We have other words following, and no error has been emitted.
+        posEnd <- getPosition
+        parseProblemAtWithEnd pos posEnd ErrorC 1140 "Unexpected parameters after condition. Missing &&/||, or bad expression?"
+
+    return $ T_Redirecting id redirs cmd
+  where
+    alt "or" = "||"
+    alt "-o" = "||"
+    alt "and" = "&&"
+    alt "-a" = "&&"
+    alt _ = "|| or &&"
+
 prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
 readCompoundCommand = do
     cmd <- choice [
@@ -2675,7 +2760,6 @@
         readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
             parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
         readSubshell,
-        readCondition,
         readWhileClause,
         readUntilClause,
         readIfClause,
@@ -2685,15 +2769,15 @@
         readBatsTest,
         readFunctionDefinition
         ]
-    spacing
     redirs <- many readIoRedirect
     id <- getNextIdSpanningTokenList (cmd:redirs)
-    unless (null redirs) $ optional $ do
-        lookAhead $ try (spacing >> needsSeparator)
-        parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."
+    optional . lookAhead $ do
+        notFollowedBy2 $ choice [readKeyword, g_Lbrace]
+        pos <- getPosition
+        many1 readNormalWord
+        posEnd <- getPosition
+        parseProblemAtWithEnd pos posEnd ErrorC 1141 "Unexpected tokens after compound command. Bad redirection or missing ;/&&/||/|?"
     return $ T_Redirecting id redirs cmd
-  where
-    needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]
 
 
 readCompoundList = readTerm
@@ -2763,17 +2847,13 @@
 
 prop_readAssignmentWord = isOk readAssignmentWord "a=42"
 prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"
-prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"
-prop_readAssignmentWord4 = isWarning readAssignmentWord "b = $(lol)"
 prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
-prop_readAssignmentWord6 = isWarning readAssignmentWord "b += (1 2 3)"
 prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
 prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
 prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
 prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
 prop_readAssignmentWord9b= isOk readAssignmentWord "foo=  "
 prop_readAssignmentWord9c= isOk readAssignmentWord "foo=  #bar"
-prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
 prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
 prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
 prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
@@ -2781,52 +2861,63 @@
 prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
 readAssignmentWord = readAssignmentWordExt True
 readWellFormedAssignment = readAssignmentWordExt False
-readAssignmentWordExt lenient = try $ do
-    start <- startSpan
-    pos <- getPosition
-    when lenient $
-        optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")
-    variable <- readVariableName
-    when lenient $
-        optional (readNormalDollar >> parseNoteAt pos ErrorC
-                                1067 "For indirection, use arrays, declare \"var$n=value\", or (for sh) read/eval.")
-    indices <- many readArrayIndex
-    hasLeftSpace <- fmap (not . null) spacing
-    pos <- getPosition
-    id <- endSpan start
-    op <- readAssignmentOp
+readAssignmentWordExt lenient = called "variable assignment" $ do
+    -- Parse up to and including the = in a 'try'
+    (id, variable, op, indices) <- try $ do
+        start <- startSpan
+        pos <- getPosition
+        -- Check for a leading $ at parse time, to warn for $foo=(bar) which
+        -- would otherwise cause a parse failure so it can't be checked later.
+        leadingDollarPos <-
+            if lenient
+            then optionMaybe $ getSpanPositionsFor (char '$')
+            else return Nothing
+        variable <- readVariableName
+        indices <- many readArrayIndex
+        hasLeftSpace <- fmap (not . null) spacing
+        opStart <- getPosition
+        id <- endSpan start
+        op <- readAssignmentOp
+        opEnd <- getPosition
+
+        when (isJust leadingDollarPos || hasLeftSpace) $ do
+            hasParen <- isFollowedBy (spacing >> char '(')
+            when hasParen $
+                sequence_ $ do
+                    (l, r) <- leadingDollarPos
+                    return $ parseProblemAtWithEnd l r ErrorC 1066 "Don't use $ on the left side of assignments."
+
+            -- Fail so that this is not parsed as an assignment.
+            fail ""
+        -- At this point we know for sure.
+        return (id, variable, op, indices)
+
+    rightPosStart <- getPosition
     hasRightSpace <- fmap (not . null) spacing
+    rightPosEnd <- getPosition
     isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
-    if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
+
+    if hasRightSpace || isEndOfCommand
       then do
-        when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
-            parseNoteAt pos WarningC 1007
+        when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ do
+            parseProblemAtWithEnd rightPosStart rightPosEnd WarningC 1007
                 "Remove space after = if trying to assign a value (for empty string, use var='' ... )."
         value <- readEmptyLiteral
         return $ T_Assignment id op variable indices value
       else do
-        when (hasLeftSpace || hasRightSpace) $
-            parseNoteAt pos ErrorC 1068 $
-                "Don't put spaces around the "
-                ++ (if op == Append
-                    then "+= when appending"
-                    else "= in assignments")
-                ++ " (or quote to make it literal)."
+        optional $ do
+            lookAhead $ char '='
+            parseProblem ErrorC 1097 "Unexpected ==. For assignment, use =. For comparison, use [/[[. Or quote for literal string."
+
         value <- readArray <|> readNormalWord
         spacing
         return $ T_Assignment id op variable indices value
   where
     readAssignmentOp = do
-        pos <- getPosition
-        unexpecting "" $ string "==="
+        -- This is probably some kind of ascii art border
+        unexpecting "===" (string "===")
         choice [
             string "+=" >> return Append,
-            do
-                try (string "==")
-                parseProblemAt pos ErrorC 1097
-                    "Unexpected ==. For assignment, use =. For comparison, use [/[[."
-                return Assign,
-
             string "=" >> return Assign
             ]
 
@@ -3093,7 +3184,7 @@
         let line = "line " ++ (show . sourceLine $ errorPos err)
             suggestion = getStringFromParsec $ errorMessages err
         in
-            "Failed to process " ++ filename ++ ", " ++ line ++ ": "
+            "Failed to process " ++ (e4m filename) ++ ", " ++ line ++ ": "
                 ++ suggestion
 
 prop_readConfigKVs1 = isOk readConfigKVs "disable=1234"
@@ -3114,6 +3205,7 @@
 prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
 prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
 prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
+prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"
 readScriptFile sourced = do
     start <- startSpan
     pos <- getPosition
@@ -3139,8 +3231,8 @@
     let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
 
     unless ignoreShebang $
-        verifyShebang pos (getShell shebangString)
-    if ignoreShebang || isValidShell (getShell shebangString) /= Just False
+        verifyShebang pos (executableFromShebang shebangString)
+    if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
       then do
             commands <- withAnnotations annotations readCompoundListOrEmpty
             id <- endSpan start
@@ -3154,16 +3246,6 @@
             return $ T_Script id shebang []
 
   where
-    basename s = reverse . takeWhile (/= '/') . reverse $ s
-    getShell sb =
-        case words sb of
-            [] -> ""
-            [x] -> basename x
-            (first:second:_) ->
-                if basename first == "env"
-                    then second
-                    else basename first
-
     verifyShebang pos s = do
         case isValidShell s of
             Just True -> return ()
@@ -3316,16 +3398,21 @@
                 prRoot = Just $
                     reattachHereDocs script (hereDocMap userstate)
             }
-        Left err ->
+        Left err -> do
+            let context = contextStack state
             return newParseResult {
                 prComments =
                     map toPositionedComment $
-                        notesForContext (contextStack state)
-                        ++ [makeErrorFor err]
+                        (filter (not . isIgnored context) $
+                            notesForContext context
+                            ++ [makeErrorFor err])
                         ++ parseProblems state,
                 prTokenPositions = Map.empty,
                 prRoot = Nothing
             }
+  where
+    -- A final pass for ignoring parse errors after failed parsing
+    isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
 
 notesForContext list = zipWith ($) [first, second] $ filter isName list
   where
diff --git a/test/shellcheck.hs b/test/shellcheck.hs
--- a/test/shellcheck.hs
+++ b/test/shellcheck.hs
@@ -4,6 +4,7 @@
 import System.Exit
 import qualified ShellCheck.Analytics
 import qualified ShellCheck.AnalyzerLib
+import qualified ShellCheck.ASTLib
 import qualified ShellCheck.Checker
 import qualified ShellCheck.Checks.Commands
 import qualified ShellCheck.Checks.Custom
@@ -17,6 +18,7 @@
     results <- sequence [
         ShellCheck.Analytics.runTests
         ,ShellCheck.AnalyzerLib.runTests
+        ,ShellCheck.ASTLib.runTests
         ,ShellCheck.Checker.runTests
         ,ShellCheck.Checks.Commands.runTests
         ,ShellCheck.Checks.Custom.runTests
