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