diff --git a/ShellCheck.cabal b/ShellCheck.cabal index 48b492e..7b5dda7 100644 --- a/ShellCheck.cabal +++ b/ShellCheck.cabal @@ -47,6 +47,7 @@ library QuickCheck >= 2.7.4 exposed-modules: ShellCheck.AST + ShellCheck.ASTLib ShellCheck.Analytics ShellCheck.Analyzer ShellCheck.Checker diff --git a/ShellCheck/AST.hs b/ShellCheck/AST.hs index 4435215..edab373 100644 --- a/ShellCheck/AST.hs +++ b/ShellCheck/AST.hs @@ -357,10 +357,3 @@ doAnalysis f = analyze f blank id doStackAnalysis startToken endToken = analyze startToken endToken id doTransform i = runIdentity . analyze blank blank i -isLoop t = case t of - T_WhileExpression {} -> True - T_UntilExpression {} -> True - T_ForIn {} -> True - T_ForArithmetic {} -> True - T_SelectIn {} -> True - _ -> False diff --git a/ShellCheck/ASTLib.hs b/ShellCheck/ASTLib.hs new file mode 100644 index 0000000..a043fd4 --- /dev/null +++ b/ShellCheck/ASTLib.hs @@ -0,0 +1,240 @@ +{- + 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 . +-} +module ShellCheck.ASTLib where + +import ShellCheck.AST + +import Control.Monad +import Data.List +import Data.Maybe + +-- Is this a type of loop? +isLoop t = case t of + T_WhileExpression {} -> True + T_UntilExpression {} -> True + T_ForIn {} -> True + T_ForArithmetic {} -> True + T_SelectIn {} -> True + _ -> False + +-- Will this split into multiple words when used as an argument? +willSplit x = + case x of + T_DollarBraced {} -> True + T_DollarExpansion {} -> True + T_Backticked {} -> True + T_BraceExpansion {} -> True + T_Glob {} -> True + T_Extglob {} -> True + T_NormalWord _ l -> any willSplit l + _ -> False + +isGlob (T_Extglob {}) = True +isGlob (T_Glob {}) = True +isGlob (T_NormalWord _ l) = any isGlob l +isGlob _ = False + +-- Is this shell word a constant? +isConstant token = + case token of + T_NormalWord _ l -> all isConstant l + T_DoubleQuoted _ l -> all isConstant l + T_SingleQuoted _ _ -> True + T_Literal _ _ -> True + _ -> False + +-- Is this an empty literal? +isEmpty token = + case token of + T_NormalWord _ l -> all isEmpty l + T_DoubleQuoted _ l -> all isEmpty l + T_SingleQuoted _ "" -> True + T_Literal _ "" -> True + _ -> False + +-- Quick&lazy oversimplification of commands, throwing away details +-- and returning a list like ["find", ".", "-name", "${VAR}*" ]. +oversimplify token = + case token of + (T_NormalWord _ l) -> [concat (concatMap oversimplify l)] + (T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)] + (T_SingleQuoted _ s) -> [s] + (T_DollarBraced _ _) -> ["${VAR}"] + (T_DollarArithmetic _ _) -> ["${VAR}"] + (T_DollarExpansion _ _) -> ["${VAR}"] + (T_Backticked _ _) -> ["${VAR}"] + (T_Glob _ s) -> [s] + (T_Pipeline _ _ [x]) -> oversimplify x + (T_Literal _ x) -> [x] + (T_SimpleCommand _ vars words) -> concatMap oversimplify words + (T_Redirecting _ _ foo) -> oversimplify foo + (T_DollarSingleQuoted _ s) -> [s] + (T_Annotation _ _ s) -> oversimplify s + -- Workaround for let "foo = bar" parsing + (TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v + otherwise -> [] + + +-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar", +-- each in a tuple of (token, stringFlag). +getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) = + let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ oversimplify x)) args in + concatMap flag textArgs + where + flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ] + flag (x, '-':args) = map (\v -> (x, [v])) args + flag _ = [] +getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)" + +-- Get all flags in a GNU way, up until -- +getAllFlags = getFlagsUntil (== "--") +-- Get all flags in a BSD way, up until first non-flag argument +getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`)) + + +-- Given a T_DollarBraced, return a simplified version of the string contents. +bracedString (T_DollarBraced _ l) = concat $ oversimplify l +bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)" + +-- Is this an expansion of multiple items of an array? +isArrayExpansion t@(T_DollarBraced _ _) = + let string = bracedString t in + "@" `isPrefixOf` string || + not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string +isArrayExpansion _ = False + +-- Is it possible that this arg becomes multiple args? +mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t + where + f t@(T_DollarBraced _ _) = + let string = bracedString t in + "!" `isPrefixOf` string + f (T_DoubleQuoted _ parts) = any f parts + f (T_NormalWord _ parts) = any f parts + f _ = False + +-- Is it certain that this word will becomes multiple words? +willBecomeMultipleArgs t = willConcatInAssignment t || f t + where + f (T_Extglob {}) = True + f (T_Glob {}) = True + f (T_BraceExpansion {}) = True + f (T_DoubleQuoted _ parts) = any f parts + f (T_NormalWord _ parts) = any f parts + f _ = False + +-- This does token cause implicit concatenation in assignments? +willConcatInAssignment token = + case token of + t@(T_DollarBraced {}) -> isArrayExpansion t + (T_DoubleQuoted _ parts) -> any willConcatInAssignment parts + (T_NormalWord _ parts) -> any willConcatInAssignment parts + _ -> False + +-- Maybe get the literal string corresponding to this token +getLiteralString :: Token -> Maybe String +getLiteralString = getLiteralStringExt (const Nothing) + +-- Definitely get a literal string, skipping over all non-literals +onlyLiteralString :: Token -> String +onlyLiteralString = fromJust . getLiteralStringExt (const $ return "") + +-- Maybe get a literal string, but only if it's an unquoted argument. +getUnquotedLiteral (T_NormalWord _ list) = + liftM concat $ mapM str list + where + str (T_Literal _ s) = return s + str _ = Nothing +getUnquotedLiteral _ = Nothing + +-- Maybe get the literal string of this token and any globs in it. +getGlobOrLiteralString = getLiteralStringExt f + where + f (T_Glob _ str) = return str + f _ = Nothing + +-- Maybe get the literal value of a token, using a custom function +-- to map unrecognized Tokens into strings. +getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String +getLiteralStringExt more = g + where + allInList = liftM concat . mapM g + g (T_DoubleQuoted _ l) = allInList l + g (T_DollarDoubleQuoted _ l) = allInList l + g (T_NormalWord _ l) = allInList l + g (TA_Expansion _ l) = allInList l + g (T_SingleQuoted _ s) = return s + g (T_Literal _ s) = return s + g x = more x + +-- Is this token a string literal? +isLiteral t = isJust $ getLiteralString t + + +-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] +getWordParts (T_NormalWord _ l) = concatMap getWordParts l +getWordParts (T_DoubleQuoted _ l) = l +getWordParts other = [other] + +-- Return a list of NormalWords that would result from brace expansion +braceExpand (T_NormalWord id list) = take 1000 $ do + items <- mapM part list + return $ T_NormalWord id items + where + part (T_BraceExpansion id items) = do + item <- items + braceExpand item + part x = return x + +-- Maybe get the command name of a token representing a command +getCommandName t = + case t of + T_Redirecting _ _ w -> getCommandName w + T_SimpleCommand _ _ (w:_) -> getLiteralString w + T_Annotation _ _ t -> getCommandName t + otherwise -> Nothing + +-- Get the basename of a token representing a command +getCommandBasename = liftM basename . getCommandName + where + basename = reverse . takeWhile (/= '/') . reverse + +isAssignment t = + case t of + T_Redirecting _ _ w -> isAssignment w + T_SimpleCommand _ (w:_) [] -> True + T_Assignment {} -> True + T_Annotation _ _ w -> isAssignment w + otherwise -> False + +-- Get the list of commands from tokens that contain them, such as +-- the body of while loops and if statements. +getCommandSequences t = + case t of + T_Script _ _ cmds -> [cmds] + T_BraceGroup _ cmds -> [cmds] + T_Subshell _ cmds -> [cmds] + T_WhileExpression _ _ cmds -> [cmds] + T_UntilExpression _ _ cmds -> [cmds] + T_ForIn _ _ _ cmds -> [cmds] + T_ForArithmetic _ _ _ _ cmds -> [cmds] + T_IfExpression _ thens elses -> map snd thens ++ [elses] + otherwise -> [] + diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs index 436974a..db68498 100644 --- a/ShellCheck/Analytics.hs +++ b/ShellCheck/Analytics.hs @@ -21,6 +21,7 @@ module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where import ShellCheck.AST +import ShellCheck.ASTLib import ShellCheck.Data import ShellCheck.Parser import ShellCheck.Interface @@ -104,11 +105,10 @@ runList spec list = notes } notes = concatMap (\f -> f params root) list -getCode (TokenComment _ (Comment _ c _)) = c - - 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 @@ -251,22 +251,6 @@ isVariableName _ = False potentially = fromMaybe (return ()) -willSplit x = - case x of - T_DollarBraced {} -> True - T_DollarExpansion {} -> True - T_Backticked {} -> True - T_BraceExpansion {} -> True - T_Glob {} -> True - T_Extglob {} -> True - T_NormalWord _ l -> any willSplit l - _ -> False - -isGlob (T_Extglob {}) = True -isGlob (T_Glob {}) = True -isGlob (T_NormalWord _ l) = any isGlob l -isGlob _ = False - wouldHaveBeenGlob s = '*' `elem` s isConfusedGlobRegex ('*':_) = True @@ -288,59 +272,6 @@ getSuspiciousRegexWildcard str = headOrDefault _ (a:_) = a headOrDefault def _ = def -isConstant token = - case token of - T_NormalWord _ l -> all isConstant l - T_DoubleQuoted _ l -> all isConstant l - T_SingleQuoted _ _ -> True - T_Literal _ _ -> True - _ -> False - -isEmpty token = - case token of - T_NormalWord _ l -> all isEmpty l - T_DoubleQuoted _ l -> all isEmpty l - T_SingleQuoted _ "" -> True - T_Literal _ "" -> True - _ -> False - -makeSimple (T_NormalWord _ [f]) = f -makeSimple (T_Redirecting _ _ f) = f -makeSimple (T_Annotation _ _ f) = f -makeSimple t = t -simplify = doTransform makeSimple - -deadSimple (T_NormalWord _ l) = [concat (concatMap deadSimple l)] -deadSimple (T_DoubleQuoted _ l) = [concat (concatMap deadSimple l)] -deadSimple (T_SingleQuoted _ s) = [s] -deadSimple (T_DollarBraced _ _) = ["${VAR}"] -deadSimple (T_DollarArithmetic _ _) = ["${VAR}"] -deadSimple (T_DollarExpansion _ _) = ["${VAR}"] -deadSimple (T_Backticked _ _) = ["${VAR}"] -deadSimple (T_Glob _ s) = [s] -deadSimple (T_Pipeline _ _ [x]) = deadSimple x -deadSimple (T_Literal _ x) = [x] -deadSimple (T_SimpleCommand _ vars words) = concatMap deadSimple words -deadSimple (T_Redirecting _ _ foo) = deadSimple foo -deadSimple (T_DollarSingleQuoted _ s) = [s] -deadSimple (T_Annotation _ _ s) = deadSimple s --- Workaround for let "foo = bar" parsing -deadSimple (TA_Sequence _ [TA_Expansion _ v]) = concatMap deadSimple v -deadSimple _ = [] - --- Turn a SimpleCommand foo -avz --bar=baz into args ["a", "v", "z", "bar"] -getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) = - let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ deadSimple x)) args in - concatMap flag textArgs - where - flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ] - flag (x, '-':args) = map (\v -> (x, [v])) args - flag _ = [] - -getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)" - -getAllFlags = getFlagsUntil (== "--") -getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`)) (!!!) list i = case drop i list of @@ -425,8 +356,8 @@ checkEchoWc _ (T_Pipeline id _ [a, b]) = ["wc", "-m"] -> countMsg _ -> return () where - acmd = deadSimple a - bcmd = deadSimple b + acmd = oversimplify a + bcmd = oversimplify b countMsg = style id 2000 "See if you can use ${#variable} instead." checkEchoWc _ _ = return () @@ -447,8 +378,8 @@ checkEchoSed _ (T_Pipeline id _ [a, b]) = guard $ length delimiters == 2 return True - acmd = deadSimple a - bcmd = deadSimple b + acmd = oversimplify a + bcmd = oversimplify b checkIn s = when (isSimpleSed s) $ style id 2001 "See if you can use ${variable//search/replace} instead." @@ -467,7 +398,7 @@ 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 (deadSimple 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) ." @@ -551,7 +482,7 @@ prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | checkPipePitfalls _ (T_Pipeline id _ commands) = do for ["find", "xargs"] $ \(find:xargs:_) -> - let args = deadSimple xargs ++ deadSimple find + let args = oversimplify xargs ++ oversimplify find in unless (any ($ args) [ hasShortParameter '0', @@ -578,12 +509,12 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do ] unless didLs $ do for ["ls", "?"] $ - \(ls:_) -> unless (hasShortParameter 'N' (deadSimple 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 "" . deadSimple) commands) + let indices = indexOfSublists l (map (headOrDefault "" . oversimplify) commands) in do mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices return . not . null $ indices @@ -609,39 +540,6 @@ indexOfSublists sub = f 0 match _ _ = False -bracedString l = concat $ deadSimple l - -isArrayExpansion (T_DollarBraced _ l) = - let string = bracedString l in - "@" `isPrefixOf` string || - not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string -isArrayExpansion _ = False - --- Is it certain that this arg will becomes multiple args? -willBecomeMultipleArgs t = willConcatInAssignment t || f t - where - f (T_Extglob {}) = True - f (T_Glob {}) = True - f (T_BraceExpansion {}) = True - f (T_DoubleQuoted _ parts) = any f parts - f (T_NormalWord _ parts) = any f parts - f _ = False - -willConcatInAssignment t@(T_DollarBraced {}) = isArrayExpansion t -willConcatInAssignment (T_DoubleQuoted _ parts) = any willConcatInAssignment parts -willConcatInAssignment (T_NormalWord _ parts) = any willConcatInAssignment parts -willConcatInAssignment _ = False - --- Is it possible that this arg becomes multiple args? -mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t - where - f (T_DollarBraced _ l) = - let string = bracedString l in - "!" `isPrefixOf` string - f (T_DoubleQuoted _ parts) = any f parts - f (T_NormalWord _ parts) = any f parts - f _ = False - prop_checkShebangParameters1 = verifyTree checkShebangParameters "#!/usr/bin/env bash -x\necho cow" prop_checkShebangParameters2 = verifyNotTree checkShebangParameters "#! /bin/sh -l " checkShebangParameters _ (T_Script id sb _) = @@ -723,8 +621,8 @@ checkBashisms _ = bashism mapM_ check expansion when (var `elem` bashVars) $ warnMsg id $ var ++ " is" where - str = concat $ deadSimple token - var = getBracedReference (bracedString token) + str = bracedString t + var = getBracedReference str check (regex, feature) = when (isJust $ matchRegex regex str) $ warnMsg id feature @@ -741,9 +639,9 @@ checkBashisms _ = bashism | t `isCommand` "echo" && "-" `isPrefixOf` argString = unless ("--" `isPrefixOf` argString) $ -- echo "-------" warnMsg (getId arg) "echo flags are" - where argString = concat $ deadSimple arg + where argString = concat $ oversimplify arg bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) - | t `isCommand` "exec" && "-" `isPrefixOf` concat (deadSimple 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" @@ -844,7 +742,7 @@ checkForInLs _ = try check id f x try _ = return () check id f x = - case deadSimple x of + 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." @@ -941,7 +839,7 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) = T_DGREAT _ -> [file] _ -> [] getRedirs _ = [] - special x = "/dev/" `isPrefixOf` concat (deadSimple x) + special x = "/dev/" `isPrefixOf` concat (oversimplify x) isOutput t = case drop 1 $ getPath (parentMap params) t of T_IoFile _ op _:_ -> @@ -971,8 +869,8 @@ checkShorthandIf _ _ = return () prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done" prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*" -checkDollarStar p t@(T_NormalWord _ [T_DollarBraced id l]) - | bracedString l == "*" = +checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _)]) + | bracedString b == "*" = unless isAssigned $ warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems." where @@ -998,7 +896,7 @@ checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree "Double quote array expansions to avoid re-splitting elements." where -- Fixme: should detect whether the alterantive is quoted - isAlternative (T_DollarBraced _ t) = ":+" `isInfixOf` bracedString t + isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b isAlternative _ = False checkUnquotedDollarAt _ _ = return () @@ -1227,11 +1125,11 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do op ++ " is for integer comparisons. Use " ++ seqv op ++ " instead." isNum t = - case deadSimple t of + case oversimplify t of [v] -> all isDigit v _ -> False isFraction t = - case deadSimple t of + case oversimplify t of [v] -> isJust $ matchRegex floatRegex v _ -> False @@ -1318,7 +1216,7 @@ 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 $ deadSimple rhs in + let s = concat $ oversimplify rhs in when (isConfusedGlobRegex s) $ warn (getId rhs) 2049 "=~ is for regex. Use == for globs." checkGlobbedRegex _ _ = return () @@ -1436,8 +1334,8 @@ 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 _ [T_DollarBraced id l]) = - unless (isException $ bracedString l) getWarning +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) @@ -1634,52 +1532,6 @@ checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) = when (t `isUnqualifiedCommand` str) $ f cmd rest checkUnqualifiedCommand _ _ _ = return () -getLiteralString = getLiteralStringExt (const Nothing) - -getGlobOrLiteralString = getLiteralStringExt f - where - f (T_Glob _ str) = return str - f _ = Nothing - -getLiteralStringExt more = g - where - allInList = liftM concat . mapM g - g (T_DoubleQuoted _ l) = allInList l - g (T_DollarDoubleQuoted _ l) = allInList l - g (T_NormalWord _ l) = allInList l - g (TA_Expansion _ l) = allInList l - g (T_SingleQuoted _ s) = return s - g (T_Literal _ s) = return s - g x = more x - -isLiteral t = isJust $ getLiteralString t - --- Get a literal string ignoring all non-literals -onlyLiteralString :: Token -> String -onlyLiteralString = fromJust . getLiteralStringExt (const $ return "") - --- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] -getWordParts (T_NormalWord _ l) = concatMap getWordParts l -getWordParts (T_DoubleQuoted _ l) = l -getWordParts other = [other] - -getUnquotedLiteral (T_NormalWord _ list) = - liftM concat $ mapM str list - where - str (T_Literal _ s) = return s - str _ = Nothing -getUnquotedLiteral _ = Nothing - --- Return a list of NormalWords resulting from brace expansion -braceExpand (T_NormalWord id list) = take 1000 $ do - items <- mapM part list - return $ T_NormalWord id items - where - part (T_BraceExpansion id items) = do - item <- items - braceExpand item - part x = return x - isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd) isUnqualifiedCommand token str = isCommandMatch token (== str) @@ -1687,22 +1539,6 @@ isCommandMatch token matcher = fromMaybe False $ do cmd <- getCommandName token return $ matcher cmd -getCommandName (T_Redirecting _ _ w) = - getCommandName w -getCommandName (T_SimpleCommand _ _ (w:_)) = - getLiteralString w -getCommandName (T_Annotation _ _ t) = getCommandName t -getCommandName _ = Nothing - -getCommandBasename = liftM basename . getCommandName -basename = reverse . takeWhile (/= '/') . reverse - -isAssignment (T_Annotation _ _ w) = isAssignment w -isAssignment (T_Redirecting _ _ w) = isAssignment w -isAssignment (T_SimpleCommand _ (w:_) []) = True -isAssignment (T_Assignment {}) = True -isAssignment _ = False - prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\"" prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'" prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" @@ -1712,10 +1548,19 @@ checkPrintfVar _ = checkUnqualifiedCommand "printf" (const f) where f (format:params) = check format f _ = return () check format = - unless ('%' `elem` concat (deadSimple format) || isLiteral 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 _ _] -> True + T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ _]] -> True + T_NormalWord id [T_Backticked _ _] -> True + T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ _]] -> True + _ -> False + prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`" prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\"" @@ -1726,14 +1571,6 @@ checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token) f _ = return () --- Check whether a word is entirely output from a single command -tokenIsJustCommandOutput t = case t of - T_NormalWord id [T_DollarExpansion _ _] -> True - T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ _]] -> True - T_NormalWord id [T_Backticked _ _] -> True - T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ _]] -> True - _ -> False - 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')\"" @@ -1837,7 +1674,7 @@ checkGrepRe _ = checkCommand "grep" (const f) where f (re:_) = do when (isGlob re) $ warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." - let string = concat $ deadSimple re + 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 @@ -1871,7 +1708,7 @@ 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 $ deadSimple x in + 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 () @@ -1907,7 +1744,7 @@ checkSudoRedirect _ (T_Redirecting _ redirs cmd) | cmd `isCommand` "sudo" = "sudo doesn't affect redirects. Use .. | sudo tee -a file" _ -> return () warnAbout _ = return () - special file = concat (deadSimple file) == "/dev/null" + special file = concat (oversimplify file) == "/dev/null" checkSudoRedirect _ _ = return () prop_checkReturn1 = verifyNot checkReturn "return" @@ -1952,7 +1789,7 @@ prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'" checkPS1Assignments _ (T_Assignment _ _ "PS1" _ word) = warnFor word where warnFor word = - let contents = concat $ deadSimple word in + 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 = @@ -2120,7 +1957,7 @@ checkUnusedEchoEscapes _ = checkCommand "echo" (const f) where isDashE = mkRegex "^-.*e" hasEscapes = mkRegex "\\\\[rnt]" - f args | concat (concatMap deadSimple allButLast) `matches` isDashE = + f args | concat (concatMap oversimplify allButLast) `matches` isDashE = return () where allButLast = reverse . drop 1 . reverse $ args f args = mapM_ checkEscapes args @@ -2164,7 +2001,7 @@ prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\"" checkSshCommandString _ = checkCommand "ssh" (const f) where nonOptions = - filter (\x -> not $ "-" `isPrefixOf` concat (deadSimple x)) + filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x)) f args = case nonOptions args of (hostport:r@(_:_)) -> checkArg $ last r @@ -2370,7 +2207,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal if var == "" then [] else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])] - where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ deadSimple token + where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest getSetParams (t:rest) = @@ -2431,7 +2268,7 @@ getIndexReferences s = fromMaybe [] $ do getReferencedVariables t = case t of - T_DollarBraced id l -> let str = bracedString l in + 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 @@ -2614,7 +2451,7 @@ checkSpacefulness params t = parents = parentMap params isCounting (T_DollarBraced id token) = - case concat $ deadSimple token of + case concat $ oversimplify token of '#':_ -> True _ -> False isCounting _ = False @@ -2622,8 +2459,8 @@ checkSpacefulness params t = -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"} isQuotedAlternative t = case t of - T_DollarBraced _ l -> - ":+" `isInfixOf` bracedString l + T_DollarBraced _ _ -> + ":+" `isInfixOf` bracedString t _ -> False isSpacefulWord :: (String -> Bool) -> [Token] -> Bool @@ -2637,7 +2474,7 @@ checkSpacefulness params t = T_Extglob {} -> True T_Literal _ s -> s `containsAny` globspace T_SingleQuoted _ s -> s `containsAny` globspace - T_DollarBraced _ l -> spacefulF $ getBracedReference $ bracedString l + T_DollarBraced _ _ -> spacefulF $ getBracedReference $ bracedString x T_NormalWord _ w -> isSpacefulWord spacefulF w T_DoubleQuoted _ w -> isSpacefulWord spacefulF w _ -> False @@ -2676,13 +2513,13 @@ checkQuotesInLiterals params t = forToken map (T_DollarBraced id t) = -- skip getBracedReference here to avoid false positives on PE - Map.lookup (concat . deadSimple $ t) map + 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 $ deadSimple t) + if containsQuotes (concat $ oversimplify t) then return $ getId t else Nothing @@ -2734,7 +2571,7 @@ checkFunctionsUsedExternally params t = | t `isUnqualifiedCommand` "alias" = mapM_ getAlias args findFunctions _ = return () getAlias arg = - let string = concat $ deadSimple arg + let string = concat $ oversimplify arg in when ('=' `elem` string) $ modify ((takeWhile (/= '=') string, getId arg):) checkArg cmd arg = potentially $ do @@ -2871,13 +2708,13 @@ checkUnassignedReferences params t = warnings isInArray var t = any isArray $ getPath (parentMap params) t where isArray (T_Array {}) = True - isArray (T_DollarBraced _ l) | var /= getBracedReference (bracedString l) = True + isArray b@(T_DollarBraced _ _) | var /= getBracedReference (bracedString b) = True isArray _ = False isGuarded (T_DollarBraced _ v) = any (`isPrefixOf` rest) ["-", ":-", "?", ":?"] where - name = concat $ deadSimple v + name = concat $ oversimplify v rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name isGuarded _ = False @@ -2895,12 +2732,12 @@ checkGlobsAsOptions _ (T_SimpleCommand _ _ args) = where check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" = info id 2035 $ - "Use ./" ++ concat (deadSimple v) + "Use ./" ++ concat (oversimplify v) ++ " so names with dashes won't become options." check _ = return () isEndOfArgs t = - case concat $ deadSimple t of + case concat $ oversimplify t of "--" -> True ":::" -> True "::::" -> True @@ -2924,7 +2761,7 @@ checkWhileReadPitfalls _ (T_WhileExpression id [command] contents) munchers = [ "ssh", "ffmpeg", "mplayer" ] isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) = - let plaintext = deadSimple cmd + let plaintext = oversimplify cmd in head (plaintext ++ [""]) == "read" && ("-u" `notElem` plaintext) && all (not . stdinRedirect) redirs @@ -2956,7 +2793,7 @@ prop_checkPrefixAssign2 = verifyNot checkPrefixAssignmentReference "var=$(echo $ checkPrefixAssignmentReference params t@(T_DollarBraced id value) = check path where - name = getBracedReference $ bracedString value + name = getBracedReference $ bracedString t path = getPath (parentMap params) t idPath = map getId path @@ -3013,7 +2850,7 @@ checkCdAndBack params = doLists doLists _ = return () isCdRevert t = - case deadSimple t of + case oversimplify t of ["cd", p] -> p `elem` ["..", "-"] _ -> False @@ -3093,7 +2930,7 @@ checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" when (any isRecursiveFlag simpleArgs) $ mapM_ (mapM_ checkWord . braceExpand) tokens where - simpleArgs = deadSimple t + simpleArgs = oversimplify t checkWord token = case getLiteralString token of @@ -3277,7 +3114,7 @@ checkOverridingPath _ (T_SimpleCommand _ vars []) = mapM_ checkVar vars where checkVar (T_Assignment id Assign "PATH" Nothing word) = - let string = concat $ deadSimple 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 @@ -3324,16 +3161,6 @@ shellSupport t = forCase _ = ("", []) -getCommandSequences (T_Script _ _ cmds) = [cmds] -getCommandSequences (T_BraceGroup _ cmds) = [cmds] -getCommandSequences (T_Subshell _ cmds) = [cmds] -getCommandSequences (T_WhileExpression _ _ cmds) = [cmds] -getCommandSequences (T_UntilExpression _ _ cmds) = [cmds] -getCommandSequences (T_ForIn _ _ _ cmds) = [cmds] -getCommandSequences (T_ForArithmetic _ _ _ _ cmds) = [cmds] -getCommandSequences (T_IfExpression _ thens elses) = map snd thens ++ [elses] -getCommandSequences _ = [] - groupWith f = groupBy ((==) `on` f) prop_checkMultipleAppends1 = verify checkMultipleAppends "foo >> file; bar >> file; baz >> file;" @@ -3365,7 +3192,7 @@ checkAliasesExpandEarly params = checkUnqualifiedCommand "alias" (const f) where f = mapM_ checkArg - checkArg arg | '=' `elem` concat (deadSimple arg) = + 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 () @@ -3563,7 +3390,7 @@ checkUncheckedCd params root = && not (isCondition $ getPath (parentMap params) t)) $ warn (getId t) 2164 "Use cd ... || exit in case cd fails." checkElement _ = return () - isCdDotDot t = deadSimple t == ["cd", ".."] + isCdDotDot t = oversimplify t == ["cd", ".."] hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root isSetE t = case t of