diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+## v0.10.0 - 2024-03-07
+### Added
+- Precompiled binaries for macOS ARM64 (darwin.aarch64)
+- Added support for busybox sh
+- Added flag --rcfile to specify an rc file by name.
+- Added `extended-analysis=true` directive to enable/disable dataflow analysis
+  (with a corresponding --extended-analysis flag).
+- SC2324: Warn when x+=1 appends instead of increments
+- SC2325: Warn about multiple `!`s in dash/sh.
+- SC2326: Warn about `foo | ! bar` in bash/dash/sh.
+- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
+- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
+- SC3014: Warn bashism `test _ == _` like in [ ]
+- SC3015: Warn bashism `test _ =~ _` like in [ ]
+- SC3016: Warn bashism `test -v _` like in [ ]
+- SC3017: Warn bashism `test -a _` like in [ ]
+
+### Fixed
+- source statements with here docs now work correctly
+- "(Array.!): undefined array element" error should no longer occur
+
+
 ## v0.9.0 - 2022-12-12
 ### Added
 - SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -77,7 +77,7 @@
 
 * Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
 
-* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
+* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar).
 
 * VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
 
@@ -112,6 +112,7 @@
 * [Code Factor](https://www.codefactor.io/)
 * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
 * [Github](https://github.com/features/actions) (only Linux)
+* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
 
 Most other services, including [GitLab](https://about.gitlab.com/), let you install
 ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@@ -193,6 +194,12 @@
 C:\> choco install shellcheck
 ```
 
+Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)):
+
+```cmd
+C:\> winget install --id koalaman.shellcheck
+```
+
 Or Windows (via [scoop](http://scoop.sh)):
 
 ```cmd
@@ -232,6 +239,9 @@
 or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
 (including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
 
+There are currently no official binaries for Apple Silicon, but third party builds are available via
+[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
+
 Distro packages already come with a `man` page. If you are building from source, it can be installed with:
 
 ```console
@@ -299,10 +309,6 @@
 
     $ cabal install
 
-Or if you intend to run the tests:
-
-    $ cabal install --enable-tests
-
 This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
 
 Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
@@ -548,4 +554,3 @@
 
 * The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
 * ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
-
diff --git a/ShellCheck.cabal b/ShellCheck.cabal
--- a/ShellCheck.cabal
+++ b/ShellCheck.cabal
@@ -1,5 +1,5 @@
 Name:             ShellCheck
-Version:          0.9.0
+Version:          0.10.0
 Synopsis:         Shell script analysis tool
 License:          GPL-3
 License-file:     LICENSE
@@ -46,21 +46,21 @@
         semigroups
     build-depends:
       -- The lower bounds are based on GHC 7.10.3
-      -- The upper bounds are based on GHC 9.4.3
-      aeson                >= 1.4.0 && < 2.2,
+      -- The upper bounds are based on GHC 9.8.1
+      aeson                >= 1.4.0 && < 2.3,
       array                >= 0.5.1 && < 0.6,
       base                 >= 4.8.0.0 && < 5,
-      bytestring           >= 0.10.6 && < 0.12,
-      containers           >= 0.5.6 && < 0.7,
-      deepseq              >= 1.4.1 && < 1.5,
-      Diff                 >= 0.4.0 && < 0.5,
-      fgl                  >= 5.7.0 && < 5.9,
+      bytestring           >= 0.10.6 && < 0.13,
+      containers           >= 0.5.6 && < 0.8,
+      deepseq              >= 1.4.1 && < 1.6,
+      Diff                 >= 0.4.0 && < 0.6,
+      fgl                  (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
       filepath             >= 1.4.0 && < 1.5,
-      mtl                  >= 2.2.2 && < 2.3,
+      mtl                  >= 2.2.2 && < 2.4,
       parsec               >= 3.1.14 && < 3.2,
       QuickCheck           >= 2.14.2 && < 2.15,
       regex-tdfa           >= 1.2.0 && < 1.4,
-      transformers         >= 0.4.2 && < 0.6,
+      transformers         >= 0.4.2 && < 0.7,
 
       -- getXdgDirectory from 1.2.3.0
       directory            >= 1.2.3 && < 1.4,
diff --git a/shellcheck.1.md b/shellcheck.1.md
--- a/shellcheck.1.md
+++ b/shellcheck.1.md
@@ -56,6 +56,13 @@
     options are cumulative, but all the codes can be specified at once,
     comma-separated as a single argument.
 
+**--extended-analysis=true/false**
+
+:   Enable/disable Dataflow Analysis to identify more issues (default true). If
+    ShellCheck uses too much CPU/RAM when checking scripts with several
+    thousand lines of code, extended analysis can be disabled with this flag
+    or a directive. This flag overrides directives and rc files.
+
 **-f** *FORMAT*, **--format=***FORMAT*
 
 :   Specify the output format of shellcheck, which prints its results in the
@@ -71,6 +78,11 @@
 
 :   Don't try to look for .shellcheckrc configuration files.
 
+--rcfile\ RCFILE
+
+:   Prefer the specified configuration file over searching for one
+    in the default locations.
+
 **-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
 
 :   Enable optional checks. The special name *all* enables all of them.
@@ -85,7 +97,8 @@
 
 **-s**\ *shell*,\ **--shell=***shell*
 
-:   Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
+:   Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
+    and *busybox*.
     The default is to deduce the shell from the file's `shell` directive,
     shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
     POSIX `sh` (not the system's), and will warn of portability issues.
@@ -243,6 +256,12 @@
 :   Enable an optional check by name, as listed with **--list-optional**.
     Only file-wide `enable` directives are considered.
 
+**extended-analysis**
+:   Set to true/false to enable/disable dataflow analysis. Specifying
+    `# shellcheck extended-analysis=false` in particularly large (2000+ line)
+    auto-generated scripts will reduce ShellCheck's resource usage at the
+    expense of certain checks. Extended analysis is enabled by default.
+
 **external-sources**
 :   Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
     arbitrary files from 'source' statements (the way most tools do).
@@ -378,7 +397,7 @@
 
 # COPYRIGHT
 
-Copyright 2012-2022, Vidar Holen and contributors.
+Copyright 2012-2024, Vidar Holen and contributors.
 Licensed under the GNU General Public License version 3 or later,
 see https://gnu.org/licenses/gpl.html
 
diff --git a/shellcheck.hs b/shellcheck.hs
--- a/shellcheck.hs
+++ b/shellcheck.hs
@@ -76,7 +76,8 @@
     externalSources  :: Bool,
     sourcePaths      :: [FilePath],
     formatterOptions :: FormatterOptions,
-    minSeverity      :: Severity
+    minSeverity      :: Severity,
+    rcfile           :: Maybe FilePath
 }
 
 defaultOptions = Options {
@@ -86,7 +87,8 @@
     formatterOptions = newFormatterOptions {
         foColorOption = ColorAuto
     },
-    minSeverity = StyleC
+    minSeverity = StyleC,
+    rcfile = Nothing
 }
 
 usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@@ -100,6 +102,8 @@
         (ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
     Option "e" ["exclude"]
         (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
+    Option "" ["extended-analysis"]
+        (ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)",
     Option "f" ["format"]
         (ReqArg (Flag "format") "FORMAT") $
         "Output format (" ++ formatList ++ ")",
@@ -107,6 +111,9 @@
         (NoArg $ Flag "list-optional" "true") "List checks disabled by default",
     Option "" ["norc"]
         (NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
+    Option "" ["rcfile"]
+        (ReqArg (Flag "rcfile") "RCFILE")
+        "Prefer the specified configuration file over searching for one",
     Option "o" ["enable"]
         (ReqArg (Flag "enable") "check1,check2..")
         "List of optional checks to enable (or 'all')",
@@ -115,7 +122,7 @@
         "Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
     Option "s" ["shell"]
         (ReqArg (Flag "shell") "SHELLNAME")
-        "Specify dialect (sh, bash, dash, ksh)",
+        "Specify dialect (sh, bash, dash, ksh, busybox)",
     Option "S" ["severity"]
         (ReqArg (Flag "severity") "SEVERITY")
         "Minimum severity of errors to consider (error, warning, info, style)",
@@ -252,9 +259,9 @@
                 else SomeProblems
 
 parseEnum name value list =
-    case filter ((== value) . fst) list of
-        [(name, value)] -> return value
-        [] -> do
+    case lookup value list of
+        Just value -> return value
+        Nothing -> do
             printErr $ "Unknown value for --" ++ name ++ ". " ++
                        "Valid options are: " ++ (intercalate ", " $ map fst list)
             throwError SupportFailure
@@ -367,6 +374,11 @@
                 }
             }
 
+        Flag "rcfile" str -> do
+            return options {
+                rcfile = Just str
+            }
+
         Flag "enable" value ->
             let cs = checkSpec options in return options {
                 checkSpec = cs {
@@ -374,6 +386,14 @@
                 }
             }
 
+        Flag "extended-analysis" str -> do
+            value <- parseBool str
+            return options {
+                checkSpec = (checkSpec options) {
+                    csExtendedAnalysis = Just value
+                }
+            }
+
         -- This flag is handled specially in 'process'
         Flag "format" _ -> return options
 
@@ -391,12 +411,20 @@
             throwError SyntaxFailure
         return (Prelude.read num :: Integer)
 
+    parseBool str = do
+        case str of
+            "true" -> return True
+            "false" -> return False
+            _ -> do
+                printErr $ "Invalid boolean, expected true/false: " ++ str
+                throwError SyntaxFailure
+
 ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
 ioInterface options files = do
     inputs <- mapM normalize files
     cache <- newIORef emptyCache
     configCache <- newIORef ("", Nothing)
-    return SystemInterface {
+    return (newSystemInterface :: SystemInterface IO) {
         siReadFile = get cache inputs,
         siFindSource = findSourceFile inputs (sourcePaths options),
         siGetConfig = getConfig configCache
@@ -441,19 +469,34 @@
         fallback :: FilePath -> IOException -> IO FilePath
         fallback path _ = return path
 
+
     -- Returns the name and contents of .shellcheckrc for the given file
-    getConfig cache filename = do
-        path <- normalize filename
-        let dir = takeDirectory path
-        (previousPath, result) <- readIORef cache
-        if dir == previousPath
-          then return result
-          else do
-            paths <- getConfigPaths dir
-            result <- findConfig paths
-            writeIORef cache (dir, result)
-            return result
+    getConfig cache filename =
+        case rcfile options of
+            Just file -> do
+                -- We have a specified rcfile. Ignore normal rcfile resolution.
+                (path, result) <- readIORef cache
+                if path == "/"
+                  then return result
+                  else do
+                    result <- readConfig file
+                    when (isNothing result) $
+                        hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file
+                    writeIORef cache ("/", result)
+                    return result
 
+            Nothing -> do
+                path <- normalize filename
+                let dir = takeDirectory path
+                (previousPath, result) <- readIORef cache
+                if dir == previousPath
+                  then return result
+                  else do
+                    paths <- getConfigPaths dir
+                    result <- findConfig paths
+                    writeIORef cache (dir, result)
+                    return result
+
     findConfig paths =
         case paths of
             (file:rest) -> do
@@ -490,7 +533,7 @@
       where
         handler :: FilePath -> IOException -> IO (String, Bool)
         handler file err = do
-            putStrLn $ file ++ ": " ++ show err
+            hPutStrLn stderr $ file ++ ": " ++ show err
             return ("", True)
 
     andM a b arg = do
diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs
--- a/src/ShellCheck/AST.hs
+++ b/src/ShellCheck/AST.hs
@@ -152,6 +152,7 @@
     | ShellOverride String
     | SourcePath String
     | ExternalSources Bool
+    | ExtendedAnalysis Bool
     deriving (Show, Eq)
 data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
 
diff --git a/src/ShellCheck/ASTLib.hs b/src/ShellCheck/ASTLib.hs
--- a/src/ShellCheck/ASTLib.hs
+++ b/src/ShellCheck/ASTLib.hs
@@ -31,6 +31,7 @@
 import Data.Functor.Identity
 import Data.List
 import Data.Maybe
+import qualified Data.List.NonEmpty as NE
 import qualified Data.Map as Map
 import Numeric (showHex)
 
@@ -157,9 +158,10 @@
         _ -> False
 
 -- Is this token a flag where the - is unquoted?
-isUnquotedFlag token = fromMaybe False $ do
-    str <- getLeadingUnquotedString token
-    return $ "-" `isPrefixOf` str
+isUnquotedFlag token =
+    case getLeadingUnquotedString token of
+        Just ('-':_) -> True
+        _ -> False
 
 -- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
 --     -re -d : -u 3 bar
@@ -758,8 +760,8 @@
 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"
+prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
+prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
 
 -- Get the shell executable from a string like '/usr/bin/env bash'
 executableFromShebang :: String -> String
@@ -776,7 +778,8 @@
             [x] -> basename x
             (first:second:args) | basename first == "busybox" ->
                 case basename second of
-                   "sh" -> "ash" -- busybox sh is ash
+                   "sh" -> "busybox sh"
+                   "ash" -> "busybox ash"
                    x -> x
             (first:args) | basename first == "env" ->
                 fromEnvArgs args
@@ -856,8 +859,7 @@
 -- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
 prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
 getIndexReferences s = fromMaybe [] $ do
-    match <- matchRegex re s
-    index <- match !!! 0
+    index:_ <- matchRegex re s
     return $ matchAllStrings variableNameRegex index
   where
     re = mkRegex "(\\[.*\\])"
@@ -868,8 +870,7 @@
 prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
 getOffsetReferences mods = fromMaybe [] $ do
 -- if mods start with [, then drop until ]
-    match <- matchRegex re mods
-    offsets <- match !!! 1
+    _:offsets:_ <- matchRegex re mods
     return $ matchAllStrings variableNameRegex offsets
   where
     re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
@@ -886,11 +887,17 @@
             in getBracedReference str == str
         _ -> False
 
+-- Return the referenced variable if (and only if) it's an unmodified parameter expansion.
+getUnmodifiedParameterExpansion t =
+    case t of
+        T_DollarBraced _ _ list -> do
+            let str = concat $ oversimplify list
+            guard $ getBracedReference str == str
+            return str
+        _ -> Nothing
+
 --- A list of the element and all its parents up to the root node.
-getPath tree t = t :
-    case Map.lookup (getId t) tree of
-        Nothing     -> []
-        Just parent -> getPath tree parent
+getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
 
 isClosingFileOp op =
     case op of
@@ -902,6 +909,12 @@
     case root of
         T_Annotation _ list _ -> [s | EnableComment s <- list]
         _ -> []
+
+getExtendedAnalysisDirective :: Token -> Maybe Bool
+getExtendedAnalysisDirective root =
+    case root of
+        T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list]
+        _ -> Nothing
 
 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
@@ -1,5 +1,5 @@
 {-
-    Copyright 2012-2022 Vidar Holen
+    Copyright 2012-2024 Vidar Holen
 
     This file is part of ShellCheck.
     https://www.shellcheck.net
@@ -19,6 +19,7 @@
 -}
 {-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE PatternGuards #-}
 module ShellCheck.Analytics (checker, optionalChecks, ShellCheck.Analytics.runTests) where
 
 import ShellCheck.AST
@@ -46,6 +47,7 @@
 import Data.Ord
 import Data.Semigroup
 import Debug.Trace -- STRIP
+import qualified Data.List.NonEmpty as NE
 import qualified Data.Map.Strict as Map
 import qualified Data.Set as S
 import Test.QuickCheck.All (forAllProperties)
@@ -201,6 +203,7 @@
     ,checkOverwrittenExitCode
     ,checkUnnecessaryArithmeticExpansionIndex
     ,checkUnnecessaryParens
+    ,checkPlusEqualsNumber
     ]
 
 optionalChecks = map fst optionalTreeChecks
@@ -343,13 +346,11 @@
 hasFloatingPoint params = shellType params == Ksh
 
 -- Checks whether the current parent path is part of a condition
-isCondition [] = False
-isCondition [_] = False
-isCondition (child:parent:rest) =
-    case child of
-        T_BatsTest {} -> True -- count anything in a @test as conditional
-        _ -> getId child `elem` map getId (getConditionChildren parent) || isCondition (parent:rest)
+isCondition (x NE.:| xs) = foldr go (const False) xs x
   where
+    go _ _ T_BatsTest{} = True -- count anything in a @test as conditional
+    go parent go_rest child =
+        getId child `elem` map getId (getConditionChildren parent) || go_rest parent
     getConditionChildren t =
         case t of
             T_AndIf _ left right -> [left]
@@ -466,9 +467,8 @@
   where
     isCommonCommand (Just s) = s `elem` commonCommands
     isCommonCommand _ = False
-    firstWordIsArg list = fromMaybe False $ do
-        head <- list !!! 0
-        return $ isGlob head || isUnquotedFlag head
+    firstWordIsArg (head:_) = isGlob head || isUnquotedFlag head
+    firstWordIsArg [] = False
 
 checkAssignAteCommand _ _ = return ()
 
@@ -489,9 +489,7 @@
 checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ val] []) =
   sequence_ $ do
     str <- getNormalString val
-    match <- matchRegex regex str
-    var <- match !!! 0
-    op <- match !!! 1
+    var:op:_ <- matchRegex regex str
     Map.lookup var references
     return . warn (getId val) 2100 $
         "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
@@ -562,7 +560,7 @@
                 hasParameter "print0",
                 hasParameter "printf"
               ]) $ warn (getId find) 2038
-                      "Use -print0/-0 or -exec + to allow for non-alphanumeric filenames."
+                      "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames."
 
     for ["ps", "grep"] $
         \(ps:grep:_) ->
@@ -645,10 +643,10 @@
 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_checkShebang13 = verifyNotTree 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_checkShebang16 = verifyNotTree 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) =
@@ -845,14 +843,14 @@
     getRedirs _ = []
     special x = "/dev/" `isPrefixOf` concat (oversimplify x)
     isInput t =
-        case drop 1 $ getPath (parentMap params) t of
+        case NE.tail $ getPath (parentMap params) t of
             T_IoFile _ op _:_ ->
                 case op of
                     T_Less _  -> True
                     _ -> False
             _ -> False
     isOutput t =
-        case drop 1 $ getPath (parentMap params) t of
+        case NE.tail $ getPath (parentMap params) t of
             T_IoFile _ op _:_ ->
                 case op of
                     T_Greater _  -> True
@@ -1086,7 +1084,7 @@
         return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else if name == "mumps" then getMumpsCommand cmd else name
 
     isProbablyOk =
-            any isOkAssignment (take 3 $ getPath parents t)
+            any isOkAssignment (NE.take 3 $ getPath parents t)
             || commandName `elem` [
                 "trap"
                 ,"sh"
@@ -1203,6 +1201,7 @@
             case shellType params of
                 Sh -> return ()  -- These are unsupported and will be caught by bashism checks.
                 Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting."
+                BusyboxSh -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting."
                 _ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])."
 
     when (op `elem` arithmeticBinaryTestOps) $ do
@@ -1263,7 +1262,8 @@
                     str = concat $ oversimplify c
                     var = getBracedReference str
                 in fromMaybe False $ do
-                    state <- CF.getIncomingState (cfgAnalysis params) id
+                    cfga <- cfgAnalysis params
+                    state <- CF.getIncomingState cfga id
                     value <- Map.lookup var $ CF.variablesInScope state
                     return $ CF.numericalStatus (CF.variableValue value) >= CF.NumericalStatusMaybe
             _ ->
@@ -1441,14 +1441,14 @@
 prop_checkConstantNullary6 = verify checkConstantNullary "[ 1 ]"
 prop_checkConstantNullary7 = verify checkConstantNullary "[ false ]"
 checkConstantNullary _ (TC_Nullary _ _ t) | isConstant t =
-    case fromMaybe "" $ getLiteralString t of
+    case onlyLiteralString t of
         "false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets."
         "0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead."
         "true" -> style (getId t) 2160 "Instead of '[ true ]', just use 'true'."
         "1" -> style (getId t) 2161 "Instead of '[ 1 ]', use 'true'."
         _ -> err (getId t) 2078 "This expression is constant. Did you forget a $ somewhere?"
   where
-    string = fromMaybe "" $ getLiteralString t
+    string = onlyLiteralString t
 
 checkConstantNullary _ _ = return ()
 
@@ -1457,9 +1457,8 @@
 prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
 checkForDecimals params t@(TA_Expansion id _) = sequence_ $ do
     guard $ not (hasFloatingPoint params)
-    str <- getLiteralString t
-    first <- str !!! 0
-    guard $ isDigit first && '.' `elem` str
+    first:rest <- getLiteralString t
+    guard $ isDigit first && '.' `elem` rest
     return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
 checkForDecimals _ _ = return ()
 
@@ -1493,7 +1492,7 @@
   where
     isException [] = True
     isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h
-    getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t
+    getWarning = fromMaybe noWarning . msum . NE.map warningFor $ parents params t
     warningFor t =
         case t of
             T_Arithmetic {} -> return normalWarning
@@ -1821,7 +1820,7 @@
             T_Literal id s
                 | not (quotesSingleThing a && quotesSingleThing b
                     || s `elem` ["=", ":", "/"]
-                    || isSpecial (getPath (parentMap params) trapped)
+                    || isSpecial (NE.toList $ getPath (parentMap params) trapped)
                 ) ->
                     warnAboutLiteral id
             _ -> return ()
@@ -2039,7 +2038,7 @@
 -- 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:_) : _ ->
+        _ NE.:| T_NormalWord parentId (me:T_Literal _ ('=':_):_) : T_SimpleCommand _ _ (cmd:_) : _ ->
             (getId t) == (getId me) && (parentId == getId cmd)
         _ -> False
 
@@ -2134,7 +2133,8 @@
                     addDoubleQuotesAround params token
 
   where
-    name = getBracedReference $ concat $ oversimplify list
+    bracedString = concat $ oversimplify list
+    name = getBracedReference bracedString
     parents = parentMap params
     needsQuoting =
               not (isArrayExpansion token) -- There's another warning for this
@@ -2144,7 +2144,8 @@
               && not (usedAsCommandName parents token)
 
     isClean = fromMaybe False $ do
-        state <- CF.getIncomingState (cfgAnalysis params) id
+        cfga <- cfgAnalysis params
+        state <- CF.getIncomingState cfga id
         value <- Map.lookup name $ CF.variablesInScope state
         return $ isCleanState value
 
@@ -2153,14 +2154,10 @@
         || CF.spaceStatus (CF.variableValue state) == CF.SpaceStatusClean
 
     isDefaultAssignment parents token =
-        let modifier = getBracedModifier $ bracedString token in
+        let modifier = getBracedModifier bracedString 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 $ pleaseReport "bracedString on non-variable"
-
 checkSpacefulnessCfg' _ _ _ = return ()
 
 
@@ -2277,7 +2274,7 @@
             (Just str, t) -> do
                 let name = basename str
                 let args = skipOver t argv
-                let argStrings = map (\x -> (fromMaybe "" $ getLiteralString x, x)) args
+                let argStrings = map (\x -> (onlyLiteralString x, x)) args
                 let candidates = getPotentialCommands name argStrings
                 mapM_ (checkArg name (getId t)) candidates
             _ -> return ()
@@ -2653,7 +2650,7 @@
     check path
   where
     name = getBracedReference $ concat $ oversimplify value
-    path = getPath (parentMap params) t
+    path = NE.toList $ getPath (parentMap params) t
     idPath = map getId path
 
     check [] = return ()
@@ -2702,7 +2699,7 @@
         return $ isCommandMatch cmd (`elem` ["tr", "read"])
 
     -- Check if this is a dereferencing context like [[ -v array[operandhere] ]]
-    isDereferenced = fromMaybe False . msum . map isDereferencingOp . getPath (parentMap p)
+    isDereferenced = fromMaybe False . msum . NE.map isDereferencingOp . getPath (parentMap p)
     isDereferencingOp t =
         case t of
             TC_Binary _ DoubleBracket str _ _ -> return $ isDereferencingBinaryOp str
@@ -2753,19 +2750,18 @@
 prop_checkLoopKeywordScope6 = verify checkLoopKeywordScope "while true; do true | { break; }; done"
 prop_checkLoopKeywordScope7 = verifyNot checkLoopKeywordScope "#!/bin/ksh\nwhile true; do true | { break; }; done"
 checkLoopKeywordScope params t |
-        name `elem` map Just ["continue", "break"] =
-    if not $ any isLoop path
-    then if any isFunction $ take 1 path
-        -- breaking at a source/function invocation is an abomination. Let's ignore it.
-        then err (getId t) 2104 $ "In functions, use return instead of " ++ fromJust name ++ "."
-        else err (getId t) 2105 $ fromJust name ++ " is only valid in loops."
-    else case map subshellType $ filter (not . isFunction) path of
+        Just name <- getCommandName t, name `elem` ["continue", "break"] =
+    if any isLoop path
+    then case map subshellType $ filter (not . isFunction) path of
         Just str:_ -> warn (getId t) 2106 $
             "This only exits the subshell caused by the " ++ str ++ "."
         _ -> return ()
+    else case path of
+        -- breaking at a source/function invocation is an abomination. Let's ignore it.
+        h:_ | isFunction h -> err (getId t) 2104 $ "In functions, use return instead of " ++ name ++ "."
+        _ -> err (getId t) 2105 $ name ++ " is only valid in loops."
   where
-    name = getCommandName t
-    path = let p = getPath (parentMap params) t in filter relevant p
+    path = let p = getPath (parentMap params) t in NE.filter relevant p
     subshellType t = case leadType params t of
         NoneScope -> Nothing
         SubshellScope str -> return str
@@ -2784,6 +2780,7 @@
             when (hasKeyword && hasParens) $
                 err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."
         Dash -> forSh
+        BusyboxSh -> forSh
         Sh   -> forSh
 
     where
@@ -2825,13 +2822,11 @@
     execWriter $ mapM_ warnForGroup referenceGroups
   where
     functionMap :: Map.Map String Token
-    functionMap = Map.fromList $
-        map (\t@(T_Function _ _ _ name _) -> (name,t)) functions
-    functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
+    functionMap = Map.fromList $ execWriter $ doAnalysis (tell . maybeToList . findFunction) root
 
     findFunction t@(T_Function id _ _ name body)
         | any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
-        = return t
+        = return (name,t)
         where flow = getVariableFlow params body
     findFunction _ = Nothing
 
@@ -3219,7 +3214,7 @@
         return $ do
             warn (getId token) 2165 "This nested loop overrides the index variable of its parent."
             warn (getId next)  2167 "This parent loop has its index variable overridden."
-    path = drop 1 $ getPath (parentMap params) token
+    path = NE.tail $ getPath (parentMap params) token
     loopVariable :: Token -> Maybe String
     loopVariable t =
         case t of
@@ -3292,17 +3287,17 @@
     -- We don't want to warn about composite expressions like
     -- [[ $? -eq 0 || $? -eq 4 ]] since these can be annoying to rewrite.
     isOnlyTestInCommand t =
-        case getPath (parentMap params) t of
-            _:(T_Condition {}):_ -> True
-            _:(T_Arithmetic {}):_ -> True
-            _:(TA_Sequence _ [_]):(T_Arithmetic {}):_ -> True
+        case NE.tail $ getPath (parentMap params) t of
+            (T_Condition {}):_ -> True
+            (T_Arithmetic {}):_ -> True
+            (TA_Sequence _ [_]):(T_Arithmetic {}):_ -> True
 
             -- Some negations and groupings are also fine
-            _:next@(TC_Unary _ _ "!" _):_ -> isOnlyTestInCommand next
-            _:next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next
-            _:next@(TC_Group {}):_ -> isOnlyTestInCommand next
-            _:next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next
-            _:next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next
+            next@(TC_Unary _ _ "!" _):_ -> isOnlyTestInCommand next
+            next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next
+            next@(TC_Group {}):_ -> isOnlyTestInCommand next
+            next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next
+            next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next
             _ -> False
 
     -- TODO: Do better $? tracking and filter on whether
@@ -3322,7 +3317,7 @@
 
     isFirstCommandInFunction = fromMaybe False $ do
         let path = getPath (parentMap params) token
-        func <- listToMaybe $ filter isFunction path
+        func <- find isFunction path
         cmd <- getClosestCommand (parentMap params) token
         return $ getId cmd == getId (getFirstCommandInFunction func)
 
@@ -3367,7 +3362,7 @@
         _ -> return ()
   where
     isInExpansion t =
-        case drop 1 $ getPath (parentMap params) t of
+        case NE.tail $ getPath (parentMap params) t of
             T_DollarExpansion _ [_] : _ -> True
             T_Backticked _ [_] : _ -> True
             t@T_Annotation {} : _ -> isInExpansion t
@@ -3841,7 +3836,7 @@
 
     isFunctionBody path =
         case path of
-            (_:f:_) -> isFunction f
+            (_ NE.:| f:_) -> isFunction f
             _ -> False
 
     isTestStructure t =
@@ -3868,7 +3863,7 @@
     -- This technically also triggers for `if true; then ( test ); fi`
     -- but it's still a valid suggestion.
     isCompoundCondition chain =
-        case dropWhile skippable (drop 1 chain) of
+        case dropWhile skippable (NE.tail chain) of
             T_IfExpression {}    : _ -> True
             T_WhileExpression {} : _ -> True
             T_UntilExpression {} : _ -> True
@@ -4031,7 +4026,7 @@
 
     isFunctionBody t =
         case getPath (parentMap params) t of
-            _:T_Function {}:_-> True
+            _ NE.:| T_Function {}:_-> True
             _ -> False
 
     dropLast t =
@@ -4046,7 +4041,8 @@
 prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))"
 prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n"
 prop_checkModifiedArithmeticInRedirection6 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/dash\nls > $((i=i+1))"
-checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash) $
+prop_checkModifiedArithmeticInRedirection7 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/busybox sh\ncat << foo\n$((i++))\nfoo\n"
+checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash || shellType params == BusyboxSh) $
     case t of
         T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs
         _ -> return ()
@@ -4358,6 +4354,7 @@
             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."
+            BusyboxSh -> err id 2279 "$0 can't be assigned in Busybox Ash. 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."
@@ -4380,9 +4377,9 @@
         return $ isVariableName str
 
     isLeadingNumberVar s =
-        let lead = takeWhile (/= '=') s
-        in not (null lead) && isDigit (head lead)
-            && all isVariableChar lead && not (all isDigit lead)
+        case takeWhile (/= '=') s of
+            lead@(x:_) -> isDigit x && all isVariableChar lead && not (all isDigit lead)
+            [] -> False
 
     msg cmd leading (T_Literal litId s) = do
         -- There are many different cases, and the order of the branches matter.
@@ -4512,7 +4509,7 @@
 checkCommandWithTrailingSymbol _ t =
     case t of
         T_SimpleCommand _ _ (cmd:_) ->
-            let str = fromJust $ getLiteralStringExt (\_ -> Just "x") cmd
+            let str = getLiteralStringDef "x" cmd
                 last = lastOrDefault 'x' str
             in
                 case str of
@@ -4627,7 +4624,8 @@
             -- Is this one of the 'for' arrays?
             (loopWord, _) <- find ((==arrayName) . snd) arrays
             -- Are we still in this loop?
-            guard $ getId loop `elem` map getId (getPath parents t)
+            let loopId = getId loop
+            guard $ any (\t -> loopId == getId t) (getPath parents t)
             return [
                 makeComment WarningC (getId loopWord) 2302 "This loops over values. To loop over keys, use \"${!array[@]}\".",
                 makeComment WarningC (getId arrayRef) 2303 $ (e4m name) ++ " is an array value, not a key. Use directly or loop over keys instead."
@@ -4709,7 +4707,7 @@
         literalArg <- getUnquotedLiteral cmd
         Map.lookup literalArg functions_
 
-    checkCmd cmd = go $ getPath (parentMap params) cmd
+    checkCmd cmd = go $ NE.toList $ getPath (parentMap params) cmd
       where
         go (child:parent:rest) = do
             case parent of
@@ -4823,15 +4821,15 @@
         ++ "separately to avoid masking its return value (or use '|| true' "
         ++ "to ignore).")
 
-    isMaskDeliberate t = hasParent isOrIf t
+    isMaskDeliberate t = any isOrIf $ NE.init $ parents params t
       where
-        isOrIf _ (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd]))
+        isOrIf (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd]))
             = getCommandBasename cmd `elem` [Just "true", Just ":"]
-        isOrIf _ _ = False
+        isOrIf _ = False
 
-    isCheckedElsewhere t = hasParent isDeclaringCommand t
+    isCheckedElsewhere t = any isDeclaringCommand $ NE.tail $ parents params t
       where
-        isDeclaringCommand t _ = fromMaybe False $ do
+        isDeclaringCommand t = fromMaybe False $ do
             cmd <- getCommand t
             basename <- getCommandBasename cmd
             return $
@@ -4851,16 +4849,7 @@
             ,"shopt"
             ]
 
-    isTransparentCommand t = fromMaybe False $ do
-        basename <- getCommandBasename t
-        return $ basename == "time"
-
-    parentChildPairs t = go $ parents params t
-      where
-        go (child:parent:rest) = (parent, child):go (parent:rest)
-        go _ = []
-
-    hasParent pred t = any (uncurry pred) (parentChildPairs t)
+    isTransparentCommand t = getCommandBasename t == Just "time"
 
 
 -- hard error on negated command that is not last
@@ -4909,7 +4898,8 @@
 checkCommandIsUnreachable params t =
     case t of
         T_Pipeline {} -> sequence_ $ do
-            state <- CF.getIncomingState (cfgAnalysis params) id
+            cfga <- cfgAnalysis params
+            state <- CF.getIncomingState cfga id
             guard . not $ CF.stateIsReachable state
             guard . not $ isSourced params t
             return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."
@@ -4931,14 +4921,15 @@
         _ -> return ()
   where
     check id = sequence_ $ do
-        state <- CF.getIncomingState (cfgAnalysis params) id
+        cfga <- cfgAnalysis params
+        state <- CF.getIncomingState cfga id
         let exitCodeIds = CF.exitCodes state
         guard . not $ S.null exitCodeIds
 
         let idToToken = idMap params
-        exitCodeTokens <- sequence $ map (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds
+        exitCodeTokens <- traverse (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds
         return $ do
-            when (all isCondition exitCodeTokens && not (usedUnconditionally t exitCodeIds)) $
+            when (all isCondition exitCodeTokens && not (usedUnconditionally cfga t exitCodeIds)) $
                 warn id 2319 "This $? refers to a condition, not a command. Assign to a variable to avoid it being overwritten."
             when (all isPrinting exitCodeTokens) $
                 warn id 2320 "This $? refers to echo/printf, not a previous command. Assign to variable to avoid it being overwritten."
@@ -4951,8 +4942,8 @@
 
     -- If we don't do anything based on the condition, assume we wanted the condition itself
     -- This helps differentiate `x; [ $? -gt 0 ] && exit $?` vs `[ cond ]; exit $?`
-    usedUnconditionally t testIds =
-        all (\c -> CF.doesPostDominate (cfgAnalysis params) (getId t) c) testIds
+    usedUnconditionally cfga t testIds =
+        all (\c -> CF.doesPostDominate cfga (getId t) c) testIds
 
     isPrinting t =
         case getCommandBasename t of
@@ -5008,6 +4999,44 @@
             replaceStart id params 1 "", -- Remove "("
             replaceEnd id params 1 ""    -- Remove ")"
         ]
+
+
+prop_checkPlusEqualsNumber1 = verify checkPlusEqualsNumber "x+=1"
+prop_checkPlusEqualsNumber2 = verify checkPlusEqualsNumber "x+=42"
+prop_checkPlusEqualsNumber3 = verifyNot checkPlusEqualsNumber "(( x += 1 ))"
+prop_checkPlusEqualsNumber4 = verifyNot checkPlusEqualsNumber "declare -i x=0; x+=1"
+prop_checkPlusEqualsNumber5 = verifyNot checkPlusEqualsNumber "x+='1'"
+prop_checkPlusEqualsNumber6 = verifyNot checkPlusEqualsNumber "n=foo; x+=n"
+prop_checkPlusEqualsNumber7 = verify checkPlusEqualsNumber "n=4; x+=n"
+prop_checkPlusEqualsNumber8 = verify checkPlusEqualsNumber "n=4; x+=$n"
+prop_checkPlusEqualsNumber9 = verifyNot checkPlusEqualsNumber "declare -ia var; var[x]+=1"
+checkPlusEqualsNumber params t =
+    case t of
+        T_Assignment id Append var _ word -> sequence_ $ do
+            cfga <- cfgAnalysis params
+            state <- CF.getIncomingState cfga id
+            guard $ isNumber state word
+            guard . not $ fromMaybe False $ CF.variableMayBeDeclaredInteger state var
+            return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), declare -i var, or quote number to silence."
+        _ -> return ()
+
+  where
+    isNumber state word =
+        let
+            unquotedLiteral = getUnquotedLiteral word
+            isEmpty = unquotedLiteral == Just ""
+            isUnquotedNumber = not isEmpty && maybe False (all isDigit) unquotedLiteral
+            isNumericalVariableName = fromMaybe False $ do
+                str <- unquotedLiteral
+                CF.variableMayBeAssignedInteger state str
+            isNumericalVariableExpansion =
+                case word of
+                    T_NormalWord _ [part] -> fromMaybe False $ do
+                        str <- getUnmodifiedParameterExpansion part
+                        CF.variableMayBeAssignedInteger state str
+                    _ -> False
+        in
+            isUnquotedNumber || isNumericalVariableName || isNumericalVariableExpansion
 
 
 return []
diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs
--- a/src/ShellCheck/AnalyzerLib.hs
+++ b/src/ShellCheck/AnalyzerLib.hs
@@ -41,6 +41,7 @@
 import Data.List
 import Data.Maybe
 import Data.Semigroup
+import qualified Data.List.NonEmpty as NE
 import qualified Data.Map as Map
 
 import Test.QuickCheck.All (forAllProperties)
@@ -103,7 +104,7 @@
     -- map from token id to start and end position
     tokenPositions     :: Map.Map Id (Position, Position),
     -- Result from Control Flow Graph analysis (including data flow analysis)
-    cfgAnalysis :: CF.CFGAnalysis
+    cfgAnalysis :: Maybe CF.CFGAnalysis
     } deriving (Show)
 
 -- TODO: Cache results of common AST ops here
@@ -196,8 +197,10 @@
         }
     in force withFix
 
+-- makeParameters :: CheckSpec -> Parameters
 makeParameters spec = params
   where
+    extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
     params = Parameters {
         rootNode = root,
         shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
@@ -206,18 +209,21 @@
             case shellType params of
                 Bash -> isOptionSet "lastpipe" root
                 Dash -> False
+                BusyboxSh -> False
                 Sh   -> False
                 Ksh  -> True,
         hasInheritErrexit =
             case shellType params of
                 Bash -> isOptionSet "inherit_errexit" root
                 Dash -> True
+                BusyboxSh -> True
                 Sh   -> True
                 Ksh  -> False,
         hasPipefail =
             case shellType params of
                 Bash -> isOptionSet "pipefail" root
                 Dash -> True
+                BusyboxSh -> isOptionSet "pipefail" root
                 Sh   -> True
                 Ksh  -> isOptionSet "pipefail" root,
         shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
@@ -225,7 +231,9 @@
         parentMap = getParentTree root,
         variableFlow = getVariableFlow params root,
         tokenPositions = asTokenPositions spec,
-        cfgAnalysis = CF.analyzeControlFlow cfParams root
+        cfgAnalysis = do
+            guard extendedAnalysis
+            return $ CF.analyzeControlFlow cfParams root
     }
     cfParams = CF.CFGParameters {
         CF.cfLastpipe = hasLastpipe params,
@@ -284,8 +292,8 @@
 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
+prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
+prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
 
 determineShellTest = determineShellTest' Nothing
 determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@@ -333,14 +341,14 @@
 
 isQuoteFreeNode strict shell tree t =
     isQuoteFreeElement t ||
-        (fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t)
+        (fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t)
   where
     -- Is this node self-quoting in itself?
     isQuoteFreeElement t =
         case t of
-            T_Assignment {} -> assignmentIsQuoting t
-            T_FdRedirect {} -> True
-            _               -> False
+            T_Assignment id _ _ _ _ -> assignmentIsQuoting id
+            T_FdRedirect {}         -> True
+            _                       -> False
 
     -- Are any subnodes inherently self-quoting?
     isQuoteFreeContext t =
@@ -350,7 +358,7 @@
             TC_Binary _ DoubleBracket _ _ _ -> return True
             TA_Sequence {}                  -> return True
             T_Arithmetic {}                 -> return True
-            T_Assignment {}                 -> return $ assignmentIsQuoting t
+            T_Assignment id _ _ _ _         -> return $ assignmentIsQuoting id
             T_Redirecting {}                -> return False
             T_DoubleQuoted _ _              -> return True
             T_DollarDoubleQuoted _ _        -> return True
@@ -365,11 +373,11 @@
     -- Check whether this assignment is self-quoting due to being a recognized
     -- assignment passed to a Declaration Utility. This will soon be required
     -- by POSIX: https://austingroupbugs.net/view.php?id=351
-    assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
+    assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
     shellParsesParamsAsAssignments = shell /= Sh
 
     -- Is this assignment a parameter to a command like export/typeset/etc?
-    isAssignmentParamToCommand (T_Assignment id _ _ _ _) =
+    isAssignmentParamToCommand id =
         case Map.lookup id tree of
             Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
             _ -> False
@@ -395,7 +403,7 @@
 -- Get the parent command (T_Redirecting) of a Token, if any.
 getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
 getClosestCommand tree t =
-    findFirst findCommand $ getPath tree t
+    findFirst findCommand $ NE.toList $ getPath tree t
   where
     findCommand t =
         case t of
@@ -409,7 +417,7 @@
     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)
+usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
   where
     go currentId (T_NormalWord id [word]:rest)
         | currentId == getId word = go id rest
@@ -426,7 +434,9 @@
     return $ getPath (parentMap params) t
 
 isParentOf tree parent child =
-    elem (getId parent) . map getId $ getPath tree child
+    any (\t -> parentId == getId t) (getPath tree child)
+  where
+    parentId = getId parent
 
 parents params = getPath (parentMap params)
 
@@ -810,7 +820,7 @@
             return (context, token, getBracedReference str)
 
     isArithmeticAssignment t = case getPath parents t of
-        this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
+        this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
         _                                  -> False
 
 isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
@@ -899,6 +909,7 @@
         Bash -> True
         Ksh -> True
         Dash -> False
+        BusyboxSh -> False
         Sh -> False
 
 isTrueAssignmentSource c =
diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs
--- a/src/ShellCheck/CFG.hs
+++ b/src/ShellCheck/CFG.hs
@@ -51,6 +51,7 @@
 import Data.Array.Unboxed
 import Data.Array.ST
 import Data.List hiding (map)
+import qualified Data.List.NonEmpty as NE
 import Data.Maybe
 import qualified Data.Map as M
 import qualified Data.Set as S
@@ -111,8 +112,8 @@
 
 -- Actions we track
 data CFEffect =
-    CFSetProps Scope String (S.Set CFVariableProp)
-    | CFUnsetProps Scope String (S.Set CFVariableProp)
+    CFSetProps (Maybe Scope) String (S.Set CFVariableProp)
+    | CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp)
     | CFReadVariable String
     | CFWriteVariable String CFValue
     | CFWriteGlobal String CFValue
@@ -192,7 +193,7 @@
                     base
 
         idToRange = M.fromList mapping
-        isRealEdge (from, to, edge) = case edge of CFEFlow -> True; _ -> False
+        isRealEdge (from, to, edge) = case edge of CFEFlow -> True; CFEExit -> True; _ -> False
         onlyRealEdges = filter isRealEdge edges
         (_, mainExit) = fromJust $ M.lookup (getId root) idToRange
 
@@ -578,7 +579,7 @@
 
         T_Array _ list -> sequentially list
 
-        T_Assignment {} -> buildAssignment DefaultScope t
+        T_Assignment {} -> buildAssignment Nothing t
 
         T_Backgrounded id body -> do
             start <- newStructuralNode
@@ -614,15 +615,15 @@
 
         T_CaseExpression id t [] -> build t
 
-        T_CaseExpression id t list -> do
+        T_CaseExpression id t list@(hd:tl) -> do
             start <- newStructuralNode
             token <- build t
-            branches <- mapM buildBranch list
+            branches <- mapM buildBranch (hd NE.:| tl)
             end <- newStructuralNode
 
-            let neighbors = zip branches $ tail branches
-            let (_, firstCond, _) = head branches
-            let (_, lastCond, lastBody) = last branches
+            let neighbors = zip (NE.toList branches) $ NE.tail branches
+            let (_, firstCond, _) = NE.head branches
+            let (_, lastCond, lastBody) = NE.last branches
 
             linkRange start token
             linkRange token firstCond
@@ -857,8 +858,8 @@
             status <- newNodeRange (CFSetExitCode id)
             linkRange assignments status
 
-        T_SimpleCommand id vars list@(cmd:_) ->
-            handleCommand t vars list $ getUnquotedLiteral cmd
+        T_SimpleCommand id vars (cmd:args) ->
+            handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd
 
         T_SingleQuoted _ _ -> none
 
@@ -887,7 +888,9 @@
         T_Less _ -> none
         T_ParamSubSpecialChar _ _ -> none
 
-        x -> error ("Unimplemented: " ++ show x)
+        x -> do
+            error ("Unimplemented: " ++ show x) -- STRIP
+            none
 
 --  Still in `where` clause
     forInHelper id name words body = do
@@ -923,8 +926,8 @@
     -- TODO: Handle assignments in declaring commands
 
     case literalCmd of
-        Just "exit" -> regularExpansion vars args $ handleExit
-        Just "return" -> regularExpansion vars args $ handleReturn
+        Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit
+        Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn
         Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args
 
         Just "declare" -> handleDeclare args
@@ -947,14 +950,14 @@
         -- This will mostly behave like 'command' but ok
         Just "builtin" ->
             case args of
-                [_] -> regular
-                (_:newargs@(newcmd:_)) ->
-                    handleCommand newcmd vars newargs $ getLiteralString newcmd
+                _ NE.:| [] -> regular
+                (_ NE.:| newcmd:newargs) ->
+                    handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd
         Just "command" ->
             case args of
-                [_] -> regular
-                (_:newargs@(newcmd:_)) ->
-                    handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd
+                _ NE.:| [] -> regular
+                (_ NE.:| newcmd:newargs) ->
+                    handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd
         _ -> regular
 
   where
@@ -982,7 +985,7 @@
                 unreachable <- newNode CFUnreachable
                 return $ Range ret unreachable
 
-    handleUnset (cmd:args) = do
+    handleUnset (cmd NE.:| args) = do
         case () of
                 _ | "n" `elem` flagNames -> unsetWith CFUndefineNameref
                 _ | "v" `elem` flagNames -> unsetWith CFUndefineVariable
@@ -994,14 +997,14 @@
         (names, flags) = partition (null . fst) pairs
         flagNames = map fst flags
         literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")]
-        literalNames = mapMaybe (\(_, t) -> getLiteralString t >>= (return . (,) t)) names
+        literalNames = mapMaybe (\(_, t) -> (,) t <$> getLiteralString t) names
         -- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id
         unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames
 
 
     variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)="
 
-    handleDeclare (cmd:args) = do
+    handleDeclare (cmd NE.:| args) = do
         isFunc <- asks cfIsFunction
         -- This is a bit of a kludge: we don't have great support for things like
         -- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x
@@ -1028,9 +1031,9 @@
 
         scope isFunc =
             case () of
-                _ | global -> GlobalScope
-                _ | isFunc -> LocalScope
-                _ -> DefaultScope
+                _ | global -> Just GlobalScope
+                _ | isFunc -> Just LocalScope
+                _ -> Nothing
 
         addedProps = S.fromList $ concat $ [
             [ CFVPArray | array ],
@@ -1058,7 +1061,7 @@
             let
                 id = getId t
                 pre = [t]
-                literal = fromJust $ getLiteralStringExt (const $ Just "\0") t
+                literal = getLiteralStringDef "\0" t
                 isKnown = '\0' `notElem` literal
                 match = fmap head $ variableAssignRegex `matchRegex` literal
                 name = fromMaybe literal match
@@ -1090,7 +1093,7 @@
             in
                 concatMap (drop 1) plusses
 
-    handlePrintf (cmd:args) =
+    handlePrintf (cmd NE.:| args) =
         newNodeRange $ CFApplyEffects $ maybeToList findVar
       where
         findVar = do
@@ -1099,7 +1102,7 @@
             name <- getLiteralString arg
             return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString
 
-    handleWait (cmd:args) =
+    handleWait (cmd NE.:| args) =
         newNodeRange $ CFApplyEffects $ maybeToList findVar
       where
         findVar = do
@@ -1108,7 +1111,7 @@
             name <- getLiteralString arg
             return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger
 
-    handleMapfile (cmd:args) =
+    handleMapfile (cmd NE.:| args) =
         newNodeRange $ CFApplyEffects [findVar]
       where
         findVar =
@@ -1128,7 +1131,7 @@
             guard $ isVariableName name
             return (getId c, name)
 
-    handleRead (cmd:args) = newNodeRange $ CFApplyEffects main
+    handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main
       where
         main = fromMaybe fallback $ do
             flags <- getGnuOpts flagsForRead args
@@ -1158,7 +1161,7 @@
             in
                 map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault
 
-    handleDEFINE (cmd:args) =
+    handleDEFINE (cmd NE.:| args) =
         newNodeRange $ CFApplyEffects $ maybeToList findVar
       where
         findVar = do
@@ -1168,14 +1171,14 @@
             return $ IdTagged (getId name) $ CFWriteVariable str CFValueString
 
     handleOthers id vars args cmd =
-        regularExpansion vars args $ do
+        regularExpansion vars (NE.toList args) $ do
             exe <- newNodeRange $ CFExecuteCommand cmd
             status <- newNodeRange $ CFSetExitCode id
             linkRange exe status
 
     regularExpansion vars args p = do
             args <- sequentially args
-            assignments <- mapM (buildAssignment PrefixScope) vars
+            assignments <- mapM (buildAssignment (Just PrefixScope)) vars
             exe <- p
             dropAssignments <-
                 if null vars
@@ -1187,15 +1190,15 @@
 
             linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments
 
-    regularExpansionWithStatus vars args@(cmd:_) p = do
-        initial <- regularExpansion vars args p
+    regularExpansionWithStatus vars args@(cmd NE.:| _) p = do
+        initial <- regularExpansion vars (NE.toList args) p
         status <- newNodeRange $ CFSetExitCode (getId cmd)
         linkRange initial status
 
 
 none = newStructuralNode
 
-data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope
+data Scope = GlobalScope | LocalScope | PrefixScope
   deriving (Eq, Ord, Show, Generic, NFData)
 
 buildAssignment scope t = do
@@ -1209,10 +1212,10 @@
                 let valueType = if null indices then f id value else CFValueArray
                 let scoper =
                                 case scope of
-                                    PrefixScope -> CFWritePrefix
-                                    LocalScope -> CFWriteLocal
-                                    GlobalScope -> CFWriteGlobal
-                                    DefaultScope -> CFWriteVariable
+                                    Just PrefixScope -> CFWritePrefix
+                                    Just LocalScope -> CFWriteLocal
+                                    Just GlobalScope -> CFWriteGlobal
+                                    Nothing -> CFWriteVariable
                 write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType
                 linkRanges [expand, index, read, write]
               where
@@ -1301,7 +1304,10 @@
     reversed = grev withExitEdges
     postDoms = dom reversed mainexit
     (_, maxNode) = nodeRange graph
-    asArray = array (0, maxNode) postDoms
+    -- Holes in the array cause "Exception: (Array.!): undefined array element" while
+    -- inspecting/debugging, so fill the array first and then update.
+    initializedArray = listArray (0, maxNode) $ repeat []
+    asArray = initializedArray // postDoms
 
 return []
 runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/src/ShellCheck/CFGAnalysis.hs b/src/ShellCheck/CFGAnalysis.hs
--- a/src/ShellCheck/CFGAnalysis.hs
+++ b/src/ShellCheck/CFGAnalysis.hs
@@ -59,6 +59,8 @@
     ,getIncomingState
     ,getOutgoingState
     ,doesPostDominate
+    ,variableMayBeDeclaredInteger
+    ,variableMayBeAssignedInteger
     ,ShellCheck.CFGAnalysis.runTests -- STRIP
     ) where
 
@@ -153,6 +155,20 @@
     (targetStart, _) <- M.lookup target $ tokenToRange analysis
     return $ targetStart `elem` (postDominators analysis ! baseEnd)
 
+-- See if any execution path results in the variable containing a state
+variableMayHaveState :: ProgramState -> String -> CFVariableProp -> Maybe Bool
+variableMayHaveState state var property = do
+    value <- M.lookup var $ variablesInScope state
+    return $ any (S.member property) $ variableProperties value
+
+-- See if any execution path declares the variable an integer (declare -i).
+variableMayBeDeclaredInteger state var = variableMayHaveState state var CFVPInteger
+
+-- See if any execution path suggests the variable may contain an integer value
+variableMayBeAssignedInteger state var = do
+    value <- M.lookup var $ variablesInScope state
+    return $ (numericalStatus $ variableValue value) >= NumericalStatusMaybe
+
 getDataForNode analysis node = M.lookup node $ nodeToData analysis
 
 -- The current state of data flow at a point in the program, potentially as a diff
@@ -283,7 +299,6 @@
                     PrefixScope -> (sPrefixValues, insertPrefix)
                     LocalScope -> (sLocalValues, insertLocal)
                     GlobalScope -> (sGlobalValues, insertGlobal)
-                    DefaultScope -> error $ pleaseReport "Unresolved scope in dependency"
 
             alreadyExists = isJust $ vmLookup name $ mapToCheck state
         in
@@ -814,7 +829,7 @@
     f (s:rest) = do
         -- Go up the stack until we find the value, and add
         -- a dependency on each state (including where it was found)
-        res <- fromMaybe (f rest) (return <$> get (stackState s) key)
+        res <- maybe (f rest) return (get (stackState s) key)
         modifySTRef (dependencies s) $ S.insert $ dep key res
         return res
 
@@ -1104,34 +1119,34 @@
 
         CFSetProps scope name props ->
             case scope of
-                DefaultScope -> do
+                Nothing -> do
                     state <- readVariable ctx name
                     writeVariable ctx name $ addProperties props state
-                GlobalScope -> do
+                Just GlobalScope -> do
                     state <- readGlobal ctx name
                     writeGlobal ctx name $ addProperties props state
-                LocalScope -> do
+                Just LocalScope -> do
                     out <- readSTRef (cOutput ctx)
                     state <- readLocal ctx name
                     writeLocal ctx name $ addProperties props state
-                PrefixScope -> do
+                Just PrefixScope -> do
                     -- Prefix values become local
                     state <- readLocal ctx name
                     writeLocal ctx name $ addProperties props state
 
         CFUnsetProps scope name props ->
             case scope of
-                DefaultScope -> do
+                Nothing -> do
                     state <- readVariable ctx name
                     writeVariable ctx name $ removeProperties props state
-                GlobalScope -> do
+                Just GlobalScope -> do
                     state <- readGlobal ctx name
                     writeGlobal ctx name $ removeProperties props state
-                LocalScope -> do
+                Just LocalScope -> do
                     out <- readSTRef (cOutput ctx)
                     state <- readLocal ctx name
                     writeLocal ctx name $ removeProperties props state
-                PrefixScope -> do
+                Just PrefixScope -> do
                     -- Prefix values become local
                     state <- readLocal ctx name
                     writeLocal ctx name $ removeProperties props state
diff --git a/src/ShellCheck/Checker.hs b/src/ShellCheck/Checker.hs
--- a/src/ShellCheck/Checker.hs
+++ b/src/ShellCheck/Checker.hs
@@ -25,6 +25,7 @@
 import ShellCheck.Interface
 import ShellCheck.Parser
 
+import Debug.Trace -- DO NOT SUBMIT
 import Data.Either
 import Data.Functor
 import Data.List
@@ -86,6 +87,7 @@
                     asCheckSourced = csCheckSourced spec,
                     asExecutionMode = Executed,
                     asTokenPositions = tokenPositions,
+                    asExtendedAnalysis = csExtendedAnalysis spec,
                     asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
                 } where as = newAnalysisSpec root
         let analysisMessages =
@@ -506,6 +508,56 @@
   where
     result = checkWithRc "disable=1104" emptyCheckSpec {
         csScript = "!/bin/bash\necho 'hello world'"
+    }
+
+prop_sourceWithHereDocWorks = null result
+  where
+    result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
+
+prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
+  where
+    result = check "cat << eof"
+
+prop_hereDocsWillHaveParsedIndices = null result
+  where
+    result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
+
+prop_rcCanSuppressDfa = null result
+  where
+    result = checkWithRc "extended-analysis=false" emptyCheckSpec {
+        csScript = "#!/bin/sh\nexit; foo;"
+    }
+
+prop_fileCanSuppressDfa = null $ traceShowId result
+  where
+    result = checkWithRc "" emptyCheckSpec {
+        csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
+    }
+
+prop_fileWinsWhenSuppressingDfa1 = null result
+  where
+    result = checkWithRc "extended-analysis=true" emptyCheckSpec {
+        csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
+    }
+
+prop_fileWinsWhenSuppressingDfa2 = result == [2317]
+  where
+    result = checkWithRc "extended-analysis=false" emptyCheckSpec {
+        csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
+    }
+
+prop_flagWinsWhenSuppressingDfa1 = result == [2317]
+  where
+    result = checkWithRc "extended-analysis=false" emptyCheckSpec {
+        csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
+        csExtendedAnalysis = Just True
+    }
+
+prop_flagWinsWhenSuppressingDfa2 = null result
+  where
+    result = checkWithRc "extended-analysis=true" emptyCheckSpec {
+        csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
+        csExtendedAnalysis = Just False
     }
 
 return []
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
@@ -20,6 +20,7 @@
 {-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE PatternGuards #-}
 
 -- This module contains checks that examine specific commands by name.
 module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
@@ -42,6 +43,7 @@
 import qualified Data.Graph.Inductive.Graph as G
 import Data.List
 import Data.Maybe
+import qualified Data.List.NonEmpty as NE
 import qualified Data.Map.Strict as M
 import qualified Data.Set as S
 import Test.QuickCheck.All (forAllProperties)
@@ -181,16 +183,15 @@
 checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
     name <- getLiteralString cmd
     return $
-        if '/' `elem` name
-        then
-            M.findWithDefault nullCheck (Basename $ basename name) map t
-        else if name == "builtin" && not (null rest) then
-            let t' = T_SimpleCommand id cmdPrefix rest
-                selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
-            in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
-        else do
-            M.findWithDefault nullCheck (Exactly name) map t
-            M.findWithDefault nullCheck (Basename name) map t
+        if | '/' `elem` name ->
+               M.findWithDefault nullCheck (Basename $ basename name) map t
+           | name == "builtin", (h:_) <- rest ->
+               let t' = T_SimpleCommand id cmdPrefix rest
+                   selectedBuiltin = onlyLiteralString h
+               in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
+           | otherwise -> do
+               M.findWithDefault nullCheck (Exactly name) map t
+               M.findWithDefault nullCheck (Basename name) map t
 
   where
     basename = reverse . takeWhile (/= '/') . reverse
@@ -299,7 +300,7 @@
                     "'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
 
             [first, second] |
-                (fromMaybe "" $ getLiteralString first) /= "length"
+                onlyLiteralString first /= "length"
                   && not (willSplit first || willSplit second) -> do
                     checkOp first
                     warn (getId t) 2307
@@ -930,7 +931,7 @@
 prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
 checkTimedCommand = CommandCheck (Exactly "time") f where
     f (T_SimpleCommand _ _ (c:args@(_:_))) =
-        whenShell [Sh, Dash] $ do
+        whenShell [Sh, Dash, BusyboxSh] $ do
             let cmd = last args -- "time" is parsed with a command as argument
             when (isPiped cmd) $
                 warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
@@ -954,7 +955,7 @@
 prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
 prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
 checkLocalScope = CommandCheck (Exactly "local") $ \t ->
-    whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
+    whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local
         path <- getPathM t
         unless (any isFunctionLike path) $
             err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
@@ -1005,8 +1006,8 @@
         sequence_ $ do
             options <- getLiteralString arg1
             getoptsVar <- getLiteralString name
-            (T_WhileExpression _ _ body) <- findFirst whileLoop path
-            caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0
+            (T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path)
+            T_CaseExpression id var list <- mapMaybe findCase body !!! 0
 
             -- Make sure getopts name and case variable matches
             [T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
@@ -1016,11 +1017,11 @@
             -- Make sure the variable isn't modified
             guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
 
-            return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
+            return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list
     f _ = return ()
 
-    check :: Id -> [String] -> Token -> Analysis
-    check optId opts (T_CaseExpression id _ list) = do
+    check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis
+    check optId opts id list = do
             unless (Nothing `M.member` handledMap) $ do
                 mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
 
@@ -1236,8 +1237,7 @@
   where
     f t = sequence_ $ do
         opts <- parseOpts $ arguments t
-        let nonFlags = [x | ("",(x, _)) <- opts]
-        commandArg <- nonFlags !!! 0
+        (_,(commandArg, _)) <- find (null . fst) opts
         command <- getLiteralString commandArg
         guard $ command `elem` builtins
         return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
@@ -1430,26 +1430,28 @@
 prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
 checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
   where
-    check t = foldM_ perArg M.empty $ arguments t
+    check t = do
+        cfga <- asks cfgAnalysis
+        when (isJust cfga) $
+            foldM_ (perArg $ fromJust cfga) M.empty $ arguments t
 
-    perArg leftArgs t =
+    perArg cfga leftArgs t =
         case t of
             T_Assignment id _ name idx t -> do
-                warnIfBackreferencing leftArgs $ t:idx
+                warnIfBackreferencing cfga leftArgs $ t:idx
                 return $ M.insert name id leftArgs
             t -> do
-                warnIfBackreferencing leftArgs [t]
+                warnIfBackreferencing cfga leftArgs [t]
                 return leftArgs
 
-    warnIfBackreferencing backrefs l = do
-        references <- findReferences l
+    warnIfBackreferencing cfga backrefs l = do
+        references <- findReferences cfga l
         let reused = M.intersection backrefs references
         mapM msg $ M.toList reused
 
     msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
 
-    findReferences list = do
-        cfga <- asks cfgAnalysis
+    findReferences cfga list = do
         let graph = CF.graph cfga
         let nodesMap = CF.tokenToNodes cfga
         let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list
diff --git a/src/ShellCheck/Checks/ControlFlow.hs b/src/ShellCheck/Checks/ControlFlow.hs
--- a/src/ShellCheck/Checks/ControlFlow.hs
+++ b/src/ShellCheck/Checks/ControlFlow.hs
@@ -78,7 +78,7 @@
 runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
 runNodeChecks perNode = do
     cfg <- asks cfgAnalysis
-    runOnAll cfg
+    sequence_ $ runOnAll <$> cfg
   where
     getData datas n@(node, label) = do
         (pre, post) <- M.lookup node datas
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
@@ -19,6 +19,7 @@
 -}
 {-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE ViewPatterns #-}
 module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
 
 import ShellCheck.AST
@@ -60,6 +61,8 @@
     ,checkBraceExpansionVars
     ,checkMultiDimensionalArrays
     ,checkPS1Assignments
+    ,checkMultipleBangs
+    ,checkBangAfterPipe
     ]
 
 testChecker (ForShell _ t) =
@@ -73,12 +76,11 @@
 prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
 prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
 prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
-checkForDecimals = ForShell [Sh, Dash, Bash] f
+checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
   where
     f t@(TA_Expansion id _) = sequence_ $ do
-        str <- getLiteralString t
-        first <- str !!! 0
-        guard $ isDigit first && '.' `elem` str
+        first:rest <- getLiteralString t
+        guard $ isDigit first && '.' `elem` rest
         return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
     f _ = return ()
 
@@ -89,6 +91,9 @@
 prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
 prop_checkBashisms5 = verify checkBashisms "source file"
 prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
+prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42"
+prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]"
+prop_checkBashisms6d = verify checkBashisms "test foo =~ bar"
 prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
 prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
 prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
@@ -104,6 +109,7 @@
 prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
 prop_checkBashisms20 = verify checkBashisms "read -ra foo"
 prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
+prop_checkBashisms21b = verify checkBashisms "test -a foo"
 prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
 prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
 prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
@@ -184,18 +190,42 @@
 prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
 prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
 prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
-checkBashisms = ForShell [Sh, Dash] $ \t -> do
+prop_checkBashisms100 = verify checkBashisms "read -r"
+prop_checkBashisms101 = verify checkBashisms "read"
+prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
+prop_checkBashisms103 = verifyNot checkBashisms "read foo"
+prop_checkBashisms104 = verifyNot checkBashisms "read ''"
+prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
+prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
+prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
+prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
+prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
+prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
+prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
+prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
+prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
+prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
+prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
+prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
+prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
+prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
+prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
+prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
+prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
+checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
     params <- ask
     kludge params t
  where
   -- This code was copy-pasted from Analytics where params was a variable
   kludge params = bashism
    where
-    isDash = shellType params == Dash
+    isBusyboxSh = shellType params == BusyboxSh
+    isDash = shellType params == Dash || isBusyboxSh
     warnMsg id code s =
         if isDash
         then err  id code $ "In dash, " ++ s ++ " not supported."
         else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
+    asStr = getLiteralString
 
     bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
     bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
@@ -206,27 +236,43 @@
     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_Condition id DoubleBracket _) =
+        unless isBusyboxSh $ 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 3012 $ "lexicographical " ++ op ++ " is"
+    bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
+        | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
+            unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
     bashism (TC_Binary id SingleBracket op _ _)
         | op `elem` [ "-ot", "-nt", "-ef" ] =
             unless isDash $ warnMsg id 3013 $ op ++ " is"
+    bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
+        | op `elem` [ "-ot", "-nt", "-ef" ] =
+            unless isDash $ warnMsg id 3013 $ op ++ " is"
     bashism (TC_Binary id SingleBracket "==" _ _) =
-            warnMsg id 3014 "== in place of = is"
+        unless isBusyboxSh $ warnMsg id 3014 "== in place of = is"
+    bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "==", rhs]) =
+        unless isBusyboxSh $ warnMsg id 3014 "== in place of = is"
     bashism (TC_Binary id SingleBracket "=~" _ _) =
             warnMsg id 3015 "=~ regex matching is"
+    bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "=~", rhs]) =
+            warnMsg id 3015 "=~ regex matching is"
     bashism (TC_Unary id SingleBracket "-v" _) =
             warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
+    bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-v", _]) =
+            warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
     bashism (TC_Unary id _ "-a" _) =
             warnMsg id 3017 "unary -a in place of -e is"
+    bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-a", _]) =
+            warnMsg id 3017 "unary -a in place of -e is"
     bashism (TA_Unary id op _)
         | op `elem` [ "|++", "|--", "++|", "--|"] =
             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_Greater _) _)) =
+        unless isBusyboxSh $ warnMsg id 3020 "&> is"
     bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
         unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
     bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
@@ -246,7 +292,8 @@
         warnMsg id 3028 $ str ++ " is"
 
     bashism t@(T_DollarBraced id _ token) = do
-        mapM_ check expansion
+        unless isBusyboxSh $ mapM_ check simpleExpansions
+        mapM_ check advancedExpansions
         when (isBashVariable var) $
             warnMsg id 3028 $ var ++ " is"
       where
@@ -356,7 +403,8 @@
                     (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
                 return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
 
-            when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
+            when (name == "source" && not isBusyboxSh) $
+                warnMsg id 3046 "'source' in place of '.' is"
             when (name == "trap") $
                 let
                     check token = sequence_ $ do
@@ -365,7 +413,7 @@
                         return $ do
                             when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
                                 warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
-                            when ("SIG" `isPrefixOf` upper) $
+                            when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
                                 warnMsg (getId token) 3048
                                     "prefixing signal names with 'SIG' is"
                             when (not isDash && upper /= str) $
@@ -379,6 +427,9 @@
                 let literal = onlyLiteralString format
                 guard $ "%q" `isInfixOf` literal
                 return $ warnMsg (getId format) 3050 "printf %q is"
+
+            when (name == "read" && all isFlag rest) $
+                warnMsg (getId cmd) 3061 "read without a variable is"
       where
         unsupportedCommands = [
             "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
@@ -402,7 +453,9 @@
             ("wait", Just [])
             ]
     bashism t@(T_SourceCommand id src _)
-        | getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
+      | getCommandName src == Just "source" =
+          unless isBusyboxSh $
+            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
@@ -410,14 +463,16 @@
     bashism _ = return ()
 
     varChars="_0-9a-zA-Z"
-    expansion = let re = mkRegex in [
+    advancedExpansions = let re = mkRegex in [
         (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 ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
+        ]
+    simpleExpansions = let re = mkRegex in [
         (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 = [
@@ -557,6 +612,30 @@
     enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
     escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
 
+
+prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
+prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
+checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
+  where
+    f token = case token of
+        T_Banged id (T_Banged _ _) ->
+            err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1."
+        _ -> return ()
+
+
+prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
+prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
+prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
+checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
+  where
+    f token = case token of
+        T_Pipeline _ _ cmds -> mapM_ check cmds
+        _ -> return ()
+
+    check token = case token of
+        T_Banged id _ ->
+            err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
+        _ -> return ()
 
 return []
 runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs
--- a/src/ShellCheck/Data.hs
+++ b/src/ShellCheck/Data.hs
@@ -156,6 +156,9 @@
         "sh"    -> return Sh
         "bash"  -> return Bash
         "bats"  -> return Bash
+        "busybox"  -> return BusyboxSh -- Used for directives and --shell=busybox
+        "busybox sh"  -> return BusyboxSh
+        "busybox ash"  -> return BusyboxSh
         "dash"  -> return Dash
         "ash"   -> return Dash -- There's also a warning for this.
         "ksh"   -> return Ksh
diff --git a/src/ShellCheck/Formatter/CheckStyle.hs b/src/ShellCheck/Formatter/CheckStyle.hs
--- a/src/ShellCheck/Formatter/CheckStyle.hs
+++ b/src/ShellCheck/Formatter/CheckStyle.hs
@@ -24,8 +24,8 @@
 
 import Data.Char
 import Data.List
-import GHC.Exts
 import System.IO
+import qualified Data.List.NonEmpty as NE
 
 format :: IO Formatter
 format = return Formatter {
@@ -45,12 +45,12 @@
     else mapM_ outputGroup fileGroups
   where
     comments = crComments cr
-    fileGroups = groupWith sourceFile comments
+    fileGroups = NE.groupWith sourceFile comments
     outputGroup group = do
-        let filename = sourceFile (head group)
+        let filename = sourceFile (NE.head group)
         result <- siReadFile sys (Just True) filename
         let contents = either (const "") id result
-        outputFile filename contents group
+        outputFile filename contents (NE.toList group)
 
 outputFile filename contents warnings = do
     let comments = makeNonVirtual warnings contents
diff --git a/src/ShellCheck/Formatter/GCC.hs b/src/ShellCheck/Formatter/GCC.hs
--- a/src/ShellCheck/Formatter/GCC.hs
+++ b/src/ShellCheck/Formatter/GCC.hs
@@ -23,8 +23,8 @@
 import ShellCheck.Formatter.Format
 
 import Data.List
-import GHC.Exts
 import System.IO
+import qualified Data.List.NonEmpty as NE
 
 format :: IO Formatter
 format = return Formatter {
@@ -39,13 +39,13 @@
 outputAll cr sys = mapM_ f groups
   where
     comments = crComments cr
-    groups = groupWith sourceFile comments
-    f :: [PositionedComment] -> IO ()
+    groups = NE.groupWith sourceFile comments
+    f :: NE.NonEmpty PositionedComment -> IO ()
     f group = do
-        let filename = sourceFile (head group)
+        let filename = sourceFile (NE.head group)
         result <- siReadFile sys (Just True) filename
         let contents = either (const "") id result
-        outputResult filename contents group
+        outputResult filename contents (NE.toList group)
 
 outputResult filename contents warnings = do
     let comments = makeNonVirtual warnings contents
diff --git a/src/ShellCheck/Formatter/JSON1.hs b/src/ShellCheck/Formatter/JSON1.hs
--- a/src/ShellCheck/Formatter/JSON1.hs
+++ b/src/ShellCheck/Formatter/JSON1.hs
@@ -27,9 +27,9 @@
 import Data.Aeson
 import Data.IORef
 import Data.Monoid
-import GHC.Exts
 import System.IO
 import qualified Data.ByteString.Lazy.Char8 as BL
+import qualified Data.List.NonEmpty as NE
 
 format :: IO Formatter
 format = do
@@ -114,10 +114,10 @@
 collectResult ref cr sys = mapM_ f groups
   where
     comments = crComments cr
-    groups = groupWith sourceFile comments
-    f :: [PositionedComment] -> IO ()
+    groups = NE.groupWith sourceFile comments
+    f :: NE.NonEmpty PositionedComment -> IO ()
     f group = do
-        let filename = sourceFile (head group)
+        let filename = sourceFile (NE.head group)
         result <- siReadFile sys (Just True) filename
         let contents = either (const "") id result
         let comments' = makeNonVirtual comments contents
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
@@ -31,9 +31,9 @@
 import Data.IORef
 import Data.List
 import Data.Maybe
-import GHC.Exts
 import System.IO
 import System.Info
+import qualified Data.List.NonEmpty as NE
 
 wikiLink = "https://www.shellcheck.net/wiki/"
 
@@ -117,19 +117,19 @@
     color <- getColorFunc $ foColorOption options
     let comments = crComments result
     appendComments ref comments (fromIntegral $ foWikiLinkCount options)
-    let fileGroups = groupWith sourceFile comments
+    let fileGroups = NE.groupWith sourceFile comments
     mapM_ (outputForFile color sys) fileGroups
 
 outputForFile color sys comments = do
-    let fileName = sourceFile (head comments)
+    let fileName = sourceFile (NE.head comments)
     result <- siReadFile sys (Just True) fileName
     let contents = either (const "") id result
     let fileLinesList = lines contents
     let lineCount = length fileLinesList
     let fileLines = listArray (1, lineCount) fileLinesList
-    let groups = groupWith lineNo comments
+    let groups = NE.groupWith lineNo comments
     forM_ groups $ \commentsForLine -> do
-        let lineNum = fromIntegral $ lineNo (head commentsForLine)
+        let lineNum = fromIntegral $ lineNo (NE.head commentsForLine)
         let line = if lineNum < 1 || lineNum > lineCount
                         then ""
                         else fileLines ! fromIntegral lineNum
@@ -139,7 +139,7 @@
         putStrLn (color "source" line)
         forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
         putStrLn ""
-        showFixedString color commentsForLine (fromIntegral lineNum) fileLines
+        showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines
 
 -- 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
@@ -1,5 +1,5 @@
 {-
-    Copyright 2012-2019 Vidar Holen
+    Copyright 2012-2024 Vidar Holen
 
     This file is part of ShellCheck.
     https://www.shellcheck.net
@@ -21,14 +21,14 @@
 module ShellCheck.Interface
     (
     SystemInterface(..)
-    , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
+    , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks)
     , CheckResult(crFilename, crComments)
     , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
     , ParseResult(prComments, prTokenPositions, prRoot)
-    , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
+    , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
     , AnalysisResult(arComments)
     , FormatterOptions(foColorOption, foWikiLinkCount)
-    , Shell(Ksh, Sh, Bash, Dash)
+    , Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
     , ExecutionMode(Executed, Sourced)
     , ErrorMessage
     , Code
@@ -39,11 +39,12 @@
     , ColorOption(ColorAuto, ColorAlways, ColorNever)
     , TokenComment(tcId, tcComment, tcFix)
     , emptyCheckResult
-    , newParseResult
-    , newAnalysisSpec
     , newAnalysisResult
+    , newAnalysisSpec
     , newFormatterOptions
+    , newParseResult
     , newPosition
+    , newSystemInterface
     , newTokenComment
     , mockedSystemInterface
     , mockRcFile
@@ -99,6 +100,7 @@
     csIncludedWarnings :: Maybe [Integer],
     csShellTypeOverride :: Maybe Shell,
     csMinSeverity :: Severity,
+    csExtendedAnalysis :: Maybe Bool,
     csOptionalChecks :: [String]
 } deriving (Show, Eq)
 
@@ -123,6 +125,7 @@
     csIncludedWarnings = Nothing,
     csShellTypeOverride = Nothing,
     csMinSeverity = StyleC,
+    csExtendedAnalysis = Nothing,
     csOptionalChecks = []
 }
 
@@ -135,6 +138,14 @@
     psShellTypeOverride = Nothing
 }
 
+newSystemInterface :: Monad m => SystemInterface m
+newSystemInterface =
+    SystemInterface {
+        siReadFile = \_ _ -> return $ Left "Not implemented",
+        siFindSource = \_ _ _ name -> return name,
+        siGetConfig = \_ -> return Nothing
+    }
+
 -- Parser input and output
 data ParseSpec = ParseSpec {
     psFilename :: String,
@@ -165,6 +176,7 @@
     asExecutionMode :: ExecutionMode,
     asCheckSourced :: Bool,
     asOptionalChecks :: [String],
+    asExtendedAnalysis :: Maybe Bool,
     asTokenPositions :: Map.Map Id (Position, Position)
 }
 
@@ -175,6 +187,7 @@
     asExecutionMode = Executed,
     asCheckSourced = False,
     asOptionalChecks = [],
+    asExtendedAnalysis = Nothing,
     asTokenPositions = Map.empty
 }
 
@@ -212,7 +225,7 @@
     }
 
 -- Supporting data types
-data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
+data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq)
 data ExecutionMode = Executed | Sourced deriving (Show, Eq)
 
 type ErrorMessage = String
@@ -311,7 +324,7 @@
 
 -- For testing
 mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
-mockedSystemInterface files = SystemInterface {
+mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
     siReadFile = rf,
     siFindSource = fs,
     siGetConfig = const $ return Nothing
@@ -326,4 +339,3 @@
 mockRcFile rcfile mock = mock {
     siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
 }
-
diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs
--- a/src/ShellCheck/Parser.hs
+++ b/src/ShellCheck/Parser.hs
@@ -46,6 +46,7 @@
 import Text.Parsec.Pos
 import qualified Control.Monad.Reader as Mr
 import qualified Control.Monad.State as Ms
+import qualified Data.List.NonEmpty as NE
 import qualified Data.Map.Strict as Map
 
 import Test.QuickCheck.All (quickCheckAll)
@@ -160,7 +161,7 @@
     deriving (Show)
 
 data HereDocContext =
-        HereDocPending Token [Context] -- on linefeed, read this T_HereDoc
+        HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc
     deriving (Show)
 
 data UserState = UserState {
@@ -238,12 +239,12 @@
         hereDocMap = Map.insert id list map
     }
 
-addPendingHereDoc t = do
+addPendingHereDoc id d q str = do
     state <- getState
     context <- getCurrentContexts
     let docs = pendingHereDocs state
     putState $ state {
-        pendingHereDocs = HereDocPending t context : docs
+        pendingHereDocs = HereDocPending id d q str context : docs
     }
 
 popPendingHereDocs = do
@@ -1057,6 +1058,16 @@
                         "This shell type is unknown. Use e.g. sh or bash."
                 return [ShellOverride shell]
 
+            "extended-analysis" -> do
+                pos <- getPosition
+                value <- plainOrQuoted $ many1 letter
+                case value of
+                    "true" -> return [ExtendedAnalysis True]
+                    "false" -> return [ExtendedAnalysis False]
+                    _ -> do
+                        parseNoteAt pos ErrorC 1146 "Unknown extended-analysis value. Expected true/false."
+                        return []
+
             "external-sources" -> do
                 pos <- getPosition
                 value <- plainOrQuoted $ many1 letter
@@ -1194,7 +1205,7 @@
 
 readDollarBracedLiteral = do
     start <- startSpan
-    vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
+    vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable
     id <- endSpan start
     return $ T_Literal id $ concat vars
 
@@ -1556,7 +1567,7 @@
     return $ concat strings
 
 readGenericLiteral1 endExp = do
-    strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp
+    strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp
     return $ concat strings
 
 readGenericEscaped = do
@@ -1835,7 +1846,7 @@
 
     -- add empty tokens for now, read the rest in readPendingHereDocs
     let doc = T_HereDoc hid dashed quoted endToken []
-    addPendingHereDoc doc
+    addPendingHereDoc hid dashed quoted endToken
     return doc
   where
     unquote :: String -> (Quoted, String)
@@ -1856,7 +1867,7 @@
     docs <- popPendingHereDocs
     mapM_ readDoc docs
   where
-    readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
+    readDoc (HereDocPending id dashed quoted endToken ctx) =
       swapContext ctx $
       do
         docStartPos <- getPosition
@@ -2283,23 +2294,32 @@
 
     subRead name script =
         withContext (ContextSource name) $
-            inSeparateContext $
-                subParse (initialPos name) (readScriptFile True) script
+            inSeparateContext $ do
+                oldState <- getState
+                setState $ oldState { pendingHereDocs = [] }
+                result <- subParse (initialPos name) (readScriptFile True) script
+                newState <- getState
+                setState $ newState { pendingHereDocs = pendingHereDocs oldState }
+                return result
 readSource t = return t
 
 
 prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
 prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
 prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat"
+prop_readPipeline4 = isOk readPipeline "! ! true"
+prop_readPipeline5 = isOk readPipeline "true | ! true"
 readPipeline = do
     unexpecting "keyword/token" readKeyword
-    do
-        (T_Bang id) <- g_Bang
-        pipe <- readPipeSequence
-        return $ T_Banged id pipe
-      <|>
-        readPipeSequence
+    readBanged readPipeSequence
 
+readBanged parser = do
+    pos <- getPosition
+    (T_Bang id) <- g_Bang
+    next <- readBanged parser
+    return $ T_Banged id next
+ <|> parser
+
 prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
 prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
 prop_readAndOr2 = isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo"
@@ -2354,14 +2374,14 @@
 
 readPipeSequence = do
     start <- startSpan
-    (cmds, pipes) <- sepBy1WithSeparators readCommand
+    (cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand)
                         (readPipe `thenSkip` (spacing >> readLineBreak))
     id <- endSpan start
     spacing
     return $ T_Pipeline id pipes cmds
   where
     sepBy1WithSeparators p s = do
-        let elems = p >>= \x -> return ([x], [])
+        let elems = (\x -> ([x], [])) <$> p
         let seps = do
             separator <- s
             return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
@@ -2384,6 +2404,10 @@
     ]
 
 readCmdName = do
+    -- If the command name is `!` then
+    optional . lookAhead . try $ do
+        char '!'
+        whitespace
     -- Ignore alias suppression
     optional . try $ do
         char '\\'
@@ -2891,8 +2915,8 @@
     kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
     kludgeAwayQuotes s p =
         case s of
-            first:rest@(_:_) ->
-                let (last:backwards) = reverse rest
+            first:second:rest ->
+                let (last NE.:| backwards) = NE.reverse (second NE.:| rest)
                     middle = reverse backwards
                 in
                     if first `elem` "'\"" && first == last
@@ -3322,10 +3346,12 @@
               then do
                     commands <- readCompoundListOrEmpty
                     id <- endSpan start
+                    readPendingHereDocs
                     verifyEof
                     let script = T_Annotation annotationId annotations $
                                     T_Script id shebang commands
-                    reparseIndices script
+                    userstate <- getState
+                    reparseIndices $ reattachHereDocs script (hereDocMap userstate)
                 else do
                     many anyChar
                     id <- endSpan start
@@ -3335,8 +3361,8 @@
     verifyShebang pos s = do
         case isValidShell s of
             Just True -> return ()
-            Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!"
-            Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify."
+            Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' scripts. Sorry!"
+            Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to specify."
 
     isValidShell s =
         let good = null s || any (`isPrefixOf` s) goodShells
@@ -3352,6 +3378,7 @@
         "sh",
         "ash",
         "dash",
+        "busybox sh",
         "bash",
         "bats",
         "ksh"
@@ -3360,6 +3387,7 @@
         "awk",
         "csh",
         "expect",
+        "fish",
         "perl",
         "python",
         "ruby",
@@ -3438,9 +3466,8 @@
       pos = errorPos parsecError
 
 getStringFromParsec errors =
-        case map f errors of
-            r -> unwords (take 1 $ catMaybes $ reverse r)  ++
-                " Fix any mentioned problems and try again."
+        headOrDefault "" (mapMaybe f $ reverse errors)  ++
+            " Fix any mentioned problems and try again."
     where
         f err =
             case err of
@@ -3471,8 +3498,7 @@
             return newParseResult {
                 prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
                 prTokenPositions = Map.map startEndPosToPos (positionMap userstate),
-                prRoot = Just $
-                    reattachHereDocs script (hereDocMap userstate)
+                prRoot = Just script
             }
         Left err -> do
             let context = contextStack state
@@ -3490,13 +3516,11 @@
     -- 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
+notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list]
   where
-    isName (ContextName _ _) = True
-    isName _ = False
-    first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
+    first (pos, str) = ParseNote pos pos ErrorC 1073 $
         "Couldn't parse this " ++ str ++ ". Fix to allow more checks."
-    second (ContextName pos str) = ParseNote pos pos InfoC 1009 $
+    second (pos, str) = ParseNote pos pos InfoC 1009 $
         "The mentioned syntax error was in this " ++ str ++ "."
 
 -- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
diff --git a/test/shellcheck.hs b/test/shellcheck.hs
--- a/test/shellcheck.hs
+++ b/test/shellcheck.hs
@@ -18,21 +18,24 @@
 
 main = do
     putStrLn "Running ShellCheck tests..."
-    results <- sequence [
-        ShellCheck.Analytics.runTests
-        ,ShellCheck.AnalyzerLib.runTests
-        ,ShellCheck.ASTLib.runTests
-        ,ShellCheck.CFG.runTests
-        ,ShellCheck.CFGAnalysis.runTests
-        ,ShellCheck.Checker.runTests
-        ,ShellCheck.Checks.Commands.runTests
-        ,ShellCheck.Checks.ControlFlow.runTests
-        ,ShellCheck.Checks.Custom.runTests
-        ,ShellCheck.Checks.ShellSupport.runTests
-        ,ShellCheck.Fixer.runTests
-        ,ShellCheck.Formatter.Diff.runTests
-        ,ShellCheck.Parser.runTests
+    failures <- filter (not . snd) <$> mapM sequenceA tests
+    if null failures then exitSuccess else do
+      putStrLn "Tests failed for the following module(s):"
+      mapM (putStrLn . ("- ShellCheck." ++) . fst) failures
+      exitFailure
+  where
+    tests =
+      [ ("Analytics"          , ShellCheck.Analytics.runTests)
+      , ("AnalyzerLib"        , ShellCheck.AnalyzerLib.runTests)
+      , ("ASTLib"             , ShellCheck.ASTLib.runTests)
+      , ("CFG"                , ShellCheck.CFG.runTests)
+      , ("CFGAnalysis"        , ShellCheck.CFGAnalysis.runTests)
+      , ("Checker"            , ShellCheck.Checker.runTests)
+      , ("Checks.Commands"    , ShellCheck.Checks.Commands.runTests)
+      , ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests)
+      , ("Checks.Custom"      , ShellCheck.Checks.Custom.runTests)
+      , ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests)
+      , ("Fixer"              , ShellCheck.Fixer.runTests)
+      , ("Formatter.Diff"     , ShellCheck.Formatter.Diff.runTests)
+      , ("Parser"             , ShellCheck.Parser.runTests)
       ]
-    if and results
-      then exitSuccess
-      else exitFailure
