ShellCheck 0.3.1 → 0.3.2
raw patch · 6 files changed
+674/−214 lines, 6 filesdep +ShellCheckdep ~basenew-uploader
Dependencies added: ShellCheck
Dependency ranges changed: base
Files
- ShellCheck.cabal +25/−4
- ShellCheck/AST.hs +16/−14
- ShellCheck/Analytics.hs +504/−158
- ShellCheck/Data.hs +1/−1
- ShellCheck/Parser.hs +109/−19
- shellcheck.hs +19/−18
ShellCheck.cabal view
@@ -1,6 +1,6 @@ Name: ShellCheck -- Must also be updated in ShellCheck/Data.hs :-Version: 0.3.1+Version: 0.3.2 Synopsis: Shell script analysis tool License: OtherLicense License-file: LICENSE@@ -9,7 +9,7 @@ Maintainer: vidar@vidarholen.net Homepage: http://www.shellcheck.net/ Build-Type: Simple-Cabal-Version: >= 1.6+Cabal-Version: >= 1.8 Bug-reports: https://github.com/koalaman/shellcheck/issues Description: The goals of ShellCheck are:@@ -28,8 +28,29 @@ location: git://github.com/koalaman/shellcheck.git library- build-depends: base >= 4, base < 5, parsec, containers, regex-compat, mtl, directory, json- exposed-modules: ShellCheck.AST, ShellCheck.Data, ShellCheck.Parser, ShellCheck.Analytics, ShellCheck.Simple+ build-depends:+ base >= 4 && < 5,+ containers,+ directory,+ json,+ mtl,+ parsec,+ regex-compat+ exposed-modules:+ ShellCheck.Analytics+ ShellCheck.AST+ ShellCheck.Data+ ShellCheck.Parser+ ShellCheck.Simple executable shellcheck+ build-depends:+ ShellCheck,+ base >= 4 && < 5,+ containers,+ directory,+ json,+ mtl,+ parsec,+ regex-compat main-is: shellcheck.hs
ShellCheck/AST.hs view
@@ -102,7 +102,7 @@ | T_NormalWord Id [Token] | T_OR_IF Id | T_OrIf Id (Token) (Token)- | T_Pipeline Id [Token]+ | T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands] | T_ProcSub Id String [Token] | T_Rbrace Id | T_Redirecting Id [Token] Token@@ -120,6 +120,7 @@ | T_While Id | T_WhileExpression Id [Token] [Token] | T_Annotation Id [Annotation] Token+ | T_Pipe Id String deriving (Show) data Annotation = DisableComment Integer deriving (Show, Eq)@@ -128,12 +129,12 @@ -- I apologize for nothing! lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)" instance Eq Token where- (==) a b = (lolHax a) == (lolHax b)+ (==) a b = lolHax a == lolHax b analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token-analyze f g i t =- round t+analyze f g i =+ round where round t = do f t@@ -182,7 +183,7 @@ b <- round cmd return $ T_Redirecting id a b delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id- delve (T_Pipeline id l) = dl l $ T_Pipeline id+ delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id delve (T_Banged id l) = d1 l $ T_Banged id delve (T_AndIf id t u) = d2 t u $ T_AndIf id delve (T_OrIf id t u) = d2 t u $ T_OrIf id@@ -297,7 +298,7 @@ T_Array id _ -> id T_Redirecting id _ _ -> id T_SimpleCommand id _ _ -> id- T_Pipeline id _ -> id+ T_Pipeline id _ _ -> id T_Banged id _ -> id T_AndIf id _ _ -> id T_OrIf id _ _ -> id@@ -337,17 +338,18 @@ T_DollarDoubleQuoted id _ -> id T_DollarBracket id _ -> id T_Annotation id _ _ -> id+ T_Pipe id _ -> id blank :: Monad m => Token -> m () blank = const $ return ()-doAnalysis f t = analyze f blank id t-doStackAnalysis startToken endToken t = analyze startToken endToken id t-doTransform i t = runIdentity $ analyze blank blank i t+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+ T_WhileExpression {} -> True+ T_UntilExpression {} -> True+ T_ForIn {} -> True+ T_ForArithmetic {} -> True+ T_SelectIn {} -> True _ -> False
ShellCheck/Analytics.hs view
@@ -17,20 +17,19 @@ -} module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics, shellForExecutable) where -import ShellCheck.AST-import ShellCheck.Data-import ShellCheck.Parser import Control.Monad import Control.Monad.State import Control.Monad.Writer-import qualified Data.Map as Map import Data.Char import Data.Functor import Data.List import Data.Maybe import Debug.Trace+import ShellCheck.AST+import ShellCheck.Data+import ShellCheck.Parser import Text.Regex-import Data.Maybe+import qualified Data.Map as Map data Shell = Ksh | Zsh | Sh | Bash deriving (Show, Eq)@@ -48,18 +47,20 @@ treeChecks = [ runNodeAnalysis (\p t -> mapM_ (\f -> f t) $- map (\f -> f p) (nodeChecks ++ (checksFor (shellType p))))+ map (\f -> f p) (nodeChecks ++ checksFor (shellType p))) ,subshellAssignmentCheck ,checkSpacefulness ,checkQuotesInLiterals ,checkShebang ,checkFunctionsUsedExternally ,checkUnusedAssignments+ ,checkUnpassedInFunctions ] checksFor Sh = [ checkBashisms ,checkTimeParameters+ ,checkForDecimals ] checksFor Ksh = [ checkEchoSed@@ -72,6 +73,7 @@ checkTimeParameters ,checkBraceExpansionVars ,checkEchoSed+ ,checkForDecimals ] runAnalytics :: [AnalysisOption] -> Token -> [Note]@@ -88,18 +90,20 @@ getShellOption = fromMaybe (determineShell root) . msum $- map ((\option ->+ map (\option -> case option of ForceShell x -> return x- )) options+ ) options checkList l t = concatMap (\f -> f t) l prop_determineShell0 = determineShell (T_Script (Id 0) "#!/bin/sh" []) == Sh prop_determineShell1 = determineShell (T_Script (Id 0) "#!/usr/bin/env ksh" []) == Ksh prop_determineShell2 = determineShell (T_Script (Id 0) "" []) == Bash+prop_determineShell3 = determineShell (T_Script (Id 0) "#!/bin/sh -e" []) == Sh determineShell (T_Script _ shebang _) = fromMaybe Bash . shellForExecutable $ shellFor shebang- where shellFor s | "/env " `isInfixOf` s = head ((drop 1 $ words s)++[""])+ where shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])+ shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s shellFor s = reverse . takeWhile (/= '/') . reverse $ s shellForExecutable "sh" = return Sh@@ -131,7 +135,6 @@ ,checkDoubleBracketOperators ,checkNoaryWasBinary ,checkConstantNoary- ,checkForDecimals ,checkDivBeforeMult ,checkArithmeticDeref ,checkArithmeticBadOctal@@ -144,7 +147,8 @@ ,checkTr ,checkPipedAssignment ,checkAssignAteCommand- ,checkUuoe+ ,checkUuoeCmd+ ,checkUuoeVar ,checkFindNameGlob ,checkGrepRe ,checkNeedlessCommands@@ -181,11 +185,15 @@ ,checkWrongArithmeticAssignment ,checkConditionalAndOrs ,checkFunctionDeclarations+ ,checkCatastrophicRm+ ,checkInteractiveSu+ ,checkStderrPipe+ ,checkSetAssignment ] -filterByAnnotation token notes =- filter (not . shouldIgnore) notes+filterByAnnotation token =+ filter (not . shouldIgnore) where numFor (Note _ _ code _) = code idFor (Note id _ _ _) = id@@ -206,8 +214,9 @@ info = makeNote InfoC style = makeNote StyleC -isVariableStartChar x = x == '_' || x >= 'a' && x <= 'z' || x >= 'A' && x <= 'Z'-isVariableChar x = isVariableStartChar x || x >= '0' && x <= '9'+isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x+isVariableChar x = isVariableStartChar x || isDigit x+variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*" prop_isVariableName1 = isVariableName "_fo123" prop_isVariableName2 = not $ isVariableName "4"@@ -215,19 +224,25 @@ isVariableName (x:r) = isVariableStartChar x && all isVariableChar r isVariableName _ = False +matchAll re = unfoldr f+ where+ f str = do+ (_, match, rest, _) <- matchRegexAll re str+ return $ (match, rest)+ willSplit x = case x of- T_DollarBraced _ _ -> True- T_DollarExpansion _ _ -> True- T_Backticked _ _ -> True- T_BraceExpansion _ s -> True- T_Glob _ _ -> True- T_Extglob _ _ _ -> True+ 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_Extglob {}) = True+isGlob (T_Glob {}) = True isGlob (T_NormalWord _ l) = any isGlob l isGlob _ = False @@ -268,22 +283,33 @@ makeSimple t = t simplify = doTransform makeSimple -deadSimple (T_NormalWord _ l) = [concat (concatMap (deadSimple) l)]-deadSimple (T_DoubleQuoted _ l) = [(concat (concatMap (deadSimple) l))]+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_Pipeline _ _ [x]) = deadSimple x deadSimple (T_Literal _ x) = [x]-deadSimple (T_SimpleCommand _ vars words) = concatMap (deadSimple) words+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 deadSimple _ = [] +-- Turn a SimpleCommand foo -avz --bar=baz into args ["a", "v", "z", "bar"]+getFlags (T_SimpleCommand _ _ (_:args)) =+ let textArgs = takeWhile (/= "--") $ map (concat . deadSimple) args in+ concatMap flag textArgs+ where+ flag ('-':'-':arg) = [ takeWhile (/= '=') arg ]+ flag ('-':args) = map (:[]) args+ flag _ = []++getFlags _ = []+ (!!!) list i = case drop i list of [] -> Nothing@@ -294,14 +320,14 @@ verifyTree f s = checkTree f s == Just True verifyNotTree f s = checkTree f s == Just False -checkNode f s = checkTree (runNodeAnalysis f) s+checkNode f = checkTree (runNodeAnalysis f) checkTree f s = case parseShell "-" s of (ParseResult (Just (t, m)) _) -> Just . not . null $ runList [] t [f] _ -> Nothing prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)"-checkEchoWc _ (T_Pipeline id [a, b]) =+checkEchoWc _ (T_Pipeline id _ [a, b]) = when (acmd == ["echo", "${VAR}"]) $ case bcmd of ["wc", "-c"] -> countMsg@@ -310,12 +336,12 @@ where acmd = deadSimple a bcmd = deadSimple b- countMsg = style id 2000 $ "See if you can use ${#variable} instead."+ countMsg = style id 2000 "See if you can use ${#variable} instead." checkEchoWc _ _ = return () prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"-checkEchoSed _ (T_Pipeline id [a, b]) =+checkEchoSed _ (T_Pipeline id _ [a, b]) = when (acmd == ["echo", "${VAR}"]) $ case bcmd of ["sed", v] -> checkIn v@@ -327,14 +353,15 @@ bcmd = deadSimple b checkIn s = case matchRegex sedRe s of- Just _ -> style id 2001 $ "See if you can use ${variable//search/replace} instead."+ Just _ -> style id 2001+ "See if you can use ${variable//search/replace} instead." _ -> return () checkEchoSed _ _ = return () prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo" prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo" prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo"-checkPipedAssignment _ (T_Pipeline _ (T_Redirecting _ _ (T_SimpleCommand id (_:_) []):_:_)) =+checkPipedAssignment _ (T_Pipeline _ _ (T_Redirecting _ _ (T_SimpleCommand id (_:_) []):_:_)) = warn id 2036 "If you wanted to assign the output of the pipeline, use a=$(b | c) ." checkPipedAssignment _ _ = return () @@ -343,10 +370,10 @@ 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) ||- (isCommonCommand (getLiteralString assignmentTerm)- && not (isCommonCommand (getLiteralString firstWord)))) $+checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) (firstWord:_)) =+ when ("-" `isPrefixOf` concat (deadSimple firstWord) ||+ isCommonCommand (getLiteralString assignmentTerm)+ && not (isCommonCommand (getLiteralString firstWord))) $ warn id 2037 "To assign the output of a command, use var=$(cmd) ." where isCommonCommand (Just s) = s `elem` commonCommands@@ -356,7 +383,7 @@ prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1" prop_checkArithmeticOpCommand2 = verify checkArithmeticOpCommand "foo=bar * 2" prop_checkArithmeticOpCommand3 = verifyNot checkArithmeticOpCommand "foo + opts"-checkArithmeticOpCommand _ (T_SimpleCommand id ((T_Assignment _ _ _ _ _):[]) (firstWord:_)) =+checkArithmeticOpCommand _ (T_SimpleCommand id [T_Assignment {}] (firstWord:_)) = fromMaybe (return ()) $ check <$> getGlobOrLiteralString firstWord where check op =@@ -374,9 +401,8 @@ var <- match !!! 0 op <- match !!! 1 Map.lookup var references- return $ do- warn (getId val) 2100 $- "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"+ return . warn (getId val) 2100 $+ "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" where regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$" references = foldl (flip ($)) Map.empty (map insertRef $ variableFlow params)@@ -399,7 +425,8 @@ prop_checkUuoc2 = verifyNot checkUuoc "cat * | grep bar" prop_checkUuoc3 = verify checkUuoc "cat $var | grep bar" prop_checkUuoc4 = verifyNot checkUuoc "cat $var"-checkUuoc _ (T_Pipeline _ ((T_Redirecting _ _ cmd):_:_)) = checkCommand "cat" f cmd+checkUuoc _ (T_Pipeline _ _ ((T_Redirecting _ _ cmd):_:_)) =+ checkCommand "cat" (const f) cmd where f [word] = when (isSimple word) $ style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead."@@ -421,10 +448,10 @@ prop_checkPipePitfalls4 = verifyNot checkPipePitfalls "find . -print0 | xargs -0 foo" prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo" prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo"-checkPipePitfalls _ (T_Pipeline id commands) = do+checkPipePitfalls _ (T_Pipeline id _ commands) = do for ["find", "xargs"] $ \(find:xargs:_) -> let args = deadSimple xargs in- when (not $ hasShortParameter args '0') $+ unless (hasShortParameter args '0') $ warn (getId find) 2038 "Use either 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow for non-alphanumeric filenames." for ["?", "echo"] $@@ -439,10 +466,10 @@ for' ["ls", "xargs"] $ \x -> warn x 2011 "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames." ]- when (not didLs) $ do+ unless didLs $ do for ["ls", "?"] $- \(ls:_) -> (when (not $ hasShortParameter (deadSimple ls) 'N') $- info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames.")+ \(ls:_) -> unless (hasShortParameter (deadSimple ls) 'N') $+ info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames." return () where for l f =@@ -471,7 +498,9 @@ bracedString l = concat $ deadSimple l-isMagicInQuotes (T_DollarBraced _ l) | '@' `elem` (bracedString l) = True+isMagicInQuotes (T_DollarBraced _ l) =+ let string = bracedString l in+ '@' `elem` string || "!" `isPrefixOf` string isMagicInQuotes _ = False prop_checkShebang1 = verifyTree checkShebang "#!/usr/bin/env bash -x\necho cow"@@ -501,29 +530,29 @@ prop_checkBashisms18= verify checkBashisms "foo &> /dev/null" checkBashisms _ = bashism where- errMsg id s = err id 2040 $ "#!/bin/sh was specified, so " ++ s ++ " is not supported, even when sh is actually bash."- warnMsg id s = warn id 2039 $ "#!/bin/sh was specified, but " ++ s ++ " is not standard."- bashism (T_ProcSub id _ _) = errMsg id "process substitution"- bashism (T_Extglob id _ _) = warnMsg id "extglob"- bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..'"- bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\""- bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loop"- bashism (T_Arithmetic id _) = warnMsg id "standalone ((..))"- bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..))"- bashism (T_SelectIn id _ _ _) = warnMsg id "select loop"- bashism (T_BraceExpansion id _) = warnMsg id "brace expansion"- bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]]"- bashism (T_HereString id _) = warnMsg id "here-string"+ 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."+ bashism (T_ProcSub id _ _) = errMsg id "process substitution is"+ bashism (T_Extglob id _ _) = warnMsg id "extglob is"+ bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"+ bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"+ bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"+ bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"+ bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"+ bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"+ bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"+ bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"+ bashism (T_HereString id _) = warnMsg id "here-strings are" bashism (TC_Binary id SingleBracket op _ _) | op `elem` [ "-nt", "-ef", "\\<", "\\>", "==" ] =- warnMsg id op+ warnMsg id $ op ++ " is" bashism (TA_Unary id op _) | op `elem` [ "|++", "|--", "++|", "--|"] =- warnMsg id (filter (/= '|') op)+ warnMsg id $ (filter (/= '|') op) ++ " is" bashism t@(T_SimpleCommand id _ _) | t `isCommand` "source" =- warnMsg id "'source' in place of '.'"- bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&>"+ warnMsg id "'source' in place of '.' is"+ bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is" bashism (T_DollarBraced id token) = mapM_ check expansion where@@ -533,27 +562,31 @@ bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | t `isCommand` "echo" && "-" `isPrefixOf` argString =- when (not $ "--" `isPrefixOf` argString) $ -- echo "-------"- warnMsg (getId arg) "echo flag"+ unless ("--" `isPrefixOf` argString) $ -- echo "-------"+ warnMsg (getId arg) "echo flags are" where argString = (concat $ deadSimple arg) bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | t `isCommand` "exec" && "-" `isPrefixOf` (concat $ deadSimple arg) =- warnMsg (getId arg) "exec flag"+ warnMsg (getId arg) "exec flags are" bashism t@(T_SimpleCommand id _ _)- | t `isCommand` "let" = warnMsg id "'let'"+ | t `isCommand` "let" = warnMsg id "'let' is" bashism t@(TA_Variable id "RANDOM") =- warnMsg id "RANDOM"+ warnMsg id "RANDOM is"+ bashism t@(T_Pipe id "|&") =+ warnMsg id "|& in place of 2>&1 | is"+ bashism (T_Array id _) =+ warnMsg id "arrays are" bashism _ = return() varChars="_0-9a-zA-Z" expansion = let re = mkRegex in [- (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references"),- (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion"),- (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefix"),- (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing"),- (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement"),- (re $ "^RANDOM$", "$RANDOM")+ (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),+ (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),+ (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),+ (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),+ (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is"),+ (re $ "^RANDOM$", "$RANDOM is") ] prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"@@ -564,15 +597,16 @@ prop_checkForInQuoted4 = verify checkForInQuoted "for f in 1,2,3; do true; done" prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done" prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done"+prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done" checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) = when (any (\x -> willSplit x && not (isMagicInQuotes x)) list- || (getLiteralString word >>= (return . wouldHaveBeenGlob)) == Just True) $+ || (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $ err id 2066 $ "Since you double quoted this, it will not word split, and the loop will only run once." checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id s]] _) = warn id 2041 $ "This is a literal string. To run as a command, use $(" ++ s ++ ")." checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) = if ',' `elem` s- then when (not $ '{' `elem` s) $+ then unless ('{' `elem` s) $ warn id 2042 $ "Use spaces, not commas, to separate loop elements." else warn id 2043 $ "This loop will only run once, with " ++ f ++ "='" ++ s ++ "'." checkForInQuoted _ _ = return ()@@ -584,7 +618,7 @@ prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done" checkForInCat _ (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w where- checkF (T_DollarExpansion id [T_Pipeline _ r])+ checkF (T_DollarExpansion id [T_Pipeline _ _ r]) | all isLineBased r = info id 2013 "To read lines rather than words, pipe/redirect to a 'while read' loop." checkF (T_Backticked id cmds) = checkF (T_DollarExpansion id cmds)@@ -644,9 +678,8 @@ _ -> False warnFor x =- if shouldWarn x- then info (getId x) 2014 "This will expand once before find runs, not per file found."- else return ()+ when(shouldWarn x) $+ info (getId x) 2014 "This will expand once before find runs, not per file found." fromWord (T_NormalWord _ l) = l fromWord _ = []@@ -670,7 +703,7 @@ check _ = return () tree = parentMap params examine t =- unless (inUnquotableContext tree t || usedAsCommandName tree t) $+ unless (isQuoteFree tree t || usedAsCommandName tree t) $ warn (getId t) 2046 "Quote this to prevent word splitting." @@ -679,7 +712,7 @@ prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol" prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null" prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"-checkRedirectToSame params s@(T_Pipeline _ list) =+checkRedirectToSame params s@(T_Pipeline _ _ list) = mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurences x) l) (getAllRedirs list))) list where note x = Note x InfoC 2094 $@@ -715,7 +748,7 @@ prop_checkShorthandIf2 = verifyNot checkShorthandIf "[[ ! -z file ]] && { scp file host || echo 'Eek'; }" prop_checkShorthandIf3 = verifyNot checkShorthandIf "foo && bar || echo baz" prop_checkShorthandIf4 = verifyNot checkShorthandIf "foo && a=b || a=c"-checkShorthandIf _ (T_AndIf id _ (T_OrIf _ _ (T_Pipeline _ t)))+checkShorthandIf _ (T_AndIf id _ (T_OrIf _ _ (T_Pipeline _ _ t))) | not $ isOk t = info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true." where@@ -814,28 +847,64 @@ prop_checkNumberComparisons3 = verifyNot checkNumberComparisons "[[ $foo ]] > 3" prop_checkNumberComparisons4 = verify checkNumberComparisons "[[ $foo > 2.72 ]]" prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]"-prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 = $foo ]]"-checkNumberComparisons _ (TC_Binary id typ op lhs rhs) = do- when (op `elem` ["<", ">", "<=", ">=", "\\<", "\\>", "\\<=", "\\>="]) $ do- when (isNum lhs || isNum rhs) $ err id 2071 $ "\"" ++ op ++ "\" is for string comparisons. Use " ++ (eqv op) ++" ."- mapM_ checkDecimals [lhs, rhs]+prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]"+prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]"+prop_checkNumberComparisons8 = verify checkNumberComparisons "[[ foo <= bar ]]"+prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"+prop_checkNumberComparisons10= verify checkNumberComparisons "#!/bin/zsh -x\n[ foo >= bar ]]"+checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do+ if (isNum lhs || isNum rhs)+ then do+ when (isLtGt op) $+ err id 2071 $+ op ++ " is for string comparisons. Use " ++ (eqv op) ++ " instead."+ when (isLeGe op) $+ err id 2071 $ op ++ " is not a valid operator. " +++ "Use " ++ (eqv op) ++ " ."+ else do+ when (isLeGe op || isLtGt op) $+ mapM_ checkDecimals [lhs, rhs] - when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq", "=", "=="]) $ do+ when (isLeGe op) $+ err id 2122 $ op ++ " is not a valid operator. " +++ "Use '! a " ++ (invert op) ++ " b' instead."++ when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do mapM_ checkDecimals [lhs, rhs] where- checkDecimals hs = when (isFraction hs) $ err (getId hs) 2072 $ decimalError- decimalError = "Decimals are not supported. Either use integers only, or use bc or awk to compare."- isNum t = case deadSimple t of [v] -> all isDigit v- _ -> False- isFraction t = case deadSimple t of [v] -> isJust $ matchRegex floatRegex v- _ -> False+ isLtGt = flip elem ["<", "\\<", ">", "\\>"]+ isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="]++ supportsDecimals =+ let sh = shellType params in+ sh == Ksh || sh == Zsh+ checkDecimals hs =+ when (isFraction hs && not supportsDecimals) $+ err (getId hs) 2072 decimalError+ decimalError = "Decimals are not supported. " +++ "Either use integers only, or use bc or awk to compare."++ isNum t =+ case deadSimple t of+ [v] -> all isDigit v+ _ -> False+ isFraction t =+ case deadSimple t of+ [v] -> isJust $ matchRegex floatRegex v+ _ -> False+ eqv ('\\':s) = eqv s eqv "<" = "-lt" eqv ">" = "-gt" eqv "<=" = "-le" eqv ">=" = "-ge" eqv _ = "the numerical equivalent"++ invert ('\\':s) = invert s+ invert "<=" = ">"+ invert ">=" = "<"+ floatRegex = mkRegex "^[0-9]+\\.[0-9]+$" checkNumberComparisons _ _ = return () @@ -947,9 +1016,14 @@ prop_checkArithmeticDeref4 = verifyNot checkArithmeticDeref "(( ! $? ))" prop_checkArithmeticDeref5 = verifyNot checkArithmeticDeref "(($1))" prop_checkArithmeticDeref6 = verifyNot checkArithmeticDeref "(( ${a[$i]} ))"-checkArithmeticDeref _ (TA_Expansion _ (T_DollarBraced id l)) | not . excepting $ bracedString l =- style id 2004 $ "Don't use $ on variables in (( ))."+prop_checkArithmeticDeref7 = verifyNot checkArithmeticDeref "(( 10#$n ))"+checkArithmeticDeref params t@(TA_Expansion _ (T_DollarBraced id l)) =+ when (not $ (excepting $ bracedString l) || inBaseExpression) $+ style id 2004 $ "$ on variables in (( )) is unnecessary." where+ inBaseExpression = any isBase $ parents params t+ isBase (TA_Base {}) = True+ isBase _ = False excepting [] = True excepting s = (any (`elem` "/.:#%?*@[]") s) || (isDigit $ head s) checkArithmeticDeref _ _ = return ()@@ -975,10 +1049,16 @@ prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)" prop_checkCommarrays2 = verify checkCommarrays "a+=(1,2,3)" prop_checkCommarrays3 = verifyNot checkCommarrays "cow=(1 \"foo,bar\" 3)"+prop_checkCommarrays4 = verifyNot checkCommarrays "cow=('one,' 'two')" checkCommarrays _ (T_Array id l) =- if any ("," `isSuffixOf`) (concatMap deadSimple l) || (length $ filter (==',') (concat $ concatMap deadSimple l)) > 1- then warn id 2054 "Use spaces, not commas, to separate array elements."- else return ()+ when (any (isCommaSeparated . literal) l) $+ warn id 2054 "Use spaces, not commas, to separate array elements."+ where+ literal (T_NormalWord _ l) = concatMap literal l+ literal (T_Literal _ str) = str+ literal _ = "str"++ isCommaSeparated str = "," `isSuffixOf` str || (length $ filter (== ',') str) > 1 checkCommarrays _ _ = return () prop_checkOrNeq1 = verify checkOrNeq "if [[ $lol -ne cow || $lol -ne foo ]]; then echo foo; fi"@@ -1025,26 +1105,37 @@ f t = modify (Map.insert (getId t) t) -inUnquotableContext tree t =- case t of- TC_Noary _ DoubleBracket _ -> True- TC_Unary _ DoubleBracket _ _ -> True- TC_Binary _ DoubleBracket _ _ _ -> True- TA_Unary _ _ _ -> True- TA_Binary _ _ _ _ -> True- TA_Trinary _ _ _ _ -> True- TA_Expansion _ _ -> True- T_Assignment _ _ _ _ _ -> True- T_Redirecting _ _ _ ->- any (isCommand t) ["local", "declare", "typeset", "export"]- T_DoubleQuoted _ _ -> True- T_CaseExpression _ _ _ -> True- T_HereDoc _ _ _ _ _ -> True- T_ForIn _ _ _ _ -> True -- Pragmatically assume it's desirable here- x -> case Map.lookup (getId x) tree of- Nothing -> False- Just parent -> inUnquotableContext tree parent+-- Is this node self quoting?+isQuoteFree tree t =+ (isQuoteFreeElement t == Just True) ||+ (head $ (mapMaybe isQuoteFreeContext $ drop 1 $ getPath tree t) ++ [False])+ where+ -- Is this node self-quoting in itself?+ isQuoteFreeElement t =+ case t of+ T_Assignment {} -> return True+ _ -> Nothing + -- Are any subnodes inherently self-quoting?+ isQuoteFreeContext t =+ case t of+ TC_Noary _ DoubleBracket _ -> return True+ TC_Unary _ DoubleBracket _ _ -> return True+ TC_Binary _ DoubleBracket _ _ _ -> return True+ TA_Unary _ _ _ -> return True+ TA_Binary _ _ _ _ -> return True+ TA_Trinary _ _ _ _ -> return True+ TA_Expansion _ _ -> return True+ T_Assignment {} -> return True+ T_Redirecting _ _ _ -> return $+ any (isCommand t) ["local", "declare", "typeset", "export"]+ T_DoubleQuoted _ _ -> return True+ T_CaseExpression _ _ _ -> return True+ T_HereDoc _ _ _ _ _ -> return True+ T_DollarBraced {} -> return True+ T_ForIn _ _ _ _ -> return True -- Pragmatically assume it's desirable here+ _ -> Nothing+ isParamTo tree cmd t = go t where@@ -1082,14 +1173,16 @@ Nothing -> [] Just parent -> getPath tree parent +parents params t = getPath (parentMap params) t+ --- Command specific checks checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =- if t `isCommand` str then f rest else return ()+ if t `isCommand` str then f cmd rest else return () checkCommand _ _ _ = return () checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =- if t `isUnqualifiedCommand` str then f rest else return ()+ if t `isUnqualifiedCommand` str then f cmd rest else return () checkUnqualifiedCommand _ _ _ = return () getLiteralString = getLiteralStringExt (const Nothing)@@ -1137,7 +1230,7 @@ prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'" prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"-checkPrintfVar _ = checkUnqualifiedCommand "printf" f where+checkPrintfVar _ = checkUnqualifiedCommand "printf" (const f) where f (dashv:var:rest) | getLiteralString dashv == (Just "-v") = f rest f (format:params) = check format f _ = return ()@@ -1147,12 +1240,12 @@ else warn (getId format) 2059 $ "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"." -prop_checkUuoe1 = verify checkUuoe "echo $(date)"-prop_checkUuoe1a= verify checkUuoe "echo `date`"-prop_checkUuoe2 = verify checkUuoe "echo \"$(date)\""-prop_checkUuoe2a= verify checkUuoe "echo \"`date`\""-prop_checkUuoe3 = verifyNot checkUuoe "echo \"The time is $(date)\""-checkUuoe _ = checkUnqualifiedCommand "echo" f where+prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"+prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"+prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""+prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""+prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""+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@@ -1160,6 +1253,33 @@ f [T_NormalWord id [T_DoubleQuoted _ [(T_Backticked _ _)]]] = msg id f _ = return () +prop_checkUuoeVar1 = verify checkUuoeVar "for f in $(echo $tmp); do echo lol; done"+prop_checkUuoeVar2 = verify checkUuoeVar "date +`echo \"$format\"`"+prop_checkUuoeVar3 = verifyNot checkUuoeVar "foo \"$(echo -e '\r')\""+prop_checkUuoeVar4 = verifyNot checkUuoeVar "echo $tmp"+prop_checkUuoeVar5 = verify checkUuoeVar "foo \"$(echo \"$(date) value:\" $value)\""+prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\""+checkUuoeVar _ p =+ case p of+ T_Backticked id [cmd] -> check id cmd+ T_DollarExpansion id [cmd] -> check id cmd+ _ -> return ()+ where+ couldBeOptimized f = case f of+ T_Glob {} -> False+ T_Extglob {} -> False+ T_BraceExpansion {} -> False+ T_NormalWord _ l -> all couldBeOptimized l+ T_DoubleQuoted _ l -> all couldBeOptimized l+ _ -> True++ check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c+ check _ _ = return ()+ warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars ->+ unless ("-" `isPrefixOf` (concat $ concatMap deadSimple vars)) $+ when (all couldBeOptimized vars) $ style id 2116+ "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."+ prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"@@ -1173,7 +1293,7 @@ prop_checkTr9 = verifyNot checkTr "a-z n-za-m" prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr" prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"-checkTr _ = checkCommand "tr" (mapM_ f)+checkTr _ = checkCommand "tr" (const $ mapM_ f) where f w | isGlob w = do -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme? warn (getId w) 2060 $ "Quote parameters to tr to prevent glob expansion."@@ -1197,7 +1317,7 @@ prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php" prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)" prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"-checkFindNameGlob _ = checkCommand "find" f where+checkFindNameGlob _ = checkCommand "find" (const f) where acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ] acceptsGlob _ = False f [] = return ()@@ -1219,7 +1339,7 @@ prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg" prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file" -checkGrepRe _ = checkCommand "grep" f where+checkGrepRe _ = checkCommand "grep" (const f) where -- --regex=*(extglob) doesn't work. Fixme? skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s skippable _ = False@@ -1241,7 +1361,7 @@ prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT" prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT" prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"-checkTrapQuotes _ = checkCommand "trap" f where+checkTrapQuotes _ = checkCommand "trap" (const f) where f (x:_) = checkTrap x f _ = return () checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs@@ -1257,11 +1377,11 @@ prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10" prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo" checkTimeParameters _ = checkUnqualifiedCommand "time" f where- f (x:_) = let s = concat $ deadSimple x in+ f cmd (x:_) = let s = concat $ deadSimple x in if "-" `isPrefixOf` s && s /= "-p" then- info (getId x) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."+ info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one." else return ()- f _ = return ()+ f _ _ = return () prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1" prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"@@ -1414,7 +1534,7 @@ doLists _ = return () stripCleanup = reverse . dropWhile cleanup . reverse- cleanup (T_Pipeline _ [cmd]) =+ cleanup (T_Pipeline _ _ [cmd]) = isCommandMatch cmd (`elem` ["echo", "exit"]) cleanup _ = False @@ -1424,7 +1544,7 @@ doList (tail t) doList' _ = return () - commentIfExec (T_Pipeline id list) =+ commentIfExec (T_Pipeline id _ list) = mapM_ commentIfExec $ take 1 list commentIfExec (T_Redirecting _ _ f@( T_SimpleCommand id _ (cmd:arg:_))) =@@ -1456,7 +1576,7 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'" prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"-checkUnusedEchoEscapes _ = checkCommand "echo" f+checkUnusedEchoEscapes _ = checkCommand "echo" (const f) where isDashE = mkRegex "^-.*e" hasEscapes = mkRegex "\\\\[rnt]"@@ -1499,7 +1619,7 @@ prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\"" prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\"" prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""-checkSshCommandString _ = checkCommand "ssh" f+checkSshCommandString _ = checkCommand "ssh" (const f) where nonOptions args = filter (\x -> not $ "-" `isPrefixOf` (concat $ deadSimple x)) args@@ -1566,11 +1686,11 @@ parentPipeline = do parent <- Map.lookup (getId t) parents case parent of- T_Pipeline _ _ -> return parent+ T_Pipeline _ _ _ -> return parent _ -> Nothing causesSubshell = do- (T_Pipeline _ list) <- parentPipeline+ (T_Pipeline _ _ list) <- parentPipeline if length list <= 1 then return False else if lastCreatesSubshell@@ -1607,10 +1727,13 @@ T_SelectIn id str words _ -> [(t, t, str, DataFrom words)] _ -> [] --- Consider 'export' a reference, since it makes the var available+-- Consider 'export/declare -x' a reference, since it makes the var available getReferencedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) = case x of "export" -> concatMap getReference rest+ "declare" -> if "x" `elem` getFlags base+ then concatMap getReference rest+ else [] _ -> [] where getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]@@ -1661,11 +1784,19 @@ -- TODO: getBracedReference s = takeWhile (\x -> not $ x `elem` ":[#%/^,") $ dropWhile (`elem` "#!") s+getIndexReferences s = fromMaybe [] $ do+ (_, index, _, _) <- matchRegexAll re s+ return $ matchAll variableNameRegex index+ where+ re = mkRegex "\\[.*\\]" getReferencedVariables t = case t of- T_DollarBraced id l -> map (\x -> (t, t, x)) $ [getBracedReference $ bracedString l]- TA_Variable id str -> [(t, t, str)]+ T_DollarBraced id l -> let str = bracedString l in+ (t, t, getBracedReference str) :+ (map (\x -> (l, l, x)) $ getIndexReferences str)+ TA_Variable id str ->+ map (\x -> (t, t, x)) $ (getBracedReference str):(getIndexReferences str) T_Assignment id Append str _ _ -> [(t, t, str)] x -> getReferencedVariableCommand x @@ -1728,7 +1859,6 @@ doFlow _ = return [] ---- Check whether variables could have spaces/globs-prop_checkSpacefulness0 = verifyTree checkSpacefulness "for f in *.mp3; do echo $f; done" prop_checkSpacefulness1 = verifyTree checkSpacefulness "a='cow moo'; echo $a" prop_checkSpacefulness2 = verifyNotTree checkSpacefulness "a='cow moo'; [[ $a ]]" prop_checkSpacefulness3 = verifyNotTree checkSpacefulness "a='cow*.mp3'; echo \"$a\""@@ -1748,6 +1878,7 @@ prop_checkSpacefulnessH = verifyTree checkSpacefulness "echo foo=$1" prop_checkSpacefulnessI = verifyNotTree checkSpacefulness "$1 --flags" prop_checkSpacefulnessJ = verifyTree checkSpacefulness "echo $PWD"+prop_checkSpacefulnessK = verifyNotTree checkSpacefulness "n+='foo bar'" checkSpacefulness params t = doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)@@ -1766,7 +1897,7 @@ if spaced && (not $ "@" `isPrefixOf` name) -- There's another warning for this && (not $ isCounting token)- && (not $ inUnquotableContext parents token)+ && (not $ isQuoteFree parents token) && (not $ usedAsCommandName parents token) then return [(Note (getId token) InfoC 2086 warning)] else return []@@ -1823,7 +1954,7 @@ checkQuotesInLiterals params t = doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) where- getQuotes name = get >>= (return . Map.lookup name)+ getQuotes name = liftM (Map.lookup name) get setQuotes name ref = modify $ Map.insert name ref deleteQuotes = modify . Map.delete parents = parentMap params@@ -1855,7 +1986,7 @@ assignment <- getQuotes name if isJust assignment && not (isParamTo parents "eval" expr)- && not (inUnquotableContext parents expr)+ && not (isQuoteFree parents expr) then return [ Note (fromJust assignment)WarningC 2089 $ "Quotes/backslashes will be treated literally. Use an array.",@@ -1921,22 +2052,27 @@ prop_checkUnused10= verifyNotTree checkUnusedAssignments "read -p 'test: '" prop_checkUnused11= verifyNotTree checkUnusedAssignments "bar=5; export foo[$bar]=3" prop_checkUnused12= verifyNotTree checkUnusedAssignments "read foo; echo ${!foo}"+prop_checkUnused13= verifyNotTree checkUnusedAssignments "x=(1); (( x[0] ))"+prop_checkUnused14= verifyNotTree checkUnusedAssignments "x=(1); n=0; echo ${x[n]}"+prop_checkUnused15= verifyNotTree checkUnusedAssignments "x=(1); n=0; (( x[n] ))"+prop_checkUnused16= verifyNotTree checkUnusedAssignments "foo=5; declare -x foo" checkUnusedAssignments params t = snd $ runWriter (mapM_ checkAssignment flow) where flow = variableFlow params references = foldl (flip ($)) defaultMap (map insertRef flow) insertRef (Reference (base, token, name)) =- Map.insert name ()+ Map.insert (stripSuffix name) () insertRef _ = id checkAssignment (Assignment (_, token, name, _)) | isVariableName name = case Map.lookup name references of Just _ -> return ()- Nothing -> do+ Nothing -> info (getId token) 2034 $ name ++ " appears unused. Verify it or export it." checkAssignment _ = return () + stripSuffix str = takeWhile isVariableChar str defaultMap = Map.fromList $ zip internalVariables $ repeat () prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"@@ -1974,14 +2110,14 @@ where munchers = [ "ssh", "ffmpeg", "mplayer" ] - isStdinReadCommand (T_Pipeline _ [T_Redirecting id redirs cmd]) =+ isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) = let plaintext = deadSimple cmd in head (plaintext ++ [""]) == "read"- && (not $ "-u" `elem` plaintext)+ && ("-u" `notElem` plaintext) && all (not . stdinRedirect) redirs isStdinReadCommand _ = False - checkMuncher (T_Pipeline _ ((T_Redirecting _ redirs cmd):_)) = do+ checkMuncher (T_Pipeline _ _ ((T_Redirecting _ redirs cmd):_)) = do let name = fromMaybe "" $ getCommandBasename cmd when ((not . any stdinRedirect $ redirs) && (name `elem` munchers)) $ do info id 2095 $@@ -2011,7 +2147,7 @@ T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars otherwise -> check rest checkVar (T_Assignment aId mode aName Nothing value) |- aName == name && (not $ aId `elem` idPath) = do+ aName == name && (aId `notElem` idPath) = do warn aId 2097 "This assignment is only seen by the forked process." warn id 2098 "This expansion will not see the mentioned assignment." checkVar _ = return ()@@ -2028,7 +2164,7 @@ && contents /= ":" then warn id 2101 "Named class needs outer [], e.g. [[:digit:]]." else- if (not $ '[' `elem` contents) && hasDupes+ if ('[' `notElem` contents) && hasDupes then info id 2102 "Ranges can only match single chars (mentioned due to duplicates)." else return () where@@ -2060,7 +2196,7 @@ _ -> False getCmd (T_Annotation id _ x) = getCmd x- getCmd (T_Pipeline id [x]) = getCommandName x+ getCmd (T_Pipeline id _ [x]) = getCommandName x getCmd _ = Nothing doList list =@@ -2119,3 +2255,213 @@ when (hasKeyword && not hasParens) $ warn id 2113 "'function' keyword is non-standard. Use 'foo()' instead of 'function foo'." checkFunctionDeclarations _ _ = return ()+++-- This is a lot of code for little gain. Consider whether it's worth it.+prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"+prop_checkCatastrophicRm2 = verify checkCatastrophicRm "foo=$(echo bar); rm -r /home/$foo"+prop_checkCatastrophicRm3 = verify checkCatastrophicRm "foo=/home; user=$(whoami); rm -r \"$foo/$user\""+prop_checkCatastrophicRm4 = verifyNot checkCatastrophicRm "foo=/home; user=cow; rm -r \"$foo/$user\""+prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "user=$(whoami); rm -r /home/${user:?Nope}"+prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"+prop_checkCatastrophicRm7 = verifyNot checkCatastrophicRm "var=$(cmd); if [ -n \"$var\" ]; then rm -r /etc/$var/*; fi"+prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"+prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"+checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =+ when (any isRecursiveFlag $ simpleArgs) $+ mapM_ checkWord tokens+ where+ -- This ugly hack is based on the fact that ids generally increase+ relevantMap (Id n) = liftM snd . listToMaybe . dropWhile (\(Id x, _) -> x > n) $ flowMapR+ flowMapR = reverse $ (\x -> zip (scanl getScopeId (Id 0) x) (scanl addNulls defaultMap x)) $ variableFlow params+ simpleArgs = deadSimple t+ defaultMap = Map.fromList (map (\x -> (x, Nothing)) variablesWithoutSpaces)++ checkWord token =+ case getLiteralString token of+ Just str ->+ when (all (/= "--") simpleArgs && (fixPath str `elem` importantPaths)) $+ info (getId token) 2114 $ "Obligatory typo warning. Use 'rm --' to disable this message."+ Nothing ->+ checkWord' token++ checkWord' token = fromMaybe (return ()) $ do+ m <- relevantMap id+ filename <- combine m token+ let path = fixPath filename+ return . when (path `elem` importantPaths) $ do+ warn (getId token) 2115 $ "Make sure this never accidentally expands to '" ++ path ++ "'."++ fixPath filename =+ let normalized = skipRepeating '/' . skipRepeating '*' $ filename in+ if normalized == "/" then normalized else stripTrailing '/' $ normalized++ unnullable = all isVariableChar . concat . deadSimple+ isRecursiveFlag "--recursive" = True+ isRecursiveFlag ('-':'-':_) = False+ isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str+ isRecursiveFlag _ = False++ stripTrailing c = reverse . dropWhile (== c) . reverse+ skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)+ skipRepeating c (a:r) = a:(skipRepeating c r)+ skipRepeating _ [] = []++ addNulls map (Reference (_, token, name)) =+ if mightBeGuarded token+ then Map.insert name Nothing map+ else map+ addNulls map (Assignment (_, token, name, DataExternal)) =+ if mightBeGuarded token+ then Map.insert name Nothing map+ else Map.insert name (Just "") map+ addNulls m (Assignment (_, token, name, DataFrom [word])) =+ if mightBeGuarded token+ then Map.insert name Nothing m+ else+ if couldFail word+ then m+ else Map.insert name ((combine m) word) m+ addNulls m (Assignment (_, token, name, DataFrom _)) =+ Map.insert name Nothing m+ addNulls map _ = map++ getScopeId n (Reference (_, token, _)) = getId token+ getScopeId n (Assignment (_, token, _, _)) = getId token+ getScopeId n _ = n++ joinMaybes :: [Maybe String] -> Maybe String+ joinMaybes = foldl (liftM2 (++)) (Just "")+ combine m token = c token+ where+ c (T_DollarBraced _ t) | unnullable t =+ Map.findWithDefault (Just "") (concat $ deadSimple t) m+ c (T_DoubleQuoted _ tokens) = joinMaybes $ map (combine m) tokens+ c (T_NormalWord _ tokens) = joinMaybes $ map (combine m) tokens+ c (T_Glob _ "*") = Just "*"+ c t = getLiteralString t++ couldFail (T_Backticked _ _) = True+ couldFail (T_DollarExpansion _ _) = True+ couldFail (T_DoubleQuoted _ foo) = any couldFail foo+ couldFail (T_NormalWord _ foo) = any couldFail foo+ couldFail _ = False++ mightBeGuarded token = any t (getPath (parentMap params) token)+ where+ t (T_Condition _ _ _) = True+ t (T_OrIf _ _ _) = True+ t (T_AndIf _ _ _) = True+ t _ = False++ paths = [+ "/", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",+ "/var"+ ]+ importantPaths = ["", "/*", "/*/*"] >>= (\x -> map (++x) paths)+checkCatastrophicRm _ _ = return ()+++prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"+prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"+prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"+prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"+checkInteractiveSu params = checkCommand "su" f+ where+ f cmd l = when (length l <= 1) $+ when (all undirected $ getPath (parentMap params) cmd) $+ info (getId cmd) 2117+ "To run commands as another user, use su -c or sudo."++ undirected (T_Pipeline _ _ l) = length l <= 1+ -- This should really just be modifications to stdin, but meh+ undirected (T_Redirecting _ list _) = null list+ undirected _ = True+++prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar"+prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/zsh\nfoo |& bar"+checkStderrPipe params =+ case shellType params of+ Ksh -> match+ _ -> const $ return ()+ where+ match (T_Pipe id "|&") =+ err id 2118 "Ksh does not support |&. Use 2>&1 |."+ match _ = return ()++prop_checkUnpassedInFunctions1 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo"+prop_checkUnpassedInFunctions2 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; };"+prop_checkUnpassedInFunctions3 = verifyNotTree checkUnpassedInFunctions "foo() { echo $lol; }; foo"+prop_checkUnpassedInFunctions4 = verifyNotTree checkUnpassedInFunctions "foo() { echo $0; }; foo"+prop_checkUnpassedInFunctions5 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; }; foo 'lol'; foo"+checkUnpassedInFunctions params root =+ execWriter $ mapM_ warnForGroup referenceGroups+ where+ functionMap :: Map.Map String Token+ functionMap = Map.fromList $+ map (\t@(T_Function _ _ _ name _) -> (name,t)) functions+ functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root+ findFunction t@(T_DollarBraced id token) = do+ str <- getLiteralString token+ unless (isPositional str) $ fail "Not positional"+ let path = getPath (parentMap params) t+ find isFunction path+ findFunction _ = Nothing++ isFunction (T_Function {}) = True+ isFunction _ = False++ referenceList :: [(String, Bool, Token)]+ referenceList = execWriter $+ doAnalysis (fromMaybe (return ()) . checkCommand) root+ checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())+ checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do+ str <- getLiteralString cmd+ unless (Map.member str functionMap) $ fail "irrelevant"+ return $ tell [(str, null args, t)]+ checkCommand _ = Nothing++ isPositional str = str == "*" || str == "@"+ || (all isDigit str && str /= "0")++ isArgumentless (_, b, _) = b+ referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList+ updateWith x@(name, _, _) = Map.insertWith (++) name [x]++ warnForGroup group =+ when (all isArgumentless group) $ do+ mapM_ suggestParams group+ warnForDeclaration group++ suggestParams (name, _, thing) =+ info (getId thing) 2119 $+ "Use " ++ name ++ " \"$@\" if function's $1 should mean script's $1."+ warnForDeclaration ((name, _, _):_) =+ warn (getId . fromJust $ Map.lookup name functionMap) 2120 $+ name ++ " references arguments, but none are ever passed."+++prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"+prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"+prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"+prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"+prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"+prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"+checkSetAssignment params = checkUnqualifiedCommand "set" f+ where+ f cmd (var:value:rest) =+ let str = literal var in+ when (isVariableName str || isAssignment str) $+ msg (getId var)+ f cmd (var:_) =+ when (isAssignment $ literal var) $+ msg (getId var)+ f _ _ = return ()++ msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."++ isAssignment str = '=' `elem` str+ literal (T_NormalWord _ l) = concatMap literal l+ literal (T_Literal _ str) = str+ literal _ = "*"
ShellCheck/Data.hs view
@@ -1,6 +1,6 @@ module ShellCheck.Data where -shellcheckVersion = "0.3.1" -- Must also be updated in ShellCheck.cabal+shellcheckVersion = "0.3.2" -- Must also be updated in ShellCheck.cabal internalVariables = [ -- Generic
ShellCheck/Parser.hs view
@@ -46,14 +46,18 @@ quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars quotable = nbsp <|> unicodeDoubleQuote <|> oneOf quotableChars bracedQuotable = oneOf "}\"$`'"-doubleQuotableChars = "\"$`\x201C\x201D"+doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars whitespace = oneOf " \t\n" <|> carriageReturn <|> nbsp linewhitespace = oneOf " \t" <|> nbsp +suspectCharAfterQuotes = variableChars <|> char '%'+ extglobStartChars = "?*@!+" extglobStart = oneOf extglobStartChars +unicodeDoubleQuoteChars = "\x201C\x201D\x2033\x2036"+ prop_spacing = isOk spacing " \\\n # Comment" spacing = do x <- many (many1 linewhitespace <|> (try $ string "\\\n"))@@ -78,7 +82,7 @@ unicodeDoubleQuote = do pos <- getPosition- char '\x201C' <|> char '\x201D'+ oneOf unicodeDoubleQuoteChars parseProblemAt pos WarningC 1015 "This is a unicode double quote. Delete and retype it." return '"' @@ -313,9 +317,11 @@ when (endedWith "]" x) $ do parseProblemAt pos ErrorC 1020 $ "You need a space before the " ++ (if single then "]" else "]]") ++ "."+ fail "Missing space before ]" when (single && endedWith ")" x) $ do parseProblemAt pos ErrorC 1021 $ "You need a space before the \\)"+ fail "Missing space before )" disregard spacing return x where endedWith str (T_NormalWord id s@(_:_)) =@@ -327,6 +333,7 @@ id <- getNextId x <- try (string "&&" <|> string "-a") softCondSpacing+ skipLineFeeds return $ TC_And id typ x readCondOrOp = do@@ -334,6 +341,7 @@ id <- getNextId x <- try (string "||" <|> string "-o") softCondSpacing+ skipLineFeeds return $ TC_Or id typ x readCondNoaryOrBinary = do@@ -412,7 +420,17 @@ str <- string "|" return $ T_Literal id str - readCondTerm = readCondNot <|> readCondExpr+ 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+ return term+ readCondNot = do id <- getNextId char '!'@@ -611,6 +629,9 @@ 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_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]" readCondition = called "test expression" $ do opos <- getPosition id <- getNextId@@ -739,18 +760,30 @@ prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'" prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'" prop_readsingleQuoted3 = isWarning readSingleQuoted "\x2018hello\x2019"+prop_readSingleQuoted4 = isWarning readNormalWord "'it's"+prop_readSingleQuoted5 = isWarning readSimpleCommand "foo='bar\ncow 'arg"+prop_readSingleQuoted6 = isOk readSimpleCommand "foo='bar cow 'arg" readSingleQuoted = called "single quoted string" $ do id <- getNextId+ startPos <- getPosition singleQuote s <- readSingleQuotedPart `reluctantlyTill` singleQuote- pos <- getPosition+ let string = concat s+ endPos <- getPosition singleQuote <?> "end of single quoted string" - let string = concat s- return (T_SingleQuoted id string) `attempting` do- x <- lookAhead anyChar- when (isAlpha x && not (null string) && isAlpha (last string)) $ parseProblemAt pos WarningC 1011 "This apostrophe terminated the single quoted string!"+ optional $ do+ c <- try . lookAhead $ suspectCharAfterQuotes <|> oneOf "'"+ if (not (null string) && isAlpha c && isAlpha (last string))+ then+ parseProblemAt endPos WarningC 1011 $+ "This apostrophe terminated the single quoted string!"+ else+ when ('\n' `elem` string && not ("\n" `isPrefixOf` string)) $+ suggestForgotClosingQuote startPos endPos "single quoted string" + return (T_SingleQuoted id string)+ readSingleQuotedLiteral = do singleQuote strs <- many1 readSingleQuotedPart@@ -763,13 +796,24 @@ prop_readBackTicked = isOk readBackTicked "`ls *.mp3`" prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"+prop_readBackTicked3 = isWarning readBackTicked "´grep \"\\\"\"´"+prop_readBackTicked4 = isOk readBackTicked "`echo foo\necho bar`"+prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar"+prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar" readBackTicked = called "backtick expansion" $ do id <- getNextId- pos <- getPosition- char '`'+ startPos <- getPosition+ backtick subStart <- getPosition- subString <- readGenericLiteral "`"- char '`'+ subString <- readGenericLiteral "`´"+ endPos <- getPosition+ backtick++ optional $ do+ c <- try . lookAhead $ suspectCharAfterQuotes+ when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $ do+ suggestForgotClosingQuote startPos endPos "backtick expansion"+ -- Result positions may be off due to escapes result <- subParse subStart readCompoundList (unEscape subString) return $ T_Backticked id result@@ -778,6 +822,12 @@ unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest unEscape ('\\':'\n':rest) = unEscape rest unEscape (c:rest) = c : unEscape rest+ backtick =+ disregard (char '`') <|> do+ pos <- getPosition+ char '´'+ parseProblemAt pos ErrorC 1077 $+ "For command expansion, the tick should slant left (` vs ´)." subParse pos parser input = do lastPosition <- getPosition@@ -792,13 +842,32 @@ prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\"" prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\"" prop_readDoubleQuoted3 = isWarning readDoubleQuoted "\x201Chello\x201D"+prop_readDoubleQuoted4 = isWarning readSimpleCommand "\"foo\nbar\"foo"+prop_readDoubleQuoted5 = isOk readSimpleCommand "lol \"foo\nbar\" etc" readDoubleQuoted = called "double quoted string" $ do id <- getNextId+ startPos <- getPosition doubleQuote x <- many doubleQuotedPart+ endPos <- getPosition doubleQuote <?> "end of double quoted string"+ optional $ do+ try . lookAhead $ suspectCharAfterQuotes <|> oneOf "$\""+ when (any hasLineFeed x && not (startsWithLineFeed x)) $+ suggestForgotClosingQuote startPos endPos "double quoted string" return $ T_DoubleQuoted id x+ where+ startsWithLineFeed ((T_Literal _ ('\n':_)):_) = True+ startsWithLineFeed _ = False+ hasLineFeed (T_Literal _ str) | '\n' `elem` str = True+ hasLineFeed _ = False +suggestForgotClosingQuote startPos endPos name = do+ parseProblemAt startPos WarningC 1078 $+ "Did you forget to close this " ++ name ++ "?"+ parseProblemAt endPos InfoC 1079 $+ "This is actually an end quote, but due to next char it looks suspect."+ doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked readDoubleQuotedLiteral = do@@ -1325,13 +1394,25 @@ readPipeSequence = do id <- getNextId- list <- readCommand `sepBy1` (readPipe `thenSkip` (spacing >> readLineBreak))+ (cmds, pipes) <- sepBy1WithSeparators readCommand+ (readPipe `thenSkip` (spacing >> readLineBreak)) spacing- return $ T_Pipeline id list+ return $ T_Pipeline id pipes cmds+ where+ sepBy1WithSeparators p s = do+ let elems = p >>= \x -> return ([x], [])+ let seps = do+ separator <- s+ return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])+ elems `chainl1` seps readPipe = do notFollowedBy2 g_OR_IF- char '|' `thenSkip` spacing+ id <- getNextId+ char '|'+ qualifier <- string "&" <|> return ""+ spacing+ return $ T_Pipe id ('|':qualifier) readCommand = (readCompoundCommand <|> readSimpleCommand) @@ -1682,7 +1763,8 @@ if space == "" && space2 /= "" then do when (variable /= "IFS") $- parseNoteAt pos InfoC 1007 $ "Note that 'var= value' (with space after equals sign) is similar to 'var=\"\"; value'."+ parseNoteAt pos WarningC 1007+ "Remove space after = if trying to assign a value (for empty string, use var='' ... )." value <- readEmptyLiteral return $ T_Assignment id op variable index value else do@@ -1729,15 +1811,23 @@ notFollowedBy2 $ char '(' return $ t id -tryWordToken s t = tryParseWordToken (string s) t `thenSkip` spacing-tryParseWordToken parser t = try $ do+tryWordToken s t = tryParseWordToken s t `thenSkip` spacing+tryParseWordToken keyword t = try $ do id <- getNextId- parser+ str <- anycaseString keyword optional (do try . lookAhead $ char '[' parseProblem ErrorC 1069 "You need a space before the [.") try $ lookAhead (keywordSeparator)+ when (str /= keyword) $+ parseProblem ErrorC 1081 $+ "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'." return $ t id++anycaseString str =+ mapM anycaseChar str+ where+ anycaseChar c = char (toLower c) <|> char (toUpper c) g_AND_IF = tryToken "&&" T_AND_IF g_OR_IF = tryToken "||" T_OR_IF
shellcheck.hs view
@@ -69,7 +69,7 @@ return $ Just (opts, files) (_, _, errors) -> do- printErr $ (concat errors) ++ "\n" ++ usageInfo header options+ printErr $ concat errors ++ "\n" ++ usageInfo header options exitWith syntaxFailure formats = Map.fromList [@@ -84,7 +84,7 @@ return $ and output where clear = ansi 0- ansi n = "\x1B[" ++ (show n) ++ "m"+ ansi n = "\x1B[" ++ show n ++ "m" colorForLevel "error" = 31 -- red colorForLevel "warning" = 33 -- yellow@@ -94,7 +94,8 @@ colorForLevel "source" = 0 -- none colorForLevel _ = 0 -- none - colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear+ colorComment level comment =+ ansi (colorForLevel level) ++ comment ++ clear doFile path = do contents <- readContents path@@ -112,15 +113,17 @@ then "" else fileLines !! (lineNum - 1) putStrLn ""- putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":")+ putStrLn $ colorFunc "message"+ ("In " ++ filename ++" line " ++ show lineNum ++ ":") putStrLn (colorFunc "source" line)- mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x+ mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x putStrLn "" ) groups return $ null comments cuteIndent comment =- (replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (code $ scCode comment) ++ ": " ++ (scMessage comment)+ replicate (scColumn comment - 1) ' ' +++ "^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment code code = "SC" ++ (show code) @@ -131,7 +134,7 @@ -- This totally ignores the filenames. Fixme? forJson options files = do comments <- liftM concat $ mapM (commentsFor options) files- putStrLn $ encodeStrict $ comments+ putStrLn $ encodeStrict comments return . null $ comments -- Mimic GCC "file:line:col: (error|warning|note): message" format@@ -178,8 +181,8 @@ severity "warning" = "warning" severity _ = "info" attr s v = concat [ s, "='", escape v, "' " ]- escape msg = concatMap escape' msg- escape' c = if isOk c then [c] else "&#" ++ (show $ ord c) ++ ";"+ escape = concatMap escape'+ escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";" isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")] formatFile name comments = concat [@@ -226,7 +229,7 @@ real (_:rest) r v target = real rest (r+1) (v+1) target getOption [] _ = Nothing-getOption ((Flag var val):_) name | name == var = return val+getOption (Flag var val:_) name | name == var = return val getOption (_:rest) flag = getOption rest flag getOptions options name =@@ -247,8 +250,8 @@ in map (Prelude.read . clean) elements :: [Int] -excludeCodes codes comments =- filter (not . hasCode) comments+excludeCodes codes =+ filter (not . hasCode) where hasCode c = scCode c `elem` codes @@ -265,7 +268,7 @@ exitWith code process Nothing = return False-process (Just (options, files)) = do+process (Just (options, files)) = let format = fromMaybe "tty" $ getOption options "format" in case Map.lookup format formats of Nothing -> do@@ -281,11 +284,9 @@ when (isJust $ getOption opts "version") printVersionAndExit let shell = getOption opts "shell" in- if isNothing shell- then return ()- else when (isNothing $ shell >>= shellForExecutable) $ do- printErr $ "Unknown shell: " ++ (fromJust shell)- exitWith supportFailure+ when (isJust shell && isNothing (shell >>= shellForExecutable)) $ do+ printErr $ "Unknown shell: " ++ (fromJust shell)+ exitWith supportFailure when (null files) $ do printErr "No files specified.\n"