ShellCheck 0.3.4 → 0.3.5
raw patch · 9 files changed
+543/−291 lines, 9 filesdep +transformersdep ~QuickCheckPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: transformers
Dependency ranges changed: QuickCheck
API changes (from Hackage documentation)
- ShellCheck.Analytics: ForceShell :: Shell -> AnalysisOption
- ShellCheck.Analytics: data AnalysisOption
- ShellCheck.Analytics: instance Eq Shell
- ShellCheck.Analytics: instance Show Shell
+ ShellCheck.AST: TA_Index :: Id -> Token -> Token
+ ShellCheck.Analytics: AnalysisOptions :: Maybe Shell -> [Integer] -> AnalysisOptions
+ ShellCheck.Analytics: data AnalysisOptions
+ ShellCheck.Analytics: defaultAnalysisOptions :: AnalysisOptions
+ ShellCheck.Analytics: optionExcludes :: AnalysisOptions -> [Integer]
+ ShellCheck.Analytics: optionShellType :: AnalysisOptions -> Maybe Shell
+ ShellCheck.Options: AnalysisOptions :: Maybe Shell -> [Integer] -> AnalysisOptions
+ ShellCheck.Options: Bash :: Shell
+ ShellCheck.Options: Ksh :: Shell
+ ShellCheck.Options: Sh :: Shell
+ ShellCheck.Options: Zsh :: Shell
+ ShellCheck.Options: data AnalysisOptions
+ ShellCheck.Options: data Shell
+ ShellCheck.Options: defaultAnalysisOptions :: AnalysisOptions
+ ShellCheck.Options: instance Eq Shell
+ ShellCheck.Options: instance Show Shell
+ ShellCheck.Options: optionExcludes :: AnalysisOptions -> [Integer]
+ ShellCheck.Options: optionShellType :: AnalysisOptions -> Maybe Shell
+ ShellCheck.Parser: readScript :: MonadState ([ParseNote], [Context]) m => ParsecT String (Id, Map Id SourcePos, [ParseNote]) m Token
- ShellCheck.Analytics: runAnalytics :: [AnalysisOption] -> Token -> [Note]
+ ShellCheck.Analytics: runAnalytics :: AnalysisOptions -> Token -> [Note]
- ShellCheck.Parser: parseShell :: SourceName -> [Char] -> ParseResult
+ ShellCheck.Parser: parseShell :: SourceName -> String -> ParseResult
- ShellCheck.Simple: shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment]
+ ShellCheck.Simple: shellCheck :: AnalysisOptions -> String -> [ShellCheckComment]
Files
- README.md +3/−3
- ShellCheck.cabal +8/−5
- ShellCheck/AST.hs +3/−0
- ShellCheck/Analytics.hs +230/−98
- ShellCheck/Options.hs +14/−0
- ShellCheck/Parser.hs +135/−91
- ShellCheck/Simple.hs +17/−12
- shellcheck.1.md +10/−6
- shellcheck.hs +123/−76
README.md view
@@ -16,7 +16,7 @@ - To point out subtle caveats, corner cases and pitfalls, that may cause an advanced user's otherwise working script to fail under future circumstances. -ShellCheck is written in Haskell, and requires at least 1 GB of RAM to compile.+ShellCheck is written in Haskell, and requires 2 GB of memory to compile. ## Installing @@ -25,9 +25,9 @@ cabal update cabal install shellcheck -On Arch Linux with community packages enabled:+On Debian based distros: - pacman -S shellcheck+ apt-get install shellcheck On OS X with homebrew:
ShellCheck.cabal view
@@ -1,7 +1,7 @@ Name: ShellCheck-Version: 0.3.4+Version: 0.3.5 Synopsis: Shell script analysis tool-License: OtherLicense+License: AGPL-3 License-file: LICENSE Category: Static Analysis Author: Vidar Holen@@ -42,11 +42,12 @@ mtl, parsec, regex-compat,- QuickCheck >= 2.2+ QuickCheck >= 2.7.4 exposed-modules: ShellCheck.Analytics ShellCheck.AST ShellCheck.Data+ ShellCheck.Options ShellCheck.Parser ShellCheck.Simple other-modules:@@ -62,7 +63,8 @@ mtl, parsec, regex-compat,- QuickCheck >= 2.2+ transformers,+ QuickCheck >= 2.7.4 main-is: shellcheck.hs test-suite test-shellcheck@@ -76,6 +78,7 @@ mtl, parsec, regex-compat,- QuickCheck >= 2.2+ transformers,+ QuickCheck >= 2.7.4 main-is: test/shellcheck.hs
ShellCheck/AST.hs view
@@ -34,6 +34,7 @@ data Token = TA_Binary Id String Token Token | TA_Expansion Id [Token]+ | TA_Index Id Token | TA_Sequence Id [Token] | TA_Trinary Id Token Token Token | TA_Unary Id String Token@@ -245,6 +246,7 @@ c <- round t3 return $ TA_Trinary id a b c delve (TA_Expansion id t) = dl t $ TA_Expansion id+ delve (TA_Index id t) = d1 t $ TA_Index id delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns delve t = return t @@ -330,6 +332,7 @@ TA_Sequence id _ -> id TA_Trinary id _ _ _ -> id TA_Expansion id _ -> id+ TA_Index id _ -> id T_ProcSub id _ _ -> id T_Glob id _ -> id T_ForArithmetic id _ _ _ _ -> id
ShellCheck/Analytics.hs view
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. -} {-# LANGUAGE TemplateHaskell #-}-module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics, shellForExecutable, runTests) where+module ShellCheck.Analytics (AnalysisOptions(..), defaultAnalysisOptions, filterByAnnotation, runAnalytics, shellForExecutable, runTests) where import Control.Arrow (first) import Control.Monad@@ -29,23 +29,20 @@ import Data.Maybe import Debug.Trace import ShellCheck.AST+import ShellCheck.Options import ShellCheck.Data import ShellCheck.Parser hiding (runTests) import Text.Regex import qualified Data.Map as Map import Test.QuickCheck.All (quickCheckAll) -data Shell = Ksh | Zsh | Sh | Bash- deriving (Show, Eq)- data Parameters = Parameters { variableFlow :: [StackData], parentMap :: Map.Map Id Token,- shellType :: Shell+ shellType :: Shell,+ shellTypeSpecified :: Bool } -data AnalysisOption = ForceShell Shell- -- Checks that are run on the AST root treeChecks :: [Parameters -> Token -> [Note]] treeChecks = [@@ -55,11 +52,12 @@ ,subshellAssignmentCheck ,checkSpacefulness ,checkQuotesInLiterals- ,checkShebang+ ,checkShebangParameters ,checkFunctionsUsedExternally ,checkUnusedAssignments ,checkUnpassedInFunctions ,checkArrayWithoutIndex+ ,checkShebang ] checksFor Sh = [@@ -81,24 +79,20 @@ ,checkForDecimals ] -runAnalytics :: [AnalysisOption] -> Token -> [Note]+runAnalytics :: AnalysisOptions -> Token -> [Note] runAnalytics options root = runList options root treeChecks runList options root list = notes where params = Parameters {- shellType = getShellOption,+ shellType = fromMaybe (determineShell root) $ optionShellType options,+ shellTypeSpecified = isJust $ optionShellType options, parentMap = getParentTree root, variableFlow = getVariableFlow (shellType params) (parentMap params) root }- notes = concatMap (\f -> f params root) list+ notes = filter (\c -> getCode c `notElem` optionExcludes options) $ concatMap (\f -> f params root) list+ getCode (Note _ _ c _) = c - getShellOption =- fromMaybe (determineShell root) . msum $- map (\option ->- case option of- ForceShell x -> return x- ) options checkList l t = concatMap (\f -> f t) l @@ -207,6 +201,7 @@ ,checkTestGlobs ,checkConcatenatedDollarAt ,checkFindActionPrecedence+ ,checkTildeInPath ] @@ -289,6 +284,15 @@ headOrDefault _ (a:_) = a headOrDefault def _ = def +getAllMatches :: Regex -> String -> [[String]]+getAllMatches regex str = fromJust $ f str+ where+ f str = do+ (_, _, rest, groups) <- matchRegexAll regex str+ more <- f rest+ return $ groups : more+ `mappend` return []+ isConstant token = case token of T_NormalWord _ l -> all isConstant l@@ -343,21 +347,21 @@ [] -> Nothing (r:_) -> Just r -verify :: (Parameters -> Token -> Writer [a] ()) -> String -> Bool+verify :: (Parameters -> Token -> Writer [Note] ()) -> String -> Bool verify f s = checkNode f s == Just True -verifyNot :: (Parameters -> Token -> Writer [a] ()) -> String -> Bool+verifyNot :: (Parameters -> Token -> Writer [Note] ()) -> String -> Bool verifyNot f s = checkNode f s == Just False -verifyTree :: (Parameters -> Token -> [a]) -> String -> Bool+verifyTree :: (Parameters -> Token -> [Note]) -> String -> Bool verifyTree f s = checkTree f s == Just True -verifyNotTree :: (Parameters -> Token -> [a]) -> String -> Bool+verifyNotTree :: (Parameters -> Token -> [Note]) -> String -> Bool verifyNotTree f s = checkTree f s == Just False checkNode f = checkTree (runNodeAnalysis f) checkTree f s = case parseShell "-" s of- (ParseResult (Just (t, m)) _) -> Just . not . null $ runList [] t [f]+ (ParseResult (Just (t, m)) _) -> Just . not . null $ runList defaultAnalysisOptions t [f] _ -> Nothing @@ -460,15 +464,13 @@ 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 \"$@\"" checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) = checkCommand "cat" (const f) cmd where- f [word] = when (isSimple word) $+ f [word] = unless (mayBecomeMultipleArgs word) $ style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead." f _ = return ()- isSimple (T_NormalWord _ parts) = all isSimple parts- isSimple (T_DollarBraced _ _) = True- isSimple x = not $ willSplit x checkUuoc _ _ = return () prop_checkNeedlessCommands = verify checkNeedlessCommands "foo=$(expr 3 + 2)"@@ -494,7 +496,7 @@ \(find:xargs:_) -> let args = deadSimple xargs ++ deadSimple find in- unless (or $ map ($ args) [+ unless (any ($ args) [ hasShortParameter '0', hasParameter "null", hasParameter "print0",@@ -531,9 +533,9 @@ for' l f = for l (first f) first func (x:_) = func (getId x) first _ _ = return ()- hasShortParameter char list = any (\x -> "-" `isPrefixOf` x && char `elem` x) list- hasParameter string list =- any (isPrefixOf string . dropWhile (== '-')) list+ hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)+ hasParameter string =+ any (isPrefixOf string . dropWhile (== '-')) checkPipePitfalls _ _ = return () indexOfSublists sub = f 0@@ -583,11 +585,18 @@ f (T_NormalWord _ parts) = any f parts f _ = False -prop_checkShebang1 = verifyTree checkShebang "#!/usr/bin/env bash -x\necho cow"-prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l "-checkShebang _ (T_Script id sb _) =+prop_checkShebangParameters1 = verifyTree checkShebangParameters "#!/usr/bin/env bash -x\necho cow"+prop_checkShebangParameters2 = verifyNotTree checkShebangParameters "#! /bin/sh -l "+checkShebangParameters _ (T_Script id sb _) = [Note id ErrorC 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 _) =+ [Note id InfoC 2148 $ "Shebang (#!) missing. Assuming " ++ (show $ shellType params) ++ "."+ | 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++))"@@ -608,8 +617,8 @@ prop_checkBashisms18= verify checkBashisms "foo &> /dev/null" checkBashisms _ = bashism where- errMsg id s = err id 2040 $ "#!/bin/sh was specified, so " ++ s ++ " not supported, even when sh is actually bash."- warnMsg id s = warn id 2039 $ "#!/bin/sh was specified, but " ++ s ++ " not standard."+ errMsg id s = err id 2040 $ "In sh, " ++ s ++ " not supported, even when sh is actually bash."+ warnMsg id s = warn id 2039 $ "In POSIX sh, " ++ s ++ " not supported." bashism (T_ProcSub id _ _) = errMsg id "process substitution is" bashism (T_Extglob id _ _) = warnMsg id "extglob is" bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"@@ -793,18 +802,18 @@ prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null" prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar" checkRedirectToSame params s@(T_Pipeline _ _ list) =- mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurences x) l) (getAllRedirs list))) list+ mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list where note x = Note x InfoC 2094 "Make sure not to read and write the same file in the same pipeline."- checkOccurences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) =+ checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) = when (exceptId /= newId && x == y && not (isOutput t && isOutput u) && not (special t)) $ do addNote $ note newId addNote $ note exceptId- checkOccurences _ _ = return ()+ checkOccurrences _ _ = return () getAllRedirs = concatMap (\(T_Redirecting _ ls _) -> concatMap getRedirs ls) getRedirs (T_FdRedirect _ _ (T_IoFile _ op file)) = case op of T_Greater _ -> [file]@@ -858,13 +867,12 @@ prop_checkUnquotedDollarAt4 = verifyNot checkUnquotedDollarAt "ls \"$@\"" prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }" prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@"-checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not isAssigned =+prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done"+prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\""+checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word = forM_ (take 1 $ filter isArrayExpansion parts) $ \x -> err (getId x) 2068 "Double quote array expansions, otherwise they're like $* and break on spaces."- where- path = getPath (parentMap p) word- isAssigned = any isAssignment . take 2 $ path checkUnquotedDollarAt _ _ = return () prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\""@@ -948,6 +956,9 @@ 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@@ -959,7 +970,10 @@ "Expressions don't expand in single quotes, use double quotes for that." commandName = fromMaybe "" $ do cmd <- getClosestCommand parents t- getCommandBasename cmd+ name <- getCommandBasename cmd+ if name == "find"+ then return $ getFindCommand cmd+ else return name isProbablyOk = any isOkAssignment (take 3 $ getPath parents t)@@ -982,8 +996,18 @@ T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted otherwise -> False - re = mkRegex "\\$[{(0-9a-zA-Z_]"+ 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 () @@ -1197,19 +1221,29 @@ 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 _ [T_DollarBraced id l]) =- unless ((isException $ bracedString l) || (not isNormal)) $- style id 2004 "$ on variables in (( )) is unnecessary."+ unless (isException $ bracedString l) getWarning where isException [] = True- isException s = any (`elem` "/.:#%?*@") s || isDigit (head s)- isNormal = fromMaybe True $ msum $ map isNormalContext $ (parents params t)- isNormalContext t =+ 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 True- T_DollarArithmetic {} -> return True- T_SimpleCommand {} -> return False- _ -> fail "Irrelevant"+ 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 ))"@@ -1296,8 +1330,14 @@ f t = modify (Map.insert (getId t) t) --- Is this node self quoting?-isQuoteFree tree 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@@ -1317,14 +1357,17 @@ T_Arithmetic {} -> return True T_Assignment {} -> return True T_Redirecting {} -> return $- any (isCommand t) ["local", "declare", "typeset", "export"]+ 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"] T_DoubleQuoted _ _ -> return True+ T_DollarDoubleQuoted _ _ -> return True T_CaseExpression {} -> return True T_HereDoc {} -> return True T_DollarBraced {} -> return True- -- Pragmatically assume it's desirable to split here- T_ForIn {} -> return True- T_SelectIn {} -> 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 =@@ -1385,7 +1428,7 @@ getLiteralStringExt more = g where- allInList = liftM concat . sequence . map g+ allInList = liftM concat . mapM g g (T_DoubleQuoted _ l) = allInList l g (T_DollarDoubleQuoted _ l) = allInList l g (T_NormalWord _ l) = allInList l@@ -1396,12 +1439,23 @@ isLiteral t = isJust $ getLiteralString t --- turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]+-- 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] -isCommand token str = isCommandMatch token (\cmd -> cmd == str || ("/" ++ str) `isSuffixOf` cmd)+getUnquotedLiteral (T_NormalWord _ list) =+ liftM concat $ mapM str list+ where+ str (T_Literal _ s) = return s+ str _ = Nothing+getUnquotedLiteral _ = Nothing++isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd) isUnqualifiedCommand token str = isCommandMatch token (== str) isCommandMatch token matcher = fromMaybe False $ do@@ -1444,18 +1498,24 @@ prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\"" checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."- f [T_NormalWord id [T_DollarExpansion _ _]] = msg id- f [T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ _]]] = msg id- f [T_NormalWord id [T_Backticked _ _]] = msg id- f [T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ _]]] = msg id+ 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')\"" 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 checkUuoeVar _ p = case p of T_Backticked id [cmd] -> check id cmd@@ -1472,8 +1532,9 @@ check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c check _ _ = return ()- warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars ->- unless ("-" `isPrefixOf` concat (concatMap deadSimple vars)) $+ isCovered first rest = null rest && tokenIsJustCommandOutput first+ warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars@(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'." @@ -1688,7 +1749,7 @@ case trapped of T_DollarExpansion id _ -> warnAboutExpansion id T_DollarBraced id _ -> warnAboutExpansion id- T_Literal id s -> unless (s == "/") $ warnAboutLiteral id+ T_Literal id s -> unless (s == "/" || s == "=") $ warnAboutLiteral id _ -> return () check _ = return ()@@ -1949,6 +2010,10 @@ "declare" -> if "x" `elem` getFlags base then concatMap getReference rest else []+ "trap" ->+ case rest of+ head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head+ _ -> [] _ -> [] where getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]@@ -1969,6 +2034,7 @@ "declare" -> concatMap getModifierParam rest "typeset" -> concatMap getModifierParam rest "local" -> concatMap getModifierParam rest+ "set" -> [(base, base, "@", DataFrom rest)] _ -> [] where@@ -2010,11 +2076,40 @@ (t, t, getBracedReference str) : map (\x -> (l, l, x)) (getIndexReferences str) TA_Expansion id _ -> maybeToList $ do- str <- getLiteralStringExt (const $ return "#") t+ str <- getLiteralStringExt literalizer t+ guard . not $ null str+ when (isDigit $ head str) $ fail "is a number" return (t, t, getBracedReference str)- T_Assignment id Append str _ _ -> [(t, t, str)]+ T_Assignment id mode str _ word ->+ [(t, t, str) | mode == Append] ++ specialReferences str t word 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++-- 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) $ getAllMatches 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@@ -2095,6 +2190,7 @@ prop_checkSpacefulnessJ = verifyTree checkSpacefulness "echo $PWD" prop_checkSpacefulnessK = verifyNotTree checkSpacefulness "n+='foo bar'" prop_checkSpacefulnessL = verifyNotTree checkSpacefulness "select foo in $bar; do true; done"+prop_checkSpacefulnessM = verifyNotTree checkSpacefulness "echo $\"$1\"" checkSpacefulness params t = doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)@@ -2166,6 +2262,7 @@ 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" checkQuotesInLiterals params t = doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) where@@ -2173,7 +2270,7 @@ setQuotes name ref = modify $ Map.insert name ref deleteQuotes = modify . Map.delete parents = parentMap params- quoteRegex = mkRegex "\"|([= ]|^)'|'( |$)|\\\\ "+ quoteRegex = mkRegex "\"|([/= ]|^)'|'( |$)|\\\\ " containsQuotes s = s `matches` quoteRegex writeF _ _ name (DataFrom values) = do@@ -2218,6 +2315,8 @@ 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@@ -2246,14 +2345,14 @@ let string = concat $ deadSimple arg in when ('=' `elem` string) $ modify ((takeWhile (/= '=') string, getId arg):)- checkArg cmd arg =- case Map.lookup (concat $ deadSimple arg) functions of- Nothing -> return ()- Just id -> do- warn (getId arg) 2033- "Shell functions can't be passed to external commands."- info id 2032 $- "Use own script or sh -c '..' to run this from " ++ cmd ++ "."+ 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"@@ -2275,7 +2374,9 @@ 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"-checkUnusedAssignments params t = snd $ runWriter (mapM_ checkAssignment flow)+prop_checkUnused20= verifyNotTree checkUnusedAssignments "a=1; PS1='$a'"+prop_checkUnused21= verifyNotTree checkUnusedAssignments "a=1; trap 'echo $a' INT"+checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) where flow = variableFlow params references = foldl (flip ($)) defaultMap (map insertRef flow)@@ -2283,14 +2384,17 @@ Map.insert (stripSuffix name) () insertRef _ = id - checkAssignment (Assignment (_, token, name, _)) | isVariableName name =- case Map.lookup name references of- Just _ -> return ()- Nothing ->- info (getId token) 2034 $- name ++ " appears unused. Verify it or export it."- checkAssignment _ = return ()+ 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) =+ info (getId token) 2034 $+ name ++ " appears unused. Verify it or export it."+ stripSuffix = takeWhile isVariableChar defaultMap = Map.fromList $ zip internalVariables $ repeat () @@ -2619,6 +2723,10 @@ 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;" checkUnpassedInFunctions params root = execWriter $ mapM_ warnForGroup referenceGroups where@@ -2626,15 +2734,23 @@ functionMap = Map.fromList $ map (\t@(T_Function _ _ _ name _) -> (name,t)) functions functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root- findFunction t@(T_DollarBraced id token) = do- str <- getLiteralString token- unless (isPositional str) $ fail "Not positional"- let path = getPath (parentMap params) t- find isFunction path++ findFunction t@(T_Function id _ _ name body) =+ let flow = getVariableFlow (shellType params) (parentMap params) body+ in+ if any isPositionalReference flow && not (any isPositionalAssignment flow)+ then return t+ else Nothing findFunction _ = Nothing - isFunction (T_Function {}) = True- isFunction _ = False+ isPositionalAssignment x =+ case x of+ Assignment (_, _, str, _) -> isPositional str+ _ -> False+ isPositionalReference x =+ case x of+ Reference (_, _, str) -> isPositional str+ _ -> False referenceList :: [(String, Bool, Token)] referenceList = execWriter $@@ -2711,7 +2827,23 @@ 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_checkUnsupported1 = verifyNot checkUnsupported "#!/bin/zsh\nfunction { echo cow; }" prop_checkUnsupported2 = verify checkUnsupported "#!/bin/sh\nfunction { echo cow; }" prop_checkUnsupported3 = verify checkUnsupported "#!/bin/sh\ncase foo in bar) baz ;& esac"@@ -2867,14 +2999,14 @@ prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'" checkFindActionPrecedence params = checkCommand "find" (const f) where- pattern = [isMatch, const True, isParam ["-o"], isMatch, const True, isAction]+ pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction] f list | length list < length pattern = return () f list@(_:rest) = if all id (zipWith ($) pattern list) then warnFor (list !! ((length pattern)-1)) else f rest- isMatch = isParam [ "-name", "-regex", "-iname", "-iregex" ]- isAction = isParam [ "-exec", "-execdir", "-delete", "-print" ]+ 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
+ ShellCheck/Options.hs view
@@ -0,0 +1,14 @@+module ShellCheck.Options where++data Shell = Ksh | Zsh | Sh | Bash+ deriving (Show, Eq)++data AnalysisOptions = AnalysisOptions {+ optionShellType :: Maybe Shell,+ optionExcludes :: [Integer]+}++defaultAnalysisOptions = AnalysisOptions {+ optionShellType = Nothing,+ optionExcludes = []+}
ShellCheck/Parser.hs view
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. -} {-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell #-}-module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote, runTests) where+module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote, runTests, readScript) where import ShellCheck.AST import ShellCheck.Data@@ -61,13 +61,13 @@ prop_spacing = isOk spacing " \\\n # Comment" spacing = do- x <- many (many1 linewhitespace <|> try (string "\\\n"))+ x <- many (many1 linewhitespace <|> try (string "\\\n" >> return "")) optional readComment return $ concat x spacing1 = do spacing <- spacing- when (null spacing) $ fail "no spacing"+ when (null spacing) $ fail "Expected whitespace" return spacing prop_allspacing = isOk allspacing "#foo"@@ -84,7 +84,7 @@ allspacingOrFail = do s <- allspacing- when (null s) $ fail "Expected spaces"+ when (null s) $ fail "Expected whitespace" unicodeDoubleQuote = do pos <- getPosition@@ -183,9 +183,9 @@ then do let (a:r) = v setCurrentContexts r- return [a]+ return $ Just a else- return []+ return Nothing pushContext c = do v <- getCurrentContexts@@ -233,8 +233,8 @@ attempting rest branch = (try branch >> rest) <|> rest -orFail parser stuff =- try (disregard parser) <|> (disregard stuff >> fail "nope")+orFail parser errorAction =+ try parser <|> (errorAction >>= fail) wasIncluded p = option False (p >> return True) @@ -252,7 +252,7 @@ popContext return v <|> do -- p failed without consuming input, abort context- popContext+ v <- popContext fail "" called s p = do@@ -270,12 +270,22 @@ parseProblemAt pos WarningC 1009 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.") where+ spacingOrLf = condSpacing True+ condSpacing required = do+ pos <- getPosition+ space <- allspacing+ when (required && null space) $+ parseProblemAt pos ErrorC 1035 "You are missing a required space here."+ when (single && '\n' `elem` space) $+ parseProblemAt pos ErrorC 1080 "When breaking lines in [ ], you need \\ before the linefeed."+ return space+ typ = if single then SingleBracket else DoubleBracket readCondBinaryOp = try $ do optional guardArithmetic id <- getNextId op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp- hardCondSpacing+ spacingOrLf return op where tryOp s = try $ do@@ -285,7 +295,7 @@ otherOp = try $ do id <- getNextId s <- readOp- when (s == "-a" || s == "-o") $ fail "Wrong operator"+ when (s == "-a" || s == "-o") $ fail "Unexpected operator" return $ TC_Binary id typ s guardArithmetic = do@@ -298,17 +308,14 @@ readCondUnaryExp = do op <- readCondUnaryOp pos <- getPosition- (do- arg <- readCondWord- return $ op arg)- <|> (do- parseProblemAt pos ErrorC 1019 "Expected this to be an argument to the unary condition."- fail "oops")+ (readCondWord >>= return . op) `orFail` do+ parseProblemAt pos ErrorC 1019 "Expected this to be an argument to the unary condition."+ return "Expected an argument for the unary operator" readCondUnaryOp = try $ do id <- getNextId s <- readOp- hardCondSpacing+ spacingOrLf return $ TC_Unary id typ s readOp = try $ do@@ -337,19 +344,20 @@ readCondAndOp = do id <- getNextId- x <- try (string "&&" <|> string "-a")- softCondSpacing- skipLineFeeds+ x <- try (readAndOrOp "&&" False <|> readAndOrOp "-a" True) return $ TC_And id typ x readCondOrOp = do optional guardArithmetic id <- getNextId- x <- try (string "||" <|> string "-o")- softCondSpacing- skipLineFeeds+ x <- try (readAndOrOp "||" False <|> readAndOrOp "-o" True) return $ TC_Or id typ x + readAndOrOp op requiresSpacing = do+ x <- string op+ condSpacing requiresSpacing+ return x+ readCondNoaryOrBinary = do id <- getNextId x <- readCondWord `attempting` (do@@ -373,16 +381,21 @@ id <- getNextId pos <- getPosition lparen <- try $ string "(" <|> string "\\("- when (single && lparen == "(") $ parseProblemAt pos ErrorC 1028 "In [..] you have to escape (). Use [[..]] instead."- when (not single && lparen == "\\(") $ parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ()."- if single then hardCondSpacing else disregard spacing+ when (single && lparen == "(") $+ parseProblemAt pos ErrorC 1028 "In [..] you have to escape (). Use [[..]] instead."+ when (not single && lparen == "\\(") $+ parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ()."+ condSpacing single x <- readCondContents cpos <- getPosition rparen <- string ")" <|> string "\\)"- if single then hardCondSpacing else disregard spacing- when (single && rparen == ")") $ parseProblemAt cpos ErrorC 1030 "In [..] you have to escape (). Use [[..]] instead."- when (not single && rparen == "\\)") $ parseProblemAt cpos ErrorC 1031 "In [[..]] you shouldn't escape ()."- when (isEscaped lparen `xor` isEscaped rparen) $ parseProblemAt pos ErrorC 1032 "Did you just escape one half of () but not the other?"+ condSpacing single+ when (single && rparen == ")") $+ parseProblemAt cpos ErrorC 1030 "In [..] you have to escape (). Use [[..]] instead."+ when (not single && rparen == "\\)") $+ parseProblemAt cpos ErrorC 1031 "In [[..]] you shouldn't escape ()."+ when (isEscaped lparen `xor` isEscaped rparen) $+ parseProblemAt pos ErrorC 1032 "Did you just escape one half of () but not the other?" return $ TC_Group id typ x where isEscaped ('\\':_) = True@@ -426,21 +439,15 @@ str <- string "|" return $ T_Literal id str - skipLineFeeds = do- pos <- getPosition- spacing <- allspacing- when (single && '\n' `elem` spacing) $- parseProblemAt pos ErrorC 1080 "In [ ] you need \\ before line feeds."- readCondTerm = do term <- readCondNot <|> readCondExpr- skipLineFeeds+ condSpacing False return term readCondNot = do id <- getNextId char '!'- softCondSpacing+ spacingOrLf expr <- readCondExpr return $ TC_Unary id typ "!" expr @@ -452,7 +459,6 @@ readCondContents = readCondOr - prop_a1 = isOk readArithmeticContents " n++ + ++c" prop_a2 = isOk readArithmeticContents "$N*4-(3,2)" prop_a3 = isOk readArithmeticContents "n|=2<<1"@@ -489,10 +495,10 @@ readArrayIndex = do id <- getNextId- start <- literal "["+ char '[' middle <- readArithmeticContents- end <- literal "]"- return $ T_NormalWord id [start, middle, end]+ char ']'+ return $ TA_Index id middle literal s = do id <- getNextId@@ -596,7 +602,7 @@ id <- getNextId op <- try $ string "++" <|> string "--" spacing- return $ TA_Unary id ("|" ++ op) x+ return $ TA_Unary id ('|':op) x <|> return x @@ -613,8 +619,10 @@ prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]" prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]" prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"-prop_readCondition10= isOk readCondition "[[ a == b \n || c == d ]]"-prop_readCondition11= isOk readCondition "[[ a == b || \n c == d ]]"+prop_readCondition10= isOk readCondition "[[\na == b\n||\nc == d ]]"+prop_readCondition10a= isOk readCondition "[[\na == b ||\nc == d ]]"+prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"+prop_readCondition11= isOk readCondition "[[ a == b ||\n c == d ]]" prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]" prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]" readCondition = called "test expression" $ do@@ -622,9 +630,17 @@ id <- getNextId open <- try (string "[[") <|> string "[" let single = open == "["- condSpacingMsg False $ if single- then "You need spaces after the opening [ and before the closing ]."- else "You need spaces after the opening [[ and before the closing ]]."++ pos <- getPosition+ space <- allspacing+ when (null space) $+ parseProblemAt pos ErrorC 1035 $ "You need a space after the " +++ if single+ then "[ and before the ]."+ else "[[ and before the ]]."+ when (single && '\n' `elem` space) $+ parseProblemAt pos ErrorC 1080 "You need \\ before line feeds to break lines in [ ]."+ condition <- readConditionContents single cpos <- getPosition@@ -635,14 +651,6 @@ many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme? return $ T_Condition id (if single then SingleBracket else DoubleBracket) condition --hardCondSpacing = condSpacingMsg False "You need a space here."-softCondSpacing = condSpacingMsg True "You need a space here."-condSpacingMsg soft msg = do- pos <- getPosition- space <- spacing- when (null space) $ (if soft then parseNoteAt else parseProblemAt) pos ErrorC 1035 msg- readAnnotationPrefix = do char '#' many linewhitespace@@ -811,6 +819,8 @@ prop_readBackTicked4 = isOk readBackTicked "`echo foo\necho bar`" prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar" prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar"+prop_readBackTicked7 = isOk readSimpleCommand "`#inline comment`"+prop_readBackTicked8 = isOk readSimpleCommand "echo `#comment` \\\nbar baz" readBackTicked = called "backtick expansion" $ do id <- getNextId startPos <- getPosition@@ -826,7 +836,7 @@ suggestForgotClosingQuote startPos endPos "backtick expansion" -- Result positions may be off due to escapes- result <- subParse subStart readCompoundList (unEscape subString)+ result <- subParse subStart readTermOrNone (unEscape subString) return $ T_Backticked id result where unEscape [] = []@@ -1390,6 +1400,12 @@ then andOr else T_Annotation aid annotations andOr +readTermOrNone = do+ allspacing+ readTerm <|> do+ eof+ return []+ readTerm = do allspacing m <- readAndOr@@ -1460,6 +1476,7 @@ g_Fi `orFail` do parseProblemAt pos ErrorC 1046 "Couldn't find 'fi' for this 'if'." parseProblem ErrorC 1047 "Expected 'fi' matching previously mentioned 'if'."+ return "Expected 'fi'." return $ T_IfExpression id ((condition, action):elifs) elses @@ -1475,12 +1492,13 @@ allspacing condition <- readTerm - optional (do- try . lookAhead $ g_Fi- parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'if'?")+ ifNextToken (g_Fi <|> g_Elif) $+ parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'if'?" called "then clause" $ do- g_Then `orFail` parseProblem ErrorC 1050 "Expected 'then'."+ g_Then `orFail` do+ parseProblem ErrorC 1050 "Expected 'then'."+ return "Expected 'then'." acceptButWarn g_Semi ErrorC 1051 "No semicolons directly after 'then'." allspacing@@ -1496,6 +1514,10 @@ parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'." allspacing condition <- readTerm++ ifNextToken (g_Fi <|> g_Elif) $+ parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?"+ g_Then acceptButWarn g_Semi ErrorC 1052 "No semicolons directly after 'then'." allspacing@@ -1514,6 +1536,11 @@ verifyNotEmptyIf "else" readTerm +ifNextToken parser action =+ optional $ do+ try . lookAhead $ parser+ action+ prop_readSubshell = isOk readSubshell "( cd /foo; tar cf stuff.tar * )" readSubshell = called "explicit subshell" $ do id <- getNextId@@ -1537,7 +1564,7 @@ list <- readTerm char '}' <|> do parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."- fail "Unable to parse"+ fail "Missing '}'" return $ T_BraceGroup id list prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"@@ -1562,7 +1589,9 @@ try . lookAhead $ g_Done parseProblemAt loopPos ErrorC 1057 "Did you forget the 'do' for this loop?") - g_Do `orFail` parseProblem ErrorC 1058 "Expected 'do'."+ g_Do `orFail` do+ parseProblem ErrorC 1058 "Expected 'do'."+ return "Expected 'do'." acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'." allspacing@@ -1575,6 +1604,7 @@ g_Done `orFail` do parseProblemAt pos ErrorC 1061 "Couldn't find 'done' for this 'do'." parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."+ return "Expected 'done'." return commands @@ -1588,11 +1618,12 @@ prop_readForClause9 = isOk readForClause "for i do true; done" prop_readForClause10= isOk readForClause "for ((;;)) { true; }" prop_readForClause11= isOk readForClause "for a b in *; do echo $a $b; done"+prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done" readForClause = called "for loop" $ do pos <- getPosition (T_For id) <- g_For spacing- readRegular id pos <|> readArithmetic id pos+ readArithmetic id pos <|> readRegular id pos where readArithmetic id pos = called "arithmetic for condition" $ do try $ string "(("@@ -1613,6 +1644,8 @@ return list readRegular id pos = do+ acceptButWarn (char '$') ErrorC 1086+ "Don't use $ on the iterator name in for loops." names <- readNames readShort names <|> readLong names where@@ -1687,7 +1720,10 @@ optional g_Lparen spacing pattern <- readPattern- g_Rparen+ void g_Rparen <|> do+ parseProblem ErrorC 1085+ "Did you forget to move the ;; after extending this case item?"+ fail "Expected ) to open a new case item" readLineBreak list <- (lookAhead readCaseSeparator >> return []) <|> readCompoundList separator <- readCaseSeparator `attempting` do@@ -1793,8 +1829,7 @@ -- Get whatever a parser would parse as a string readStringForParser parser = do pos <- lookAhead (parser >> getPosition)- s <- readUntil pos- return s+ readUntil pos where readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos)) @@ -1809,6 +1844,7 @@ prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= " prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"+prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'" readAssignmentWord = try $ do id <- getNextId pos <- getPosition@@ -1844,14 +1880,10 @@ id <- getNextId return $ T_Literal id "" --- This is only approximate. Fixme?--- * Bash allows foo[' ' "" $(true) 2 ``]=var--- * foo[bar] dereferences bar readArrayIndex = do char '[' optional space- x <- readNormalishWord "]"- optional space+ x <- readArithmeticContents char ']' return x @@ -1903,8 +1935,8 @@ "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'." return $ t id -anycaseString =- mapM anycaseChar+anycaseString str =+ mapM anycaseChar str <?> str where anycaseChar c = char (toLower c) <|> char (toUpper c) @@ -1942,10 +1974,12 @@ g_Bang = do id <- getNextId char '!'- softCondSpacing+ void spacing1 <|> do+ pos <- getPosition+ parseProblemAt pos ErrorC 1035+ "You are missing a required space after the !." return $ T_Bang id - g_Semi = do notFollowedBy2 g_DSEMI tryToken ";" T_Semi@@ -1958,12 +1992,21 @@ ifParse p t f = (lookAhead (try p) >> t) <|> f +prop_readShebang1 = isOk readShebang "#!/bin/sh\n"+prop_readShebang2 = isWarning readShebang "!# /bin/sh\n" readShebang = do- try $ string "#!"+ try readCorrect <|> try readSwapped str <- many $ noneOf "\r\n" optional carriageReturn optional linefeed return str+ where+ readCorrect = void $ string "#!"+ readSwapped = do+ pos <- getPosition+ string "!#"+ parseProblemAt pos ErrorC 1084+ "Use #!, not !#, for the shebang." prop_readScript1 = isOk readScript "#!/bin/bash\necho hello world\n" prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"@@ -1987,11 +2030,11 @@ return $ T_Script id sb commands; } <|> do { parseProblem WarningC 1014 "Couldn't read any commands.";- return $ T_Script id sb [T_EOF id];+ return $ T_Script id sb [] } else do many anyChar- return $ T_Script id sb [T_EOF id];+ return $ T_Script id sb []; where basename s = reverse . takeWhile (/= '/') . reverse $ s@@ -2060,20 +2103,21 @@ data ParseResult = ParseResult { parseResult :: Maybe (Token, Map.Map Id SourcePos), parseNotes :: [ParseNote] } deriving (Show) makeErrorFor parsecError =- ParseNote (errorPos parsecError) ErrorC 1072 $ getStringFromParsec $ errorMessages parsecError+ ParseNote (errorPos parsecError) ErrorC 1072 $+ getStringFromParsec $ errorMessages parsecError getStringFromParsec errors =- case map snd $ sortWith fst $ map f errors of- r -> unwords (take 1 $ nub r) ++ " Fix any mentioned problems and try again."- where f err =+ case map f errors of+ r -> unwords (take 1 $ catMaybes $ reverse r) +++ " Fix any mentioned problems and try again."+ where+ f err = case err of- UnExpect s -> (1, unexpected s)- SysUnExpect s -> (2, unexpected s)- Expect s -> (3, "Expected " ++ s ++ ".")- Message s -> (4, s ++ ".")- wut "" = "eof"- wut x = x- unexpected s = "Unexpected " ++ wut s ++ "."+ UnExpect s -> return $ unexpected s+ SysUnExpect s -> return $ unexpected s+ Expect s -> return $ "Expected " ++ s ++ "."+ Message s -> if null s then Nothing else return $ s ++ "."+ unexpected s = "Unexpected " ++ (if null s then "eof" else s) ++ "." parseShell filename contents = case rp (parseWithNotes readScript) filename contents of
ShellCheck/Simple.hs view
@@ -18,15 +18,16 @@ {-# LANGUAGE TemplateHaskell #-} module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage, runTests) where -import ShellCheck.Parser hiding (runTests)-import ShellCheck.Analytics hiding (runTests)-import Data.Maybe-import Text.Parsec.Pos import Data.List+import Data.Maybe+import ShellCheck.Analytics hiding (runTests)+import ShellCheck.Options+import ShellCheck.Parser hiding (runTests) import Test.QuickCheck.All (quickCheckAll)+import Text.Parsec.Pos -shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment]-shellCheck script options =+shellCheck :: AnalysisOptions -> String -> [ShellCheckComment]+shellCheck options script = let (ParseResult result notes) = parseShell "-" script in let allNotes = notes ++ concat (maybeToList $ do (tree, posMap) <- result@@ -51,21 +52,25 @@ formatNote (ParseNote pos severity code text) = ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text +testCheck = shellCheck defaultAnalysisOptions { optionExcludes = [2148] } -- Ignore #! warnings prop_findsParseIssue =- let comments = shellCheck "echo \"$12\"" [] in+ let comments = testCheck "echo \"$12\"" in length comments == 1 && scCode (head comments) == 1037 prop_commentDisablesParseIssue1 =- null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []+ null $ testCheck "#shellcheck disable=SC1037\necho \"$12\"" prop_commentDisablesParseIssue2 =- null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []+ null $ testCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" prop_findsAnalysisIssue =- let comments = shellCheck "echo $1" [] in+ let comments = testCheck "echo $1" in length comments == 1 && scCode (head comments) == 2086 prop_commentDisablesAnalysisIssue1 =- null $ shellCheck "#shellcheck disable=SC2086\necho $1" []+ null $ testCheck "#shellcheck disable=SC2086\necho $1" prop_commentDisablesAnalysisIssue2 =- null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []+ null $ testCheck "#shellcheck disable=SC2086\n#lol\necho $1"++prop_optionDisablesIssue1 =+ null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2086, 2148] }) "echo $1" return [] runTests = $quickCheckAll
shellcheck.1.md view
@@ -18,23 +18,27 @@ # OPTIONS -**-f** *FORMAT*, **--format=***FORMAT*--: Specify the output format of shellcheck, which prints its results in the- standard output. Subsequent **-f** options are ignored, see **FORMATS**- below for more information.- **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...] : Explicitly exclude the specified codes from the report. Subsequent **-e** options are cumulative, but all the codes can be specified at once, comma-separated as a single argument. +**-f** *FORMAT*, **--format=***FORMAT*++: Specify the output format of shellcheck, which prints its results in the+ standard output. Subsequent **-f** options are ignored, see **FORMATS**+ below for more information.+ **-s**\ *shell*,\ **--shell=***shell* : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *ksh* and *zsh*. The default is to use the file's shebang, or *bash* if the target shell can't be determined.++**-V**\ *version*,\ **--version**++: Print version and exit. # FORMATS
shellcheck.hs view
@@ -17,12 +17,16 @@ -} import Control.Exception import Control.Monad+import Control.Monad.Trans+import Control.Monad.Trans.Error import Data.Char import Data.Maybe+import Data.Monoid import GHC.Exts import GHC.IO.Device import Prelude hiding (catch) import ShellCheck.Data+import ShellCheck.Options import ShellCheck.Simple import ShellCheck.Analytics import System.Console.GetOpt@@ -34,23 +38,29 @@ import qualified Data.Map as Map data Flag = Flag String String+data Status = NoProblems | SomeProblems | BadInput | SupportFailure | SyntaxFailure | RuntimeException deriving (Ord, Eq) +instance Error Status where+ noMsg = RuntimeException++instance Monoid Status where+ mempty = NoProblems+ mappend = max+ header = "Usage: shellcheck [OPTIONS...] FILES..." options = [- Option ['f'] ["format"]- (ReqArg (Flag "format") "FORMAT") "output format",- Option ['e'] ["exclude"]+ Option "e" ["exclude"] (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",- Option ['s'] ["shell"]+ Option "f" ["format"]+ (ReqArg (Flag "format") "FORMAT") "output format",+ Option "s" ["shell"] (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)",- Option ['V'] ["version"]+ Option "V" ["version"] (NoArg $ Flag "version" "true") "Print version information" ] printErr = hPutStrLn stderr -syntaxFailure = ExitFailure 3-supportFailure = ExitFailure 4 instance JSON ShellCheckComment where showJSON c = makeObj [@@ -62,16 +72,15 @@ ] readJSON = undefined +parseArguments :: [String] -> ErrorT Status IO ([Flag], [FilePath]) parseArguments argv = case getOpt Permute options argv of- (opts, files, []) -> do- verifyOptions opts files- return $ Just (opts, files)-+ (opts, files, []) -> return (opts, files) (_, _, errors) -> do- printErr $ concat errors ++ "\n" ++ usageInfo header options- exitWith syntaxFailure+ liftIO . printErr $ concat errors ++ "\n" ++ usageInfo header options+ throwError SyntaxFailure +formats :: Map.Map String (AnalysisOptions -> [FilePath] -> IO Status) formats = Map.fromList [ ("json", forJson), ("gcc", forGcc),@@ -79,9 +88,21 @@ ("tty", forTty) ] +toStatus = liftM (either id (const NoProblems)) . runErrorT++catchExceptions :: IO Status -> IO Status+catchExceptions action = action -- action `catch` handler+ where+ handler err = do+ printErr $ show (err :: SomeException)+ return RuntimeException++checkComments comments = if null comments then NoProblems else SomeProblems++forTty :: AnalysisOptions -> [FilePath] -> IO Status forTty options files = do output <- mapM doFile files- return $ and output+ return $ mconcat output where clear = ansi 0 ansi n = "\x1B[" ++ show n ++ "m"@@ -97,7 +118,7 @@ colorComment level comment = ansi (colorForLevel level) ++ comment ++ clear - doFile path = do+ doFile path = catchExceptions $ do contents <- readContents path doInput path contents @@ -119,34 +140,36 @@ mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x putStrLn "" ) groups- return $ null comments+ return . checkComments $ comments cuteIndent comment = replicate (scColumn comment - 1) ' ' ++ "^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment - code code = "SC" ++ (show code)+ code code = "SC" ++ show code getColorFunc = do term <- hIsTerminalDevice stdout return $ if term then colorComment else const id -- This totally ignores the filenames. Fixme?-forJson options files = do+forJson :: AnalysisOptions -> [FilePath] -> IO Status+forJson options files = catchExceptions $ do comments <- liftM concat $ mapM (commentsFor options) files putStrLn $ encodeStrict comments- return . null $ comments+ return $ checkComments comments -- Mimic GCC "file:line:col: (error|warning|note): message" format+forGcc :: AnalysisOptions -> [FilePath] -> IO Status forGcc options files = do files <- mapM process files- return $ and files+ return $ mconcat files where- process file = do+ process file = catchExceptions $ do contents <- readContents file let comments = makeNonVirtual (getComments options contents) contents mapM_ (putStrLn . format file) comments- return $ null comments+ return $ checkComments comments format filename c = concat [ filename, ":",@@ -162,20 +185,18 @@ ] -- Checkstyle compatible output. A bit of a hack to avoid XML dependencies+forCheckstyle :: AnalysisOptions -> [FilePath] -> IO Status forCheckstyle options files = do putStrLn "<?xml version='1.0' encoding='UTF-8'?>" putStrLn "<checkstyle version='4.3'>"- statuses <- mapM (\x -> process x `catch` report) files+ statuses <- mapM process files putStrLn "</checkstyle>"- return $ and statuses+ return $ mconcat statuses where- process file = do+ process file = catchExceptions $ do comments <- commentsFor options file putStrLn (formatFile file comments)- return $ null comments- report error = do- printErr $ show (error :: SomeException)- return False+ return $ checkComments comments severity "error" = "error" severity "warning" = "warning"@@ -197,31 +218,31 @@ attr "column" $ show . scColumn $ c, attr "severity" $ severity . scSeverity $ c, attr "message" $ scMessage c,- attr "source" $ "ShellCheck.SC" ++ (show $ scCode c),+ attr "source" $ "ShellCheck.SC" ++ show (scCode c), "/>\n" ] -commentsFor options file =- liftM (getComments options) $ readContents file--getComments options contents =- excludeCodes (getExclusions options) $ shellCheck contents analysisOptions- where- analysisOptions = catMaybes [ shellOption ]- shellOption = do- option <- getOption options "shell"- sh <- shellForExecutable option- return $ ForceShell sh+commentsFor options file = liftM (getComments options) $ readContents file +getComments = shellCheck -readContents file = if file == "-" then getContents else readFile file+readContents :: FilePath -> IO String+readContents file =+ if file == "-"+ then getContents+ else readFile file -- Realign comments from a tabstop of 8 to 1 makeNonVirtual comments contents = map fix comments where ls = lines contents- fix c = c { scColumn = real (ls !! (scLine c - 1)) 0 0 (scColumn c) }+ fix c = c {+ scColumn =+ if scLine c > 0 && scLine c <= length ls+ then real (ls !! (scLine c - 1)) 0 0 (scColumn c)+ else scColumn c+ } real _ r v target | target <= v = r real [] r v _ = r -- should never happen real ('\t':rest) r v target =@@ -240,7 +261,7 @@ where split' (a:rest) element = if a == char- then (reverse element) : split' rest []+ then reverse element : split' rest [] else split' rest (a:element) split' [] element = [reverse element] @@ -257,45 +278,71 @@ main = do args <- getArgs- parsedArgs <- parseArguments args- code <- do- status <- process parsedArgs- return $ if status then ExitSuccess else ExitFailure 1- `catch` return- `catch` \err -> do- printErr $ show (err :: SomeException)- return $ ExitFailure 2- exitWith code+ status <- toStatus $ do+ (flags, files) <- parseArguments args+ process flags files+ exitWith $ statusToCode status -process Nothing = return False-process (Just (options, files)) =- let format = fromMaybe "tty" $ getOption options "format" in+statusToCode status =+ case status of+ NoProblems -> ExitSuccess+ SomeProblems -> ExitFailure 1+ BadInput -> ExitFailure 5+ SyntaxFailure -> ExitFailure 3+ SupportFailure -> ExitFailure 4+ RuntimeException -> ExitFailure 2++process :: [Flag] -> [FilePath] -> ErrorT Status IO ()+process flags files = do+ options <- foldM (flip parseOption) defaultAnalysisOptions flags+ verifyFiles files+ let format = fromMaybe "tty" $ getOption flags "format" case Map.lookup format formats of Nothing -> do- printErr $ "Unknown format " ++ format- printErr $ "Supported formats:"- mapM_ (printErr . write) $ Map.keys formats- exitWith supportFailure+ liftIO $ do+ printErr $ "Unknown format " ++ format+ printErr "Supported formats:"+ mapM_ (printErr . write) $ Map.keys formats+ throwError SupportFailure where write s = " " ++ s- Just f -> do- f options files+ Just f -> ErrorT $ liftM Left $ f options files -verifyOptions opts files = do- when (isJust $ getOption opts "version") printVersionAndExit+parseOption flag options =+ case flag of+ Flag "shell" str ->+ fromMaybe (die $ "Unknown shell: " ++ str) $ do+ shell <- shellForExecutable str+ return $ return options { optionShellType = Just shell } - let shell = getOption opts "shell" in- when (isJust shell && isNothing (shell >>= shellForExecutable)) $ do- printErr $ "Unknown shell: " ++ (fromJust shell)- exitWith supportFailure+ Flag "exclude" str -> do+ new <- mapM parseNum $ split ',' str+ let old = optionExcludes options+ return options { optionExcludes = new ++ old } + Flag "version" _ -> do+ liftIO printVersion+ throwError NoProblems++ _ -> return options+ where+ die s = do+ liftIO $ printErr s+ throwError SupportFailure+ parseNum ('S':'C':str) = parseNum str+ parseNum num = do+ unless (all isDigit num) $ do+ liftIO . printErr $ "Bad exclusion: " ++ num+ throwError SyntaxFailure+ return (Prelude.read num :: Integer)++verifyFiles files = when (null files) $ do- printErr "No files specified.\n"- printErr $ usageInfo header options- exitWith syntaxFailure+ liftIO $ printErr "No files specified.\n"+ liftIO $ printErr $ usageInfo header options+ throwError SyntaxFailure -printVersionAndExit = do- putStrLn $ "ShellCheck - shell script analysis tool"+printVersion = do+ putStrLn "ShellCheck - shell script analysis tool" putStrLn $ "version: " ++ shellcheckVersion- putStrLn $ "license: GNU Affero General Public License, version 3"- putStrLn $ "website: http://www.shellcheck.net"- exitWith ExitSuccess+ putStrLn "license: GNU Affero General Public License, version 3"+ putStrLn "website: http://www.shellcheck.net"