packages feed

ShellCheck 0.7.1 → 0.7.2

raw patch · 16 files changed

+1510/−557 lines, 16 files

Files

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