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