ShellCheck 0.3.3 → 0.3.4
raw patch · 9 files changed
+850/−501 lines, 9 filesdep +QuickCheckdep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: QuickCheck
Dependency ranges changed: base
API changes (from Hackage documentation)
- ShellCheck.AST: TA_Base :: Id -> String -> Token -> Token
- ShellCheck.AST: TA_Literal :: Id -> String -> Token
- ShellCheck.AST: TA_Variable :: Id -> String -> Token
+ ShellCheck.AST: CaseBreak :: CaseType
+ ShellCheck.AST: CaseContinue :: CaseType
+ ShellCheck.AST: CaseFallThrough :: CaseType
+ ShellCheck.AST: T_ :: Id -> [Token] -> Token
+ ShellCheck.AST: T_IndexedElement :: Id -> Token -> Token -> Token
+ ShellCheck.AST: data CaseType
+ ShellCheck.AST: instance Eq CaseType
+ ShellCheck.AST: instance Show CaseType
+ ShellCheck.Analytics: runTests :: IO Bool
+ ShellCheck.Parser: instance Show Context
+ ShellCheck.Parser: runTests :: IO Bool
+ ShellCheck.Simple: runTests :: IO Bool
- ShellCheck.AST: TA_Expansion :: Id -> Token -> Token
+ ShellCheck.AST: TA_Expansion :: Id -> [Token] -> Token
- ShellCheck.AST: T_CaseExpression :: Id -> Token -> [([Token], [Token])] -> Token
+ ShellCheck.AST: T_CaseExpression :: Id -> Token -> [(CaseType, [Token], [Token])] -> Token
- ShellCheck.Data: shellcheckVersion :: [Char]
+ ShellCheck.Data: shellcheckVersion :: String
Files
- README.md +88/−0
- ShellCheck.cabal +29/−4
- ShellCheck/AST.hs +11/−13
- ShellCheck/Analytics.hs +346/−278
- ShellCheck/Data.hs +4/−1
- ShellCheck/Parser.hs +209/−184
- ShellCheck/Simple.hs +26/−21
- shellcheck.1.md +121/−0
- test/shellcheck.hs +16/−0
+ README.md view
@@ -0,0 +1,88 @@+# ShellCheck - A shell script static analysis tool++http://www.shellcheck.net++Copyright 2012-2014, Vidar 'koala_man' Holen+Licensed under the GNU Affero General Public License, v3++The goals of ShellCheck are:++ - To point out and clarify typical beginner's syntax issues,+ that causes a shell to give cryptic error messages.++ - To point out and clarify typical intermediate level semantic problems,+ that causes a shell to behave strangely and counter-intuitively.++ - 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.++## Installing++On systems with Cabal:++ cabal update+ cabal install shellcheck++On Arch Linux with community packages enabled:++ pacman -S shellcheck++On OS X with homebrew:++ brew install shellcheck++ShellCheck is also available as an online service:++ http://www.shellcheck.net++## Building with Cabal++This sections describes how to build ShellCheck from a source directory.++First, make sure cabal is installed. On Debian based distros:++ apt-get install cabal-install++On Fedora:++ yum install cabal-install++On Mac OS X with homebrew (http://brew.sh/):++ brew install cabal-install++On Mac OS X with MacPorts (http://www.macports.org/):++ port install hs-cabal-install++Let cabal update itself, in case your distro version is outdated:++ $ cabal update+ $ cabal install cabal-install++With cabal installed, cd to the ShellCheck source directory and:++ $ cabal install++This will install ShellCheck to your ~/.cabal/bin directory.++Add the directory to your PATH (for bash, add this to your ~/.bashrc file):++ export PATH=$HOME/.cabal/bin:$PATH++Verify that your PATH is set up correctly:++ $ which shellcheck+ ~/.cabal/bin/shellcheck++## Running tests++To run the unit test suite:++ cabal configure --enable-tests+ cabal build+ cabal test++Happy ShellChecking!
ShellCheck.cabal view
@@ -1,6 +1,5 @@ Name: ShellCheck--- Must also be updated in ShellCheck/Data.hs :-Version: 0.3.3+Version: 0.3.4 Synopsis: Shell script analysis tool License: OtherLicense License-file: LICENSE@@ -23,6 +22,13 @@ * To point out subtle caveats, corner cases and pitfalls, that may cause an advanced user's otherwise working script to fail under future circumstances. +Extra-Source-Files:+ -- documentation+ README.md+ shellcheck.1.md+ -- tests+ test/shellcheck.hs+ source-repository head type: git location: git://github.com/koalaman/shellcheck.git@@ -35,13 +41,16 @@ json, mtl, parsec,- regex-compat+ regex-compat,+ QuickCheck >= 2.2 exposed-modules: ShellCheck.Analytics ShellCheck.AST ShellCheck.Data ShellCheck.Parser ShellCheck.Simple+ other-modules:+ Paths_ShellCheck executable shellcheck build-depends:@@ -52,5 +61,21 @@ json, mtl, parsec,- regex-compat+ regex-compat,+ QuickCheck >= 2.2 main-is: shellcheck.hs++test-suite test-shellcheck+ type: exitcode-stdio-1.0+ build-depends:+ ShellCheck,+ base >= 4 && < 5,+ containers,+ directory,+ json,+ mtl,+ parsec,+ regex-compat,+ QuickCheck >= 2.2+ main-is: test/shellcheck.hs+
ShellCheck/AST.hs view
@@ -29,16 +29,14 @@ data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq) data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq) data ForInType = NormalForIn | ShortForIn deriving (Show, Eq)+data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq) data Token =- TA_Base Id String Token- | TA_Binary Id String Token Token- | TA_Expansion Id Token- | TA_Literal Id String+ TA_Binary Id String Token Token+ | TA_Expansion Id [Token] | TA_Sequence Id [Token] | TA_Trinary Id Token Token Token | TA_Unary Id String Token- | TA_Variable Id String | TC_And Id ConditionType String Token Token | TC_Binary Id ConditionType String Token Token | TC_Group Id ConditionType Token@@ -49,6 +47,8 @@ | T_AndIf Id (Token) (Token) | T_Arithmetic Id Token | T_Array Id [Token]+ | T_IndexedElement Id Token Token+ | T_ Id [Token] | T_Assignment Id AssignmentMode String (Maybe Token) Token | T_Backgrounded Id Token | T_Backticked Id [Token]@@ -58,7 +58,7 @@ | T_BraceGroup Id [Token] | T_CLOBBER Id | T_Case Id- | T_CaseExpression Id Token [([Token],[Token])]+ | T_CaseExpression Id Token [(CaseType, [Token], [Token])] | T_Condition Id ConditionType Token | T_DGREAT Id | T_DLESS Id@@ -179,6 +179,7 @@ b <- round value return $ T_Assignment id mode var a b delve (T_Array id t) = dl t $ T_Array id+ delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id delve (T_Redirecting id redirs cmd) = do a <- roundAll redirs b <- round cmd@@ -207,10 +208,10 @@ delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v delve (T_CaseExpression id word cases) = do newWord <- round word- newCases <- mapM (\(c, t) -> do+ newCases <- mapM (\(o, c, t) -> do x <- mapM round c y <- mapM round t- return (x,y)+ return (o, x,y) ) cases return $ T_CaseExpression id newWord newCases @@ -243,8 +244,7 @@ b <- round t2 c <- round t3 return $ TA_Trinary id a b c- delve (TA_Expansion id t) = d1 t $ TA_Expansion id- delve (TA_Base id b t) = d1 t $ TA_Base id b+ delve (TA_Expansion id t) = dl t $ TA_Expansion id delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns delve t = return t @@ -297,6 +297,7 @@ T_FdRedirect id _ _ -> id T_Assignment id _ _ _ _ -> id T_Array id _ -> id+ T_IndexedElement id _ _ -> id T_Redirecting id _ _ -> id T_SimpleCommand id _ _ -> id T_Pipeline id _ _ -> id@@ -327,11 +328,8 @@ TA_Binary id _ _ _ -> id TA_Unary id _ _ -> id TA_Sequence id _ -> id- TA_Variable id _ -> id TA_Trinary id _ _ _ -> id TA_Expansion id _ -> id- TA_Literal id _ -> id- TA_Base id _ _ -> id T_ProcSub id _ _ -> id T_Glob id _ -> id T_ForArithmetic id _ _ _ _ -> id
ShellCheck/Analytics.hs view
@@ -15,21 +15,25 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. -}-module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics, shellForExecutable) where+{-# LANGUAGE TemplateHaskell #-}+module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics, shellForExecutable, runTests) where +import Control.Arrow (first) import Control.Monad import Control.Monad.State import Control.Monad.Writer import Data.Char import Data.Functor+import Data.Function (on) import Data.List import Data.Maybe import Debug.Trace import ShellCheck.AST import ShellCheck.Data-import ShellCheck.Parser+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)@@ -46,8 +50,8 @@ treeChecks :: [Parameters -> Token -> [Note]] treeChecks = [ runNodeAnalysis- (\p t -> mapM_ (\f -> f t) $- map (\f -> f p) (nodeChecks ++ checksFor (shellType p)))+ (\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p))+ (nodeChecks ++ checksFor (shellType p)))) ,subshellAssignmentCheck ,checkSpacefulness ,checkQuotesInLiterals@@ -201,6 +205,8 @@ ,checkAliasesUsesArgs ,checkShouldUseGrepQ ,checkTestGlobs+ ,checkConcatenatedDollarAt+ ,checkFindActionPrecedence ] @@ -242,7 +248,7 @@ where f str = do (_, match, rest, _) <- matchRegexAll re str- return $ (match, rest)+ return (match, rest) willSplit x = case x of@@ -267,7 +273,7 @@ isConfusedGlobRegex _ = False getSuspiciousRegexWildcard str =- if (not $ str `matches` contra)+ if not $ str `matches` contra then do match <- matchRegex suspicious str str <- match !!! 0@@ -306,7 +312,7 @@ simplify = doTransform makeSimple deadSimple (T_NormalWord _ l) = [concat (concatMap deadSimple l)]-deadSimple (T_DoubleQuoted _ 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}"]@@ -423,7 +429,7 @@ prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1" prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2"-checkWrongArithmeticAssignment params (T_SimpleCommand id ((T_Assignment _ _ _ _ val):[]) []) =+checkWrongArithmeticAssignment params (T_SimpleCommand id (T_Assignment _ _ _ _ val:[]) []) = fromMaybe (return ()) $ do str <- getNormalString val match <- matchRegex regex str@@ -454,7 +460,7 @@ 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):_:_)) =+checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) = checkCommand "cat" (const f) cmd where f [word] = when (isSimple word) $@@ -470,7 +476,7 @@ prop_checkNeedlessCommands3 = verifyNot checkNeedlessCommands "foo=$(expr foo : regex)" prop_checkNeedlessCommands4 = verifyNot checkNeedlessCommands "foo=$(expr foo \\< regex)" checkNeedlessCommands _ cmd@(T_SimpleCommand id _ args) |- cmd `isCommand` "expr" && (not $ any (`elem` words) exceptions) =+ cmd `isCommand` "expr" && not (any (`elem` words) exceptions) = style id 2003 "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]." where -- These operators are hard to replicate in POSIX@@ -482,11 +488,19 @@ prop_checkPipePitfalls4 = verifyNot checkPipePitfalls "find . -print0 | xargs -0 foo" prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo" prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo"+prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo" checkPipePitfalls _ (T_Pipeline id _ commands) = do for ["find", "xargs"] $- \(find:xargs:_) -> let args = deadSimple xargs in- unless (hasShortParameter args '0') $- warn (getId find) 2038 "Use either 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow for non-alphanumeric filenames."+ \(find:xargs:_) ->+ let args = deadSimple xargs ++ deadSimple find+ in+ unless (or $ map ($ args) [+ hasShortParameter '0',+ hasParameter "null",+ hasParameter "print0",+ hasParameter "printf"+ ]) $ warn (getId find) 2038+ "Use -print0/-0 or -exec + to allow for non-alphanumeric filenames." for ["?", "echo"] $ \(_:echo:_) -> info (getId echo) 2008 "echo doesn't read from stdin, are you sure you should be piping to it?"@@ -505,22 +519,24 @@ ] unless didLs $ do for ["ls", "?"] $- \(ls:_) -> unless (hasShortParameter (deadSimple ls) 'N') $+ \(ls:_) -> unless (hasShortParameter 'N' (deadSimple ls)) $ info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames." return () where for l f = let indices = indexOfSublists l (map (headOrDefault "" . deadSimple) commands) in do- mapM_ f (map (\n -> take (length l) $ drop n $ commands) indices)+ mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices return . not . null $ indices for' l f = for l (first f) first func (x:_) = func (getId x) first _ _ = return ()- hasShortParameter list char = any (\x -> "-" `isPrefixOf` x && char `elem` x) list+ hasShortParameter char list = any (\x -> "-" `isPrefixOf` x && char `elem` x) list+ hasParameter string list =+ any (isPrefixOf string . dropWhile (== '-')) list checkPipePitfalls _ _ = return () -indexOfSublists sub all = f 0 all+indexOfSublists sub = f 0 where f _ [] = [] f n a@(r:rest) =@@ -570,9 +586,7 @@ prop_checkShebang1 = verifyTree checkShebang "#!/usr/bin/env bash -x\necho cow" prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l " checkShebang _ (T_Script id sb _) =- if (length $ words sb) > 2 then- [Note id ErrorC 2096 $ "On most OS, shebangs can only specify a single parameter."]- else []+ [Note id ErrorC 2096 "On most OS, shebangs can only specify a single parameter." | length (words sb) > 2] prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"@@ -612,11 +626,15 @@ warnMsg id $ op ++ " is" bashism (TA_Unary id op _) | op `elem` [ "|++", "|--", "++|", "--|"] =- warnMsg id $ (filter (/= '|') op) ++ " is"+ warnMsg id $ filter (/= '|') op ++ " is" bashism t@(T_SimpleCommand id _ _) | t `isCommand` "source" = warnMsg id "'source' in place of '.' is" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"+ bashism t@(TA_Expansion id _) | getLiteralString t == Just "RANDOM" =+ warnMsg id "RANDOM is"+ bashism t@(T_DollarBraced id _) | getBracedReference (bracedString t) == "RANDOM" =+ warnMsg id "$RANDOM is" bashism (T_DollarBraced id token) = mapM_ check expansion where@@ -628,20 +646,18 @@ | t `isCommand` "echo" && "-" `isPrefixOf` argString = unless ("--" `isPrefixOf` argString) $ -- echo "-------" warnMsg (getId arg) "echo flags are"- where argString = (concat $ deadSimple arg)+ where argString = concat $ deadSimple arg bashism t@(T_SimpleCommand _ _ (cmd:arg:_))- | t `isCommand` "exec" && "-" `isPrefixOf` (concat $ deadSimple arg) =+ | t `isCommand` "exec" && "-" `isPrefixOf` concat (deadSimple arg) = warnMsg (getId arg) "exec flags are" bashism t@(T_SimpleCommand id _ _) | t `isCommand` "let" = warnMsg id "'let' is"- bashism t@(TA_Variable 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()+ bashism _ = return () varChars="_0-9a-zA-Z" expansion = let re = mkRegex in [@@ -650,7 +666,7 @@ (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"), (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"), (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is"),- (re $ "^RANDOM$", "$RANDOM is")+ (re "^RANDOM$", "$RANDOM is") ] prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"@@ -665,14 +681,14 @@ checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) = when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list || (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $- err id 2066 $ "Since you double quoted this, it will not word split, and the loop will only run once."+ err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once." checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [T_SingleQuoted id s]] _) = warn id 2041 $ "This is a literal string. To run as a command, use $(" ++ s ++ ")." checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [T_Literal id s]] _) = if ',' `elem` s then unless ('{' `elem` s) $- warn id 2042 $ "Use spaces, not commas, to separate loop elements."- else warn id 2043 $ "This loop will only run once, with " ++ (head f) ++ "='" ++ s ++ "'."+ warn id 2042 "Use spaces, not commas, to separate loop elements."+ else warn id 2043 $ "This loop will only run once, with " ++ head f ++ "='" ++ s ++ "'." checkForInQuoted _ _ = return () prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"@@ -694,7 +710,7 @@ prop_checkForInLs = verify checkForInLs "for f in $(ls *.mp3); do mplayer \"$f\"; done" prop_checkForInLs2 = verify checkForInLs "for f in `ls *.mp3`; do mplayer \"$f\"; done" prop_checkForInLs3 = verify checkForInLs "for f in `find / -name '*.mp3'`; do mplayer \"$f\"; done"-checkForInLs _ t = try t+checkForInLs _ = try where try (T_ForIn _ _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) = check id f x@@ -718,14 +734,14 @@ prop_checkFindExec6 = verify checkFindExec "find / -type d -execdir rm *.jpg \\;" checkFindExec _ cmd@(T_SimpleCommand _ _ t@(h:r)) | cmd `isCommand` "find" = do c <- broken r False- when c $ do+ when c $ let wordId = getId $ last t in err wordId 2067 "Missing ';' or + terminating -exec. You can't use |/||/&&, and ';' has to be a separate, quoted argument." where broken [] v = return v broken (w:r) v = do- when v $ (mapM_ warnFor $ fromWord w)+ when v (mapM_ warnFor $ fromWord w) case getLiteralString w of Just "-exec" -> broken r True Just "-execdir" -> broken r True@@ -738,7 +754,7 @@ T_DollarExpansion _ _ -> True T_Backticked _ _ -> True T_Glob _ _ -> True- T_Extglob _ _ _ -> True+ T_Extglob {} -> True _ -> False warnFor x =@@ -759,8 +775,8 @@ prop_checkUnquotedExpansions5 = verifyNot checkUnquotedExpansions "for f in $(cmd); do echo $f; done" prop_checkUnquotedExpansions6 = verifyNot checkUnquotedExpansions "$(cmd)" prop_checkUnquotedExpansions7 = verifyNot checkUnquotedExpansions "cat << foo\n$(ls)\nfoo"-checkUnquotedExpansions params t =- check t+checkUnquotedExpansions params =+ check where check t@(T_DollarExpansion _ _) = examine t check t@(T_Backticked _ _) = examine t@@ -779,7 +795,7 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) = mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurences x) l) (getAllRedirs list))) list where- note x = Note x InfoC 2094 $+ 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) = when (exceptId /= newId@@ -789,17 +805,17 @@ addNote $ note newId addNote $ note exceptId checkOccurences _ _ = return ()- getAllRedirs l = concatMap (\(T_Redirecting _ ls _) -> concatMap getRedirs ls) l+ getAllRedirs = concatMap (\(T_Redirecting _ ls _) -> concatMap getRedirs ls) getRedirs (T_FdRedirect _ _ (T_IoFile _ op file)) = case op of T_Greater _ -> [file] T_Less _ -> [file] T_DGREAT _ -> [file] _ -> [] getRedirs _ = []- special x = "/dev/" `isPrefixOf` (concat $ deadSimple x)+ special x = "/dev/" `isPrefixOf` concat (deadSimple x) isOutput t = case drop 1 $ getPath (parentMap params) t of- (T_IoFile _ op _):_ ->+ T_IoFile _ op _:_ -> case op of T_Greater _ -> True T_DGREAT _ -> True@@ -816,7 +832,7 @@ | not $ isOk t = info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true." where- isOk [t] = isAssignment t || (fromMaybe False $ do+ isOk [t] = isAssignment t || fromMaybe False (do name <- getCommandBasename t return $ name `elem` ["echo", "exit", "return"]) isOk _ = False@@ -825,10 +841,10 @@ prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done" prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"-checkDollarStar p t@(T_NormalWord _ [(T_DollarBraced id l)])- | (bracedString l) == "*" =+checkDollarStar p t@(T_NormalWord _ [T_DollarBraced id l])+ | bracedString l == "*" = unless isAssigned $- warn id 2048 $ "Use \"$@\" (with quotes) to prevent whitespace problems."+ warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems." where path = getPath (parentMap p) t isAssigned = any isAssignment . take 2 $ path@@ -843,14 +859,28 @@ prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }" prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@" checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not isAssigned =- flip mapM_ (take 1 $ filter isArrayExpansion parts) $ \x -> do- err (getId x) 2068 $+ 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$@\""+prop_checkConcatenatedDollarAt2 = verify checkConcatenatedDollarAt "echo ${arr[@]}lol"+prop_checkConcatenatedDollarAt3 = verify checkConcatenatedDollarAt "echo $a$@"+prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@"+prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\""+checkConcatenatedDollarAt p word@(T_NormalWord {})+ | not $ isQuoteFree (parentMap p) word =+ unless (null $ drop 1 parts) $+ mapM_ for array+ where+ parts = getWordParts word+ array = take 1 $ filter isArrayExpansion parts+ for t = err (getId t) 2145 "Argument mixes string and array. Use * or separate argument."+checkConcatenatedDollarAt _ _ = return () prop_checkArrayAsString1 = verify checkArrayAsString "a=$@" prop_checkArrayAsString2 = verify checkArrayAsString "a=\"${arr[@]}\""@@ -880,8 +910,8 @@ return . maybeToList $ do name <- getLiteralString token assignment <- Map.lookup name map- return [(Note id WarningC 2128- "Expanding an array without an index only gives the first element.")]+ return [Note id WarningC 2128+ "Expanding an array without an index only gives the first element."] readF _ _ _ = return [] writeF _ t name (DataFrom [T_Array {}]) = do@@ -900,11 +930,11 @@ T_Greater _ -> error T_DGREAT _ -> error _ -> return ()- where error = err id 2069 $ "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last."+ where error = err id 2069 "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last." checkStderrRedirect _ _ = return () -lt x = trace ("FAILURE " ++ (show x)) x-ltt t x = trace ("FAILURE " ++ (show t)) x+lt x = trace ("FAILURE " ++ show x) x+ltt t = trace ("FAILURE " ++ show t) prop_checkSingleQuotedVariables = verify checkSingleQuotedVariables "echo '$foo'"@@ -925,15 +955,14 @@ else unless isProbablyOk showMessage where parents = parentMap params- showMessage = info id 2016 $+ showMessage = info id 2016 "Expressions don't expand in single quotes, use double quotes for that." commandName = fromMaybe "" $ do cmd <- getClosestCommand parents t- name <- getCommandBasename cmd- return name+ getCommandBasename cmd isProbablyOk =- (any isOkAssignment $ take 3 $ getPath parents t)+ any isOkAssignment (take 3 $ getPath parents t) || commandName `elem` [ "trap" ,"sh"@@ -978,22 +1007,22 @@ prop_checkNumberComparisons11= verify checkNumberComparisons "[[ $foo -eq 'N' ]]" prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]" checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do- if (isNum lhs && (not $ isNonNum rhs)- || isNum rhs && (not $ isNonNum lhs))+ if isNum lhs && not (isNonNum rhs)+ || isNum rhs && not (isNonNum lhs) then do when (isLtGt op) $ err id 2071 $- op ++ " is for string comparisons. Use " ++ (eqv op) ++ " instead."+ op ++ " is for string comparisons. Use " ++ eqv op ++ " instead." when (isLeGe op) $ err id 2071 $ op ++ " is not a valid operator. " ++- "Use " ++ (eqv op) ++ " ."+ "Use " ++ eqv op ++ " ." else do when (isLeGe op || isLtGt op) $ mapM_ checkDecimals [lhs, rhs] when (isLeGe op) $ err id 2122 $ op ++ " is not a valid operator. " ++- "Use '! a " ++ (invert op) ++ " b' instead."+ "Use '! a " ++ invert op ++ " b' instead." when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do mapM_ checkDecimals [lhs, rhs]@@ -1021,7 +1050,7 @@ numChar x = isDigit x || x `elem` "+-. " stringError t = err (getId t) 2130 $- op ++ " is for integer comparisons. Use " ++ (seqv op) ++ " instead."+ op ++ " is for integer comparisons. Use " ++ seqv op ++ " instead." isNum t = case deadSimple t of@@ -1096,7 +1125,7 @@ T_NormalWord id [T_SingleQuoted _ _] -> error id _ -> return () where- error id = err id 2076 $ "Don't quote rhs of =~, it'll match literally rather than as a regex."+ error id = err id 2076 "Don't quote rhs of =~, it'll match literally rather than as a regex." checkQuotedCondRegex _ _ = return () prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"@@ -1106,9 +1135,8 @@ prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]" checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) = let s = concat $ deadSimple rhs in- if isConfusedGlobRegex s- then warn (getId rhs) 2049 $ "=~ is for regex. Use == for globs."- else return ()+ when (isConfusedGlobRegex s) $+ warn (getId rhs) 2049 "=~ is for regex. Use == for globs." checkGlobbedRegex _ _ = return () @@ -1118,8 +1146,8 @@ prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]" prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]" checkConstantIfs _ (TC_Binary id typ op lhs rhs)- | op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] = do- when (isJust lLit && isJust rLit) $ warn id 2050 $ "This expression is constant. Did you forget the $ on a variable?"+ | op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] =+ when (isJust lLit && isJust rLit) $ warn id 2050 "This expression is constant. Did you forget the $ on a variable?" where lLit = getLiteralString lhs rLit = getLiteralString rhs@@ -1130,32 +1158,35 @@ prop_checkNoaryWasBinary3 = verify checkNoaryWasBinary "[ $foo!=3 ]" checkNoaryWasBinary _ (TC_Noary _ _ t@(T_NormalWord id l)) | not $ isConstant t = do let str = concat $ deadSimple t- when ('=' `elem` str) $ err id 2077 $ "You need spaces around the comparison operator."+ when ('=' `elem` str) $ err id 2077 "You need spaces around the comparison operator." checkNoaryWasBinary _ _ = return () prop_checkConstantNoary = verify checkConstantNoary "[[ '$(foo)' ]]" prop_checkConstantNoary2 = verify checkConstantNoary "[ \"-f lol\" ]" prop_checkConstantNoary3 = verify checkConstantNoary "[[ cmd ]]" prop_checkConstantNoary4 = verify checkConstantNoary "[[ ! cmd ]]"-checkConstantNoary _ (TC_Noary _ _ t@(T_NormalWord id _)) | isConstant t = do- err id 2078 $ "This expression is constant. Did you forget a $ somewhere?"+checkConstantNoary _ (TC_Noary _ _ t@(T_NormalWord id _)) | isConstant t =+ err id 2078 "This expression is constant. Did you forget a $ somewhere?" checkConstantNoary _ _ = return () prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}" prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}" checkBraceExpansionVars _ (T_BraceExpansion id s) | "..$" `isInfixOf` s =- warn id 2051 $ "Bash doesn't support variables in brace range expansions."+ warn id 2051 "Bash doesn't support variables in brace range expansions." checkBraceExpansionVars _ _ = return () prop_checkForDecimals = verify checkForDecimals "((3.14*c))"-checkForDecimals _ (TA_Literal id s) | any (== '.') s = do- err id 2079 $ "(( )) doesn't support decimals. Use bc or awk."+checkForDecimals _ t@(TA_Expansion id _) = potentially $ do+ str <- getLiteralString t+ first <- str !!! 0+ guard $ isDigit first && '.' `elem` str+ return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." checkForDecimals _ _ = return () prop_checkDivBeforeMult = verify checkDivBeforeMult "echo $((c/n*100))" prop_checkDivBeforeMult2 = verifyNot checkDivBeforeMult "echo $((c*100/n))"-checkDivBeforeMult _ (TA_Binary _ "*" (TA_Binary id "/" _ _) _) = do- info id 2017 $ "Increase precision by replacing a/b*c with a*c/b."+checkDivBeforeMult _ (TA_Binary _ "*" (TA_Binary id "/" _ _) _) =+ info id 2017 "Increase precision by replacing a/b*c with a*c/b." checkDivBeforeMult _ _ = return () prop_checkArithmeticDeref = verify checkArithmeticDeref "echo $((3+$foo))"@@ -1163,24 +1194,33 @@ prop_checkArithmeticDeref3 = verifyNot checkArithmeticDeref "cow=1/40; (( s+= ${cow%%/*} ))" prop_checkArithmeticDeref4 = verifyNot checkArithmeticDeref "(( ! $? ))" prop_checkArithmeticDeref5 = verifyNot checkArithmeticDeref "(($1))"-prop_checkArithmeticDeref6 = verifyNot checkArithmeticDeref "(( ${a[$i]} ))"+prop_checkArithmeticDeref6 = verify checkArithmeticDeref "(( a[$i] ))" 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."+prop_checkArithmeticDeref8 = verifyNot checkArithmeticDeref "let i=$i+1"+checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id l]) =+ unless ((isException $ bracedString l) || (not isNormal)) $+ 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)+ isException [] = True+ isException s = any (`elem` "/.:#%?*@") s || isDigit (head s)+ isNormal = fromMaybe True $ msum $ map isNormalContext $ (parents params t)+ isNormalContext t =+ case t of+ T_Arithmetic {} -> return True+ T_DollarArithmetic {} -> return True+ T_SimpleCommand {} -> return False+ _ -> fail "Irrelevant" checkArithmeticDeref _ _ = return () prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))" prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))" prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))"-checkArithmeticBadOctal _ (TA_Base id "0" (TA_Literal _ str)) | '9' `elem` str || '8' `elem` str =- err id 2080 $ "Numbers with leading 0 are considered octal."+checkArithmeticBadOctal _ t@(TA_Expansion id _) = potentially $ do+ str <- getLiteralString t+ guard $ str `matches` octalRE+ return $ err id 2080 "Numbers with leading 0 are considered octal."+ where+ octalRE = mkRegex "^0[0-7]*[8-9]" checkArithmeticBadOctal _ _ = return () prop_checkComparisonAgainstGlob = verify checkComparisonAgainstGlob "[[ $cow == $bar ]]"@@ -1188,25 +1228,28 @@ prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]" prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]" checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _])) | op == "=" || op == "==" =- warn id 2053 $ "Quote the rhs of = in [[ ]] to prevent glob interpretation."+ warn id 2053 "Quote the rhs of = in [[ ]] to prevent glob interpretation." checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word) | (op == "=" || op == "==") && isGlob word =- err (getId word) 2081 $ "[ .. ] can't match globs. Use [[ .. ]] or grep."+ err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or grep." checkComparisonAgainstGlob _ _ = return () prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)" prop_checkCommarrays2 = verify checkCommarrays "a+=(1,2,3)" prop_checkCommarrays3 = verifyNot checkCommarrays "cow=(1 \"foo,bar\" 3)" prop_checkCommarrays4 = verifyNot checkCommarrays "cow=('one,' 'two')"+prop_checkCommarrays5 = verify checkCommarrays "a=([a]=b, [c]=d)"+prop_checkCommarrays6 = verify checkCommarrays "a=([a]=b,[c]=d,[e]=f)" checkCommarrays _ (T_Array id l) = when (any (isCommaSeparated . literal) l) $ warn id 2054 "Use spaces, not commas, to separate array elements." where+ literal (T_IndexedElement _ _ l) = literal l literal (T_NormalWord _ l) = concatMap literal l literal (T_Literal _ str) = str literal _ = "str" - isCommaSeparated str = "," `isSuffixOf` str || (length $ filter (== ',') str) > 1+ 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"@@ -1229,10 +1272,10 @@ prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]" prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]" checkValidCondOps _ (TC_Binary id _ s _ _)- | not (s `elem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="]) =+ | s `notElem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="] = warn id 2057 "Unknown binary operator." checkValidCondOps _ (TC_Unary id _ s _)- | not (s `elem` [ "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-o", "-v", "-R"]) =+ | s `notElem` [ "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-o", "-v", "-R"] = warn id 2058 "Unknown unary operator." checkValidCondOps _ _ = return () @@ -1241,14 +1284,14 @@ getParentTree t = snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty) where- pre t = modify (\(l, m) -> (t:l, m))+ pre t = modify (first ((:) t)) post t = do- ((_:rest), map) <- get+ (_:rest, map) <- get case rest of [] -> put (rest, map) (x:_) -> put (rest, Map.insert (getId t) x map) getTokenMap t =- snd $ runState (doAnalysis f t) (Map.empty)+ execState (doAnalysis f t) Map.empty where f t = modify (Map.insert (getId t) t) @@ -1256,7 +1299,7 @@ -- Is this node self quoting? isQuoteFree tree t = (isQuoteFreeElement t == Just True) ||- (head $ (mapMaybe isQuoteFreeContext $ drop 1 $ getPath tree t) ++ [False])+ head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False]) where -- Is this node self-quoting in itself? isQuoteFreeElement t =@@ -1270,24 +1313,22 @@ 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+ TA_Sequence {} -> return True+ T_Arithmetic {} -> return True T_Assignment {} -> return True- T_Redirecting _ _ _ -> return $+ T_Redirecting {} -> return $ any (isCommand t) ["local", "declare", "typeset", "export"] T_DoubleQuoted _ _ -> return True- T_CaseExpression _ _ _ -> return True- T_HereDoc _ _ _ _ _ -> 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 _ -> Nothing -isParamTo tree cmd t =- go t+isParamTo tree cmd =+ go where go x = case Map.lookup (getId x) tree of Nothing -> False@@ -1297,24 +1338,24 @@ T_SingleQuoted _ _ -> go t T_DoubleQuoted _ _ -> go t T_NormalWord _ _ -> go t- T_SimpleCommand _ _ _ -> isCommand t cmd- T_Redirecting _ _ _ -> isCommand t cmd+ T_SimpleCommand {} -> isCommand t cmd+ T_Redirecting {} -> isCommand t cmd _ -> False getClosestCommand tree t = msum . map getCommand $ getPath tree t where- getCommand t@(T_Redirecting _ _ _) = return t+ getCommand t@(T_Redirecting {}) = return t getCommand _ = Nothing usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) where- go currentId ((T_NormalWord id [word]):rest)- | currentId == (getId word) = go id rest- go currentId ((T_DoubleQuoted id [word]):rest)- | currentId == (getId word) = go id rest- go currentId ((T_SimpleCommand _ _ (word:_)):_)- | currentId == (getId word) = True+ go currentId (T_NormalWord id [word]:rest)+ | currentId == getId word = go id rest+ go currentId (T_DoubleQuoted id [word]:rest)+ | currentId == getId word = go id rest+ go currentId (T_SimpleCommand _ _ (word:_):_)+ | currentId == getId word = True go _ _ = False -- A list of the element and all its parents@@ -1323,16 +1364,16 @@ Nothing -> [] Just parent -> getPath tree parent -parents params t = getPath (parentMap params) t+parents params = getPath (parentMap params) --- Command specific checks checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =- if t `isCommand` str then f cmd rest else return ()+ when (t `isCommand` str) $ f cmd rest checkCommand _ _ _ = return () checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =- if t `isUnqualifiedCommand` str then f cmd rest else return ()+ when (t `isUnqualifiedCommand` str) $ f cmd rest checkUnqualifiedCommand _ _ _ = return () getLiteralString = getLiteralStringExt (const Nothing)@@ -1342,12 +1383,13 @@ f (T_Glob _ str) = return str f _ = Nothing -getLiteralStringExt more t = g t+getLiteralStringExt more = g where- allInList l = let foo = map g l in if all isJust foo then return $ concat (catMaybes foo) else Nothing- g s@(T_DoubleQuoted _ l) = allInList l- g s@(T_DollarDoubleQuoted _ l) = allInList l- g s@(T_NormalWord _ l) = allInList l+ allInList = liftM concat . sequence . map g+ g (T_DoubleQuoted _ l) = allInList l+ g (T_DollarDoubleQuoted _ l) = allInList l+ g (T_NormalWord _ l) = allInList l+ g (TA_Expansion _ l) = allInList l g (T_SingleQuoted _ s) = return s g (T_Literal _ s) = return s g x = more x@@ -1355,14 +1397,12 @@ isLiteral t = isJust $ getLiteralString t -- turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]-getWordParts t = g t- where- g (T_NormalWord _ l) = concatMap g l- g (T_DoubleQuoted _ l) = l- g other = [other]+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)-isUnqualifiedCommand token str = isCommandMatch token (\cmd -> cmd == str)+isUnqualifiedCommand token str = isCommandMatch token (== str) isCommandMatch token matcher = fromMaybe False $ do cmd <- getCommandName token@@ -1376,7 +1416,7 @@ getCommandName _ = Nothing getCommandBasename = liftM basename . getCommandName-basename = reverse . (takeWhile (/= '/')) . reverse+basename = reverse . takeWhile (/= '/') . reverse isAssignment (T_Annotation _ _ w) = isAssignment w isAssignment (T_Redirecting _ _ w) = isAssignment w@@ -1389,14 +1429,13 @@ prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var" checkPrintfVar _ = checkUnqualifiedCommand "printf" (const f) where- f (dashv:var:rest) | getLiteralString dashv == (Just "-v") = f rest+ f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest f (format:params) = check format f _ = return () check format =- if '%' `elem` (concat $ deadSimple format) || isLiteral format- then return ()- else warn (getId format) 2059 $- "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."+ unless ('%' `elem` concat (deadSimple format) || isLiteral format) $+ warn (getId format) 2059+ "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"." prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"@@ -1405,10 +1444,10 @@ 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 [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 _ = return () prop_checkUuoeVar1 = verify checkUuoeVar "for f in $(echo $tmp); do echo lol; done"@@ -1434,7 +1473,7 @@ check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c check _ _ = return () warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars ->- unless ("-" `isPrefixOf` (concat $ concatMap deadSimple 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'." @@ -1453,23 +1492,23 @@ prop_checkTr11= verifyNot checkTr "tr abc '[d*]'" 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."+ f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?+ warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion." f word = case getLiteralString word of Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets." Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets." Just s -> do -- Eliminate false positives by only looking for dupes in SET2?- when ((not $ "-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $+ when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $ info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)." unless ("[:" `isPrefixOf` s) $- when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && (not $ '*' `elem` s)) $+ when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $ info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets." Nothing -> return () duplicated s = let relevant = filter isAlpha s- in not $ relevant == nub relevant+ in relevant /= nub relevant prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"@@ -1506,21 +1545,21 @@ f [] = return () f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r f (re:_) = do- when (isGlob re) $ do- warn (getId re) 2062 $ "Quote the grep pattern so the shell won't interpret it."+ when (isGlob re) $+ warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." let string = concat $ deadSimple re if isConfusedGlobRegex string then- warn (getId re) 2063 $ "Grep uses regex, but this looks like a glob."+ warn (getId re) 2063 "Grep uses regex, but this looks like a glob." else potentially $ do char <- getSuspiciousRegexWildcard string return $ info (getId re) 2022 $- "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ (wordStartingWith char) ++ "'."+ "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'." wordStartingWith c = head . filter ([c] `isPrefixOf`) $ candidates where candidates =- sampleWords ++ (map (\(x:r) -> (toUpper x) : r) sampleWords) ++ [c:"test"]+ sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"] prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"@@ -1531,7 +1570,7 @@ f _ = return () checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs checkTrap _ = return ()- warning id = warn id 2064 $ "Use single quotes, otherwise this expands now rather than when signalled."+ warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled." checkExpansions (T_DollarExpansion id _) = warning id checkExpansions (T_Backticked id _) = warning id checkExpansions (T_DollarBraced id _) = warning id@@ -1543,16 +1582,15 @@ prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo" checkTimeParameters _ = checkUnqualifiedCommand "time" f where f cmd (x:_) = let s = concat $ deadSimple x in- if "-" `isPrefixOf` s && s /= "-p" then+ when ("-" `isPrefixOf` s && s /= "-p") $ info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."- else return () f _ _ = return () prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1" prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1" prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo" checkTestRedirects _ (T_Redirecting id redirs@(redir:_) cmd) | cmd `isCommand` "test" =- warn (getId redir) 2065 $ "This is interpretted as a shell file redirection, not a comparison."+ warn (getId redir) 2065 "This is interpretted as a shell file redirection, not a comparison." checkTestRedirects _ _ = return () prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"@@ -1566,20 +1604,20 @@ mapM_ warnAbout redirs where warnAbout (T_FdRedirect _ s (T_IoFile id op file))- | (s == "" || s == "&") && (not $ special file) =+ | (s == "" || s == "&") && not (special file) = case op of T_Less _ ->- info (getId op) 2024 $+ info (getId op) 2024 "sudo doesn't affect redirects. Use sudo cat file | .." T_Greater _ ->- warn (getId op) 2024 $+ warn (getId op) 2024 "sudo doesn't affect redirects. Use ..| sudo tee file" T_DGREAT _ ->- warn (getId op) 2024 $+ warn (getId op) 2024 "sudo doesn't affect redirects. Use .. | sudo tee -a file" _ -> return () warnAbout _ = return ()- special file = (concat $ deadSimple file) == "/dev/null"+ special file = concat (deadSimple file) == "/dev/null" checkSudoRedirect _ _ = return () prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"@@ -1621,8 +1659,8 @@ err i 2082 "To expand via indirection, use name=\"foo$n\"; echo \"${!name}\"." where isIndirection vars =- let list = catMaybes (map isIndirectionPart vars) in- not (null list) && all id list+ let list = mapMaybe isIndirectionPart vars in+ not (null list) && and list isIndirectionPart t = case t of T_DollarExpansion _ _ -> Just True T_Backticked _ _ -> Just True@@ -1642,11 +1680,11 @@ prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\"" checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens) where- check ((T_SingleQuoted _ _):(T_Literal id str):_)+ check (T_SingleQuoted _ _:T_Literal id str:_) | all isAlphaNum str =- info id 2026 $ "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? "+ info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? " - check ((T_DoubleQuoted _ _):trapped:(T_DoubleQuoted _ _):_) =+ check (T_DoubleQuoted _ _:trapped:T_DoubleQuoted _ _:_) = case trapped of T_DollarExpansion id _ -> warnAboutExpansion id T_DollarBraced id _ -> warnAboutExpansion id@@ -1655,9 +1693,9 @@ check _ = return () warnAboutExpansion id =- warn id 2027 $ "The surrounding quotes actually unquote this. Remove or escape them."+ warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them." warnAboutLiteral id =- warn id 2140 $ "The double quotes around this do nothing. Remove or escape them."+ warn id 2140 "The double quotes around this do nothing. Remove or escape them." checkInexplicablyUnquoted _ _ = return () prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""@@ -1669,9 +1707,9 @@ where verify id ('~':_) = warn id 2088 "Note that ~ does not expand in quotes." verify _ _ = return ()- check (T_NormalWord _ ((T_SingleQuoted id str):_)) =+ check (T_NormalWord _ (T_SingleQuoted id str:_)) = verify id str- check (T_NormalWord _ ((T_DoubleQuoted _ ((T_Literal id str):_)):_)) =+ check (T_NormalWord _ (T_DoubleQuoted _ (T_Literal id str:_):_)) = verify id str check _ = return () @@ -1719,7 +1757,7 @@ commentIfExec (T_Redirecting _ _ f@( T_SimpleCommand id _ (cmd:arg:_))) = when (f `isUnqualifiedCommand` "exec") $- warn (id) 2093 $+ warn id 2093 "Remove \"exec \" if script should continue after this command." commentIfExec _ = return () @@ -1751,7 +1789,7 @@ where isDashE = mkRegex "^-.*e" hasEscapes = mkRegex "\\\\[rnt]"- f args | (concat $ concatMap deadSimple allButLast) `matches` isDashE =+ f args | concat (concatMap deadSimple allButLast) `matches` isDashE = return () where allButLast = reverse . drop 1 . reverse $ args f args = mapM_ checkEscapes args@@ -1794,8 +1832,8 @@ prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\"" checkSshCommandString _ = checkCommand "ssh" (const f) where- nonOptions args =- filter (\x -> not $ "-" `isPrefixOf` (concat $ deadSimple x)) args+ nonOptions =+ filter (\x -> not $ "-" `isPrefixOf` concat (deadSimple x)) f args = case nonOptions args of (hostport:r@(_:_)) -> checkArg $ last r@@ -1803,7 +1841,7 @@ checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) = case filter (not . isConstant) parts of [] -> return ()- (x:_) -> info (getId x) 2029 $+ (x:_) -> info (getId x) 2029 "Note that, unescaped, this expands on the client side." checkArg _ = return () @@ -1850,7 +1888,7 @@ T_Backticked _ _ -> SubshellScope "`..` expansion" T_Backgrounded _ _ -> SubshellScope "backgrounding &" T_Subshell _ _ -> SubshellScope "(..) group"- T_Redirecting _ _ _ ->+ T_Redirecting {} -> if fromMaybe False causesSubshell then SubshellScope "pipeline" else NoneScope@@ -1859,7 +1897,7 @@ parentPipeline = do parent <- Map.lookup (getId t) parents case parent of- T_Pipeline _ _ _ -> return parent+ T_Pipeline {} -> return parent _ -> Nothing causesSubshell = do@@ -1868,7 +1906,7 @@ then return False else if lastCreatesSubshell then return True- else return . not $ (getId . head $ reverse list) == (getId t)+ else return . not $ (getId . head $ reverse list) == getId t lastCreatesSubshell = case shell of@@ -1885,15 +1923,19 @@ [(x, x, name, DataFrom [w])] _ -> [] ) vars- c@(T_SimpleCommand _ _ _) ->+ c@(T_SimpleCommand {}) -> getModifiedVariableCommand c - TA_Unary _ "++|" (TA_Variable id name) -> [(t, t, name, DataFrom [t])]- TA_Unary _ "|++" (TA_Variable id name) -> [(t, t, name, DataFrom [t])]- TA_Binary _ op (TA_Variable id name) rhs ->- if any (==op) ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]- then [(t, t, name, DataFrom [rhs])]- else []+ TA_Unary _ "++|" var -> maybeToList $ do+ name <- getLiteralString var+ return (t, t, name, DataFrom [t])+ TA_Unary _ "|++" var -> maybeToList $ do+ name <- getLiteralString var+ return (t, t, name, DataFrom [t])+ TA_Binary _ op lhs rhs -> maybeToList $ do+ guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]+ name <- getLiteralString lhs+ return (t, t, name, DataFrom [rhs]) --Points to 'for' rather than variable T_ForIn id _ strs words _ -> map (\str -> (t, t, str, DataFrom words)) strs@@ -1901,7 +1943,7 @@ _ -> [] -- Consider 'export/declare -x' a reference, since it makes the var available-getReferencedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) =+getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) = case x of "export" -> concatMap getReference rest "declare" -> if "x" `elem` getFlags base@@ -1915,7 +1957,7 @@ getReferencedVariableCommand _ = [] -getModifiedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) =+getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) = filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $ case x of "read" ->@@ -1932,10 +1974,10 @@ where stripEquals s = let rest = dropWhile (/= '=') s in if rest == "" then "" else tail rest- stripEqualsFrom (T_NormalWord id1 ((T_Literal id2 s):rs)) =- (T_NormalWord id1 ((T_Literal id2 (stripEquals s)):rs))+ stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =+ T_NormalWord id1 (T_Literal id2 (stripEquals s):rs) stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =- (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]])+ T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]] stripEqualsFrom t = t getLiteral t = do@@ -1951,11 +1993,11 @@ if var == "" then [] else [(base, token, var, DataFrom [stripEqualsFrom token])]- where var = takeWhile (isVariableChar) $ dropWhile (\x -> x `elem` "+-") $ concat $ deadSimple token+ where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ deadSimple token getModifiedVariableCommand _ = [] -- TODO:-getBracedReference s = takeWhile (\x -> not $ x `elem` ":[#%/^,") $ dropWhile (`elem` "#!") s+getBracedReference s = takeWhile (`notElem` ":[#%/^,") $ dropWhile (`elem` "#!") s getIndexReferences s = fromMaybe [] $ do (_, index, _, _) <- matchRegexAll re s return $ matchAll variableNameRegex index@@ -1966,9 +2008,10 @@ case t of 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)+ map (\x -> (l, l, x)) (getIndexReferences str)+ TA_Expansion id _ -> maybeToList $ do+ str <- getLiteralStringExt (const $ return "#") t+ return (t, t, getBracedReference str) T_Assignment id Append str _ _ -> [(t, t, str)] x -> getReferencedVariableCommand x @@ -1979,15 +2022,15 @@ startScope t = let scopeType = leadType shell parents t in do- when (scopeType /= NoneScope) $ modify ((StackScope scopeType):)- if assignFirst t then setWritten t else return ()+ when (scopeType /= NoneScope) $ modify (StackScope scopeType:)+ when (assignFirst t) $ setWritten t endScope t = let scopeType = leadType shell parents t in do setRead t- if assignFirst t then return () else setWritten t- when (scopeType /= NoneScope) $ modify ((StackScopeEnd):)+ unless (assignFirst t) $ setWritten t+ when (scopeType /= NoneScope) $ modify (StackScopeEnd:) assignFirst (T_ForIn {}) = True assignFirst (T_SelectIn {}) = True@@ -1995,16 +2038,16 @@ setRead t = let read = getReferencedVariables t- in mapM_ (\v -> modify ((Reference v):)) read+ in mapM_ (\v -> modify (Reference v:)) read setWritten t = let written = getModifiedVariables t- in mapM_ (\v -> modify ((Assignment v):)) written+ in mapM_ (\v -> modify (Assignment v:)) written findSubshelled [] _ _ = return ()-findSubshelled ((Assignment x@(_, _, str, _)):rest) ((reason,scope):lol) deadVars =+findSubshelled (Assignment x@(_, _, str, _):rest) ((reason,scope):lol) deadVars = findSubshelled rest ((reason, x:scope):lol) $ Map.insert str Alive deadVars-findSubshelled ((Reference (_, readToken, str)):rest) scopes deadVars = do+findSubshelled (Reference (_, readToken, str):rest) scopes deadVars = do case Map.findWithDefault Alive str deadVars of Alive -> return () Dead writeToken reason -> do@@ -2012,15 +2055,15 @@ info (getId readToken) 2031 $ str ++ " was modified in a subshell. That change might be lost." findSubshelled rest scopes deadVars -findSubshelled ((StackScope (SubshellScope reason)):rest) scopes deadVars =+findSubshelled (StackScope (SubshellScope reason):rest) scopes deadVars = findSubshelled rest ((reason,[]):scopes) deadVars -findSubshelled ((StackScopeEnd):rest) ((reason, scope):oldScopes) deadVars =+findSubshelled (StackScopeEnd:rest) ((reason, scope):oldScopes) deadVars = findSubshelled rest oldScopes $ foldl (\m (_, token, var, _) -> Map.insert var (Dead token reason) m) deadVars scope -doVariableFlowAnalysis readFunc writeFunc empty flow = fst $ runState (+doVariableFlowAnalysis readFunc writeFunc empty flow = evalState ( foldM (\list x -> do { l <- doFlow x; return $ l ++ list; }) [] flow ) empty where@@ -2062,18 +2105,17 @@ map <- get return $ Map.findWithDefault True name map - setSpaces name bool = do+ setSpaces name bool = modify $ Map.insert name bool readF _ token name = do spaced <- hasSpaces name- if spaced- && (not $ "@" `isPrefixOf` name) -- There's another warning for this- && (not $ isCounting token)- && (not $ isQuoteFree parents token)- && (not $ usedAsCommandName parents token)- then return [(Note (getId token) InfoC 2086 warning)]- else return []+ return [Note (getId token) InfoC 2086 warning |+ spaced+ && not ("@" `isPrefixOf` name) -- There's another warning for this+ && not (isCounting token)+ && not (isQuoteFree parents token)+ && not (usedAsCommandName parents token)] where warning = "Double quote to prevent globbing and word splitting." @@ -2096,14 +2138,14 @@ isCounting _ = False isSpacefulWord :: (String -> Bool) -> [Token] -> Bool- isSpacefulWord f words = any (isSpaceful f) words+ isSpacefulWord f = any (isSpaceful f) isSpaceful :: (String -> Bool) -> Token -> Bool isSpaceful spacefulF x = case x of T_DollarExpansion _ _ -> True T_Backticked _ _ -> True T_Glob _ _ -> True- T_Extglob _ _ _ -> True+ T_Extglob {} -> True T_Literal _ s -> s `containsAny` globspace T_SingleQuoted _ s -> s `containsAny` globspace T_DollarBraced _ l -> spacefulF $ getBracedReference $ bracedString l@@ -2112,7 +2154,7 @@ _ -> False where globspace = "*? \t\n"- containsAny s chars = any (\c -> c `elem` s) chars+ containsAny s = any (`elem` s) prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"@@ -2157,16 +2199,17 @@ readF _ expr name = do assignment <- getQuotes name- if isJust assignment- && not (isParamTo parents "eval" expr)- && not (isQuoteFree parents expr)- then return [- Note (fromJust assignment)WarningC 2089 $- "Quotes/backslashes will be treated literally. Use an array.",- Note (getId expr) WarningC 2090 $- "Quotes/backslashes in this variable will not be respected."- ]- else return []+ return+ (if isJust assignment+ && not (isParamTo parents "eval" expr)+ && not (isQuoteFree parents expr)+ then [+ Note (fromJust assignment)WarningC 2089+ "Quotes/backslashes will be treated literally. Use an array.",+ Note (getId expr) WarningC 2090+ "Quotes/backslashes in this variable will not be respected."+ ]+ else []) prop_checkFunctionsUsedExternally1 =@@ -2193,7 +2236,7 @@ mapM_ (checkArg name) args checkCommand _ _ = return () - analyse f t = snd $ runState (doAnalysis f t) []+ analyse f t = execState (doAnalysis f t) [] functions = Map.fromList $ analyse findFunctions t findFunctions (T_Function id _ _ name _) = modify ((name, id):) findFunctions t@(T_SimpleCommand id _ (_:args))@@ -2207,7 +2250,7 @@ case Map.lookup (concat $ deadSimple arg) functions of Nothing -> return () Just id -> do- warn (getId arg) 2033 $+ 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 ++ "."@@ -2230,6 +2273,8 @@ prop_checkUnused15= verifyNotTree checkUnusedAssignments "x=(1); n=0; (( x[n] ))" prop_checkUnused16= verifyNotTree checkUnusedAssignments "foo=5; declare -x foo" prop_checkUnused17= verifyNotTree checkUnusedAssignments "read -i 'foo' -e -p 'Input: ' bar; $bar;"+prop_checkUnused18= verifyNotTree checkUnusedAssignments "a=1; arr=( [$a]=42 ); echo \"${arr[@]}\""+prop_checkUnused19= verifyNotTree checkUnusedAssignments "a=1; let b=a+1; echo $b" checkUnusedAssignments params t = snd $ runWriter (mapM_ checkAssignment flow) where flow = variableFlow params@@ -2246,7 +2291,7 @@ name ++ " appears unused. Verify it or export it." checkAssignment _ = return () - stripSuffix str = takeWhile isVariableChar str+ stripSuffix = takeWhile isVariableChar defaultMap = Map.fromList $ zip internalVariables $ repeat () prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"@@ -2255,9 +2300,9 @@ checkGlobsAsOptions _ (T_SimpleCommand _ _ args) = mapM_ check $ takeWhile (not . isEndOfArgs) args where- check v@(T_NormalWord _ ((T_Glob id s):_)) | s == "*" || s == "?" =+ check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" = info id 2035 $- "Use ./" ++ (concat $ deadSimple v)+ "Use ./" ++ concat (deadSimple v) ++ " so names with dashes won't become options." check _ = return () @@ -2280,7 +2325,7 @@ prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file" checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)- | isStdinReadCommand command = do+ | isStdinReadCommand command = mapM_ checkMuncher contents where munchers = [ "ssh", "ffmpeg", "mplayer" ]@@ -2292,10 +2337,10 @@ && all (not . stdinRedirect) redirs isStdinReadCommand _ = False - checkMuncher (T_Pipeline _ _ ((T_Redirecting _ redirs cmd):_)) | not $ any stdinRedirect redirs = do+ checkMuncher (T_Pipeline _ _ (T_Redirecting _ redirs cmd:_)) | not $ any stdinRedirect redirs = case cmd of (T_IfExpression _ thens elses) ->- mapM_ checkMuncher . concat $ (map fst thens) ++ (map snd thens) ++ [elses]+ mapM_ checkMuncher . concat $ map fst thens ++ map snd thens ++ [elses] _ -> potentially $ do name <- getCommandBasename cmd@@ -2347,12 +2392,11 @@ && contents /= ":" then warn id 2101 "Named class needs outer [], e.g. [[:digit:]]." else- if ('[' `notElem` contents) && hasDupes- then info id 2102 "Ranges can only match single chars (mentioned due to duplicates)."- else return ()+ when ('[' `notElem` contents && hasDupes) $+ info id 2102 "Ranges can only match single chars (mentioned due to duplicates)." where isCharClass str = "[" `isPrefixOf` str && "]" `isSuffixOf` str- contents = drop 1 . take ((length str) - 1) $ str+ contents = drop 1 . take (length str - 1) $ str hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents checkCharRangeGlob _ _ = return () @@ -2404,10 +2448,10 @@ if not $ any isLoop path then if any isFunction $ take 1 path -- breaking at a source/function invocation is an abomination. Let's ignore it.- then err (getId t) 2104 $ "In functions, use return instead of " ++ (fromJust name) ++ "."- else err (getId t) 2105 $ (fromJust name) ++ " is only valid in loops."+ then err (getId t) 2104 $ "In functions, use return instead of " ++ fromJust name ++ "."+ else err (getId t) 2105 $ fromJust name ++ " is only valid in loops." else case map subshellType $ filter (not . isFunction) path of- (Just str):_ -> warn (getId t) 2106 $+ Just str:_ -> warn (getId t) 2106 $ "This only exits the subshell caused by the " ++ str ++ "." _ -> return () where@@ -2416,7 +2460,7 @@ subshellType t = case leadType (shellType params) (parentMap params) t of NoneScope -> Nothing SubshellScope str -> return str- isFunction t = case t of T_Function _ _ _ _ _ -> True; _ -> False+ isFunction t = case t of T_Function {} -> True; _ -> False relevant t = isLoop t || isFunction t || isJust (subshellType t) checkLoopKeywordScope _ _ = return () @@ -2426,10 +2470,10 @@ prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }" checkFunctionDeclarations params (T_Function id (FunctionKeyword hasKeyword) (FunctionParentheses hasParens) _ _) =- case (shellType params) of+ case shellType params of Bash -> return () Zsh -> return ()- Ksh -> do+ Ksh -> when (hasKeyword && hasParens) $ err id 2111 "ksh does not allow 'function' keyword and '()' at the same time." Sh -> do@@ -2451,7 +2495,7 @@ 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) $+ when (any isRecursiveFlag simpleArgs) $ mapM_ checkWord tokens where -- This ugly hack is based on the fact that ids generally increase@@ -2463,8 +2507,8 @@ 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."+ when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $+ info (getId token) 2114 "Obligatory typo warning. Use 'rm --' to disable this message." Nothing -> checkWord' token @@ -2472,12 +2516,12 @@ m <- relevantMap id filename <- combine m token let path = fixPath filename- return . when (path `elem` importantPaths) $ do+ return . when (path `elem` importantPaths) $ 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+ if normalized == "/" then normalized else stripTrailing '/' normalized unnullable = all isVariableChar . concat . deadSimple isRecursiveFlag "--recursive" = True@@ -2487,7 +2531,7 @@ 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 c (a:r) = a:skipRepeating c r skipRepeating _ [] = [] addNulls map (Reference (_, token, name)) =@@ -2498,13 +2542,10 @@ 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 [word]))+ | mightBeGuarded token = Map.insert name Nothing m+ | couldFail word = m+ | otherwise = Map.insert name (combine m word) m addNulls m (Assignment (_, token, name, DataFrom _)) = Map.insert name Nothing m addNulls map _ = map@@ -2515,7 +2556,7 @@ joinMaybes :: [Maybe String] -> Maybe String joinMaybes = foldl (liftM2 (++)) (Just "")- combine m token = c token+ combine m = c where c (T_DollarBraced _ t) | unnullable t = Map.findWithDefault (Just "") (concat $ deadSimple t) m@@ -2532,9 +2573,9 @@ mightBeGuarded token = any t (getPath (parentMap params) token) where- t (T_Condition _ _ _) = True- t (T_OrIf _ _ _) = True- t (T_AndIf _ _ _) = True+ t (T_Condition {}) = True+ t (T_OrIf {}) = True+ t (T_AndIf {}) = True t _ = False paths = [@@ -2673,8 +2714,10 @@ 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"+prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac" checkUnsupported params t =- when (shellType params `notElem` support) $+ when ((not $ null support) && (shellType params `notElem` support)) $ report name where (name, support) = shellSupport t@@ -2689,23 +2732,26 @@ T_ForIn _ _ (_:_:_) _ _ -> ("multi-index for loops", [Zsh]) T_ForIn _ ShortForIn _ _ _ -> ("short form for loops", [Zsh]) T_ProcSub _ "=" _ -> ("=(..) process substitution", [Zsh])- otherwise -> ("", [Bash, Ksh, Sh, Zsh])--getCommandSequences t =- f t+ T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list)+ otherwise -> ("", []) where- f (T_Script _ _ cmds) = [cmds]- f (T_BraceGroup _ cmds) = [cmds]- f (T_Subshell _ cmds) = [cmds]- f (T_WhileExpression _ _ cmds) = [cmds]- f (T_UntilExpression _ _ cmds) = [cmds]- f (T_ForIn _ _ _ _ cmds) = [cmds]- f (T_ForArithmetic _ _ _ _ cmds) = [cmds]- f (T_IfExpression _ thens elses) = (map snd thens) ++ [elses]- f _ = []+ forCase seps | any (== CaseContinue) seps = ("cases with ;;&", [Bash])+ forCase seps | any (== CaseFallThrough) seps = ("cases with ;&", [Bash, Ksh, Zsh])+ forCase _ = ("", []) -groupWith f l = groupBy (\x y -> f x == f y) l +getCommandSequences (T_Script _ _ cmds) = [cmds]+getCommandSequences (T_BraceGroup _ cmds) = [cmds]+getCommandSequences (T_Subshell _ cmds) = [cmds]+getCommandSequences (T_WhileExpression _ _ cmds) = [cmds]+getCommandSequences (T_UntilExpression _ _ cmds) = [cmds]+getCommandSequences (T_ForIn _ _ _ _ cmds) = [cmds]+getCommandSequences (T_ForArithmetic _ _ _ _ cmds) = [cmds]+getCommandSequences (T_IfExpression _ thens elses) = map snd thens ++ [elses]+getCommandSequences _ = []++groupWith f = groupBy ((==) `on` f)+ prop_checkMultipleAppends1 = verify checkMultipleAppends "foo >> file; bar >> file; baz >> file;" prop_checkMultipleAppends2 = verify checkMultipleAppends "foo >> file; bar | grep f >> file; baz >> file;" prop_checkMultipleAppends3 = verifyNot checkMultipleAppends "foo < file; bar < file; baz < file;"@@ -2722,7 +2768,7 @@ checkGroup _ = return () getTarget (T_Pipeline _ _ args@(_:_)) = getTarget (last args) getTarget (T_Redirecting id list _) = do- file <- (mapMaybe getAppend list) !!! 0+ file <- mapMaybe getAppend list !!! 0 return (file, id) getTarget _ = Nothing getAppend (T_FdRedirect _ _ (T_IoFile _ (T_DGREAT {}) f)) = return f@@ -2736,8 +2782,8 @@ checkUnqualifiedCommand "alias" (const f) where f = mapM_ checkArg- checkArg arg | '=' `elem` (concat $ deadSimple arg) =- flip mapM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $+ checkArg arg | '=' `elem` concat (deadSimple arg) =+ forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $ \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping." checkArg _ = return () @@ -2748,8 +2794,8 @@ str <- getLiteralString value return $ check str where- n = if (shellType params == Sh) then "'<literal linefeed here>'" else "$'\\n'"- t = if (shellType params == Sh) then "\"$(printf '\\t')\"" else "$'\\t'"+ n = if shellType params == Sh then "'<literal linefeed here>'" else "$'\\n'"+ t = if shellType params == Sh then "\"$(printf '\\t')\"" else "$'\\t'" check value = case value of "\\n" -> suggest n@@ -2815,3 +2861,25 @@ err (getId token) 2144 $ op ++ " doesn't work with globs. Use a for loop." checkTestGlobs _ _ = return ()++prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"+prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"+prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"+checkFindActionPrecedence params = checkCommand "find" (const f)+ where+ pattern = [isMatch, const True, isParam ["-o"], 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" ]+ isParam strs t = fromMaybe False $ do+ param <- getLiteralString t+ return $ param `elem` strs+ warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."++return []+runTests = $quickCheckAll+
ShellCheck/Data.hs view
@@ -1,6 +1,9 @@ module ShellCheck.Data where -shellcheckVersion = "0.3.3" -- Must also be updated in ShellCheck.cabal+import Data.Version (showVersion)+import Paths_ShellCheck (version)++shellcheckVersion = showVersion version internalVariables = [ -- Generic
ShellCheck/Parser.hs view
@@ -15,15 +15,15 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. -}-{-# LANGUAGE NoMonomorphismRestriction #-}--module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote) where+{-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell #-}+module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote, runTests) where import ShellCheck.AST import ShellCheck.Data import Text.Parsec import Debug.Trace import Control.Monad+import Control.Arrow (first) import Data.Char import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub) import qualified Data.Map as Map@@ -33,9 +33,10 @@ import System.IO import Text.Parsec.Error import GHC.Exts (sortWith)+import Test.QuickCheck.All (quickCheckAll) backslash = char '\\'-linefeed = (optional carriageReturn) >> char '\n'+linefeed = optional carriageReturn >> char '\n' singleQuote = char '\'' <|> unicodeSingleQuote doubleQuote = char '"' <|> unicodeDoubleQuote variableStart = upper <|> lower <|> oneOf "_"@@ -60,7 +61,7 @@ prop_spacing = isOk spacing " \\\n # Comment" spacing = do- x <- many (many1 linewhitespace <|> (try $ string "\\\n"))+ x <- many (many1 linewhitespace <|> try (string "\\\n")) optional readComment return $ concat x @@ -110,7 +111,7 @@ data Note = Note Id Severity Code String deriving (Show, Eq) data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq) data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)-data Context = ContextName SourcePos String | ContextAnnotation [Annotation]+data Context = ContextName SourcePos String | ContextAnnotation [Annotation] deriving (Show) type Code = Integer codeForParseNote (ParseNote _ _ code _) = code@@ -131,7 +132,7 @@ let newMap = Map.insert newId sourcepos map putState (newId, newMap, notes) return newId- where incId (Id n) = (Id $ n+1)+ where incId (Id n) = Id $ n+1 getNextId = do pos <- getPosition@@ -151,7 +152,7 @@ addParseNote n = do irrelevant <- shouldIgnoreCode (codeForParseNote n)- when (not irrelevant) $ do+ unless irrelevant $ do (a, b, notes) <- getState putState (a, b, n:notes) @@ -169,7 +170,7 @@ pos <- getPosition parseProblemAt pos level code msg -setCurrentContexts c = do+setCurrentContexts c = Ms.modify (\(list, _) -> (list, c)) getCurrentContexts = do@@ -192,8 +193,8 @@ parseProblemAt pos level code msg = do irrelevant <- shouldIgnoreCode code- when (not irrelevant) $- Ms.modify (\(list, current) -> ((ParseNote pos level code msg):list, current))+ unless irrelevant $+ Ms.modify (first ((:) (ParseNote pos level code msg))) -- Store non-parse problems inside @@ -209,15 +210,15 @@ optional follow return r -unexpecting s p = try $ do+unexpecting s p = try $ (try p >> unexpected s) <|> return () notFollowedBy2 = unexpecting "keyword/token" -disregard x = x >> return ()+disregard = void -reluctantlyTill p end = do- (lookAhead ((disregard $ try end) <|> eof) >> return []) <|> do+reluctantlyTill p end =+ (lookAhead (disregard (try end) <|> eof) >> return []) <|> do x <- p more <- reluctantlyTill p end return $ x:more@@ -229,15 +230,15 @@ more <- reluctantlyTill p end return $ x:more -attempting rest branch = do- ((try branch) >> rest) <|> rest+attempting rest branch =+ (try branch >> rest) <|> rest -orFail parser stuff = do+orFail parser stuff = try (disregard parser) <|> (disregard stuff >> fail "nope") wasIncluded p = option False (p >> return True) -acceptButWarn parser level code note = do+acceptButWarn parser level code note = optional $ try (do pos <- getPosition parser@@ -252,17 +253,17 @@ return v <|> do -- p failed without consuming input, abort context popContext- fail $ ""+ fail "" called s p = do pos <- getPosition withContext (ContextName pos s) p -withAnnotations anns p =- withContext (ContextAnnotation anns) p+withAnnotations anns =+ withContext (ContextAnnotation anns) -readConditionContents single = do- readCondContents `attempting` (lookAhead $ do+readConditionContents single =+ readCondContents `attempting` lookAhead (do pos <- getPosition s <- many1 letter when (s `elem` commonCommands) $@@ -273,7 +274,7 @@ readCondBinaryOp = try $ do optional guardArithmetic id <- getNextId- op <- (choice $ (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"])) <|> otherOp+ op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp hardCondSpacing return op where@@ -301,7 +302,7 @@ arg <- readCondWord return $ op arg) <|> (do- parseProblemAt pos ErrorC 1019 $ "Expected this to be an argument to the unary condition."+ parseProblemAt pos ErrorC 1019 "Expected this to be an argument to the unary condition." fail "oops") readCondUnaryOp = try $ do@@ -316,7 +317,7 @@ return ('-':s) readCondWord = do- notFollowedBy2 (try (spacing >> (string "]")))+ notFollowedBy2 (try (spacing >> string "]")) x <- readNormalWord pos <- getPosition when (endedWith "]" x) $ do@@ -324,14 +325,14 @@ "You need a space before the " ++ (if single then "]" else "]]") ++ "." fail "Missing space before ]" when (single && endedWith ")" x) $ do- parseProblemAt pos ErrorC 1021 $+ 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@(_:_)) =- case (last s) of T_Literal id s -> str `isSuffixOf` s- _ -> False+ case last s of T_Literal id s -> str `isSuffixOf` s+ _ -> False endedWith _ _ = False readCondAndOp = do@@ -364,9 +365,9 @@ op <- readCondBinaryOp y <- if isRegex then readRegex- else readCondWord <|> ( (parseProblemAt pos ErrorC 1027 $ "Expected another argument for this operator.") >> mzero)+ else readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero) return (x `op` y)- ) <|> (return $ TC_Noary id typ x)+ ) <|> return (TC_Noary id typ x) readCondGroup = do id <- getNextId@@ -389,7 +390,7 @@ xor x y = x && not y || not x && y -- Currently a bit of a hack since parsing rules are obscure- regexOperatorAhead = (lookAhead $ do+ regexOperatorAhead = lookAhead (do try (string "=~") <|> try (string "~=") return True) <|> return False@@ -467,6 +468,8 @@ prop_aD = isOk readArithmeticContents "foo[9*y+x]++" prop_aE = isOk readArithmeticContents "1+`echo 2`" prop_aF = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"+prop_a10= isOk readArithmeticContents "$foo$bar"+prop_a11= isOk readArithmeticContents "i<(0+(1+1))" readArithmeticContents = readSequence where@@ -484,25 +487,34 @@ spacing return $ token id op - readVar = do+ readArrayIndex = do id <- getNextId- x <- readVariableName- y <- readArrayIndex <|> return ""- optional spacing- return $ TA_Variable id (x ++ y)+ start <- literal "["+ middle <- readArithmeticContents+ end <- literal "]"+ return $ T_NormalWord id [start, middle, end] - -- Doesn't help with foo[foo]- readArrayIndex = do- char '['- x <- many1 $ noneOf "]"- char ']'- return $ "[" ++ x ++ "]"+ literal s = do+ id <- getNextId+ string s+ return $ T_Literal id s + readArithmeticLiteral =+ readArrayIndex <|> literal "#"+ readExpansion = do id <- getNextId- x <- readNormalDollar <|> readBackTicked+ pieces <- many1 $ choice [+ readArithmeticLiteral,+ readSingleQuoted,+ readDoubleQuoted,+ readNormalDollar,+ readBraced,+ readBackTicked,+ readNormalLiteral "+-*/=%^,]"+ ] spacing- return $ TA_Expansion id x+ return $ TA_Expansion id pieces readGroup = do char '('@@ -511,40 +523,7 @@ spacing return s - readNumber = do- id <- getNextId- num <- many1 $ oneOf "0123456789."- return $ TA_Literal id (num)-- readBased = getArbitrary <|> getHex <|> getOct- where- getThing prefix litchars = try $ do- id <- getNextId- x <- prefix- t <- readExpansion <|> (do- i <- getNextId- stuff <- many1 litchars- return $ TA_Literal i stuff)- return $ TA_Base id x t-- getArbitrary = getThing arbitrary variableChars- getHex = getThing hex hexDigit- getOct = getThing oct digit-- arbitrary = try $ do- b <- many1 digit- s <- char '#'- return (b ++ [s])- hex = try $ do- z <- char '0'- x <- oneOf "xX"- return (z:x:[])- oct = string "0"-- readArithTerm = readBased <|> readArithTermUnit- readArithTermUnit = readGroup <|> readExpansion <|> readQuoted <|> readNumber <|> readVar-- readQuoted = readDoubleQuoted <|> readSingleQuoted+ readArithTerm = readGroup <|> readExpansion readSequence = do spacing@@ -641,7 +620,7 @@ readCondition = called "test expression" $ do opos <- getPosition id <- getNextId- open <- (try $ string "[[") <|> (string "[")+ open <- try (string "[[") <|> string "[" let single = open == "[" condSpacingMsg False $ if single then "You need spaces after the opening [ and before the closing ]."@@ -649,7 +628,7 @@ condition <- readConditionContents single cpos <- getPosition- close <- (try $ string "]]") <|> (string "]")+ close <- try (string "]]") <|> string "]" when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?" when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?" spacing@@ -674,12 +653,12 @@ readAnnotation = called "shellcheck annotation" $ do try readAnnotationPrefix many1 linewhitespace- values <- many1 (readDisable)+ values <- many1 readDisable linefeed many linewhitespace return $ concat values where- readDisable = forKey "disable" $ do+ readDisable = forKey "disable" $ readCode `sepBy` char ',' where readCode = do@@ -718,12 +697,12 @@ return $ T_NormalWord id x checkPossibleTermination pos [T_Literal _ x] =- if x `elem` ["do", "done", "then", "fi", "esac"]- then parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."- else return ()+ when (x `elem` ["do", "done", "then", "fi", "esac"]) $+ parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)." checkPossibleTermination _ _ = return () readNormalWordPart end = do+ notFollowedBy2 $ oneOf end checkForParenthesis choice [ readSingleQuoted,@@ -737,7 +716,7 @@ readLiteralCurlyBraces ] where- checkForParenthesis = do+ checkForParenthesis = return () `attempting` do pos <- getPosition lookAhead $ char '('@@ -806,9 +785,9 @@ optional $ do c <- try . lookAhead $ suspectCharAfterQuotes <|> oneOf "'"- if (not (null string) && isAlpha c && isAlpha (last string))+ if not (null string) && isAlpha c && isAlpha (last string) then- parseProblemAt endPos WarningC 1011 $+ parseProblemAt endPos WarningC 1011 "This apostrophe terminated the single quoted string!" else when ('\n' `elem` string && not ("\n" `isPrefixOf` string)) $@@ -824,7 +803,7 @@ readSingleQuotedPart = readSingleEscaped- <|> (many1 $ noneOf "'\\\x2018\x2019")+ <|> many1 (noneOf "'\\\x2018\x2019") prop_readBackTicked = isOk readBackTicked "`ls *.mp3`" prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"@@ -843,7 +822,7 @@ optional $ do c <- try . lookAhead $ suspectCharAfterQuotes- when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $ do+ when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $ suggestForgotClosingQuote startPos endPos "backtick expansion" -- Result positions may be off due to escapes@@ -858,7 +837,7 @@ disregard (char '`') <|> do pos <- getPosition char '´'- parseProblemAt pos ErrorC 1077 $+ parseProblemAt pos ErrorC 1077 "For command expansion, the tick should slant left (` vs ´)." subParse pos parser input = do@@ -889,7 +868,7 @@ suggestForgotClosingQuote startPos endPos "double quoted string" return $ T_DoubleQuoted id x where- startsWithLineFeed ((T_Literal _ ('\n':_)):_) = True+ startsWithLineFeed (T_Literal _ ('\n':_):_) = True startsWithLineFeed _ = False hasLineFeed (T_Literal _ str) | '\n' `elem` str = True hasLineFeed _ = False@@ -897,7 +876,7 @@ suggestForgotClosingQuote startPos endPos name = do parseProblemAt startPos WarningC 1078 $ "Did you forget to close this " ++ name ++ "?"- parseProblemAt endPos InfoC 1079 $+ parseProblemAt endPos InfoC 1079 "This is actually an end quote, but due to next char it looks suspect." doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked@@ -914,7 +893,7 @@ return $ T_Literal id (concat s) readDoubleLiteralPart = do- x <- many1 $ (readDoubleEscaped <|> (many1 $ noneOf ('\\':doubleQuotableChars)))+ x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars))) return $ concat x readNormalLiteral end = do@@ -937,9 +916,9 @@ readClass = try $ do id <- getNextId char '['- s <- many1 (predefined <|> (liftM return $ letter <|> digit <|> oneOf globchars))+ s <- many1 (predefined <|> liftM return (letter <|> digit <|> oneOf globchars)) char ']'- return $ T_Glob id $ "[" ++ (concat s) ++ "]"+ return $ T_Glob id $ "[" ++ concat s ++ "]" where globchars = "^-_:?*.,!~@#$%=+{}/~" predefined = do@@ -953,20 +932,20 @@ c <- extglobStart <|> char '[' return $ T_Literal id [c] -readNormalLiteralPart end = do- readNormalEscaped <|> (many1 $ noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}"))+readNormalLiteralPart end =+ readNormalEscaped <|> many1 (noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}")) readNormalEscaped = called "escaped char" $ do pos <- getPosition backslash do- next <- (quotable <|> oneOf "?*@!+[]{}.,")+ next <- quotable <|> oneOf "?*@!+[]{}.," return $ if next == '\n' then "" else [next] <|> do next <- anyChar case escapedChar next of- Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use " ++ (alternative next) ++ " instead."+ Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use " ++ alternative next ++ " instead." Nothing -> parseNoteAt pos InfoC 1001 $ "This \\" ++ [next] ++ " will be a regular '" ++ [next] ++ "' in this context." return [next] where@@ -991,7 +970,7 @@ f <- extglobStart char '(' return f- contents <- readExtglobPart `sepBy` (char '|')+ contents <- readExtglobPart `sepBy` char '|' char ')' return $ T_Extglob id [c] contents @@ -1003,7 +982,7 @@ readExtglobGroup = do id <- getNextId char '('- contents <- readExtglobPart `sepBy` (char '|')+ contents <- readExtglobPart `sepBy` char '|' char ')' return $ T_Extglob id "" contents readExtglobLiteral = do@@ -1030,18 +1009,18 @@ readDoubleEscaped = do bs <- backslash (linefeed >> return "")- <|> (doubleQuotable >>= return . return)- <|> (anyChar >>= (return . \x -> [bs, x]))+ <|> liftM return doubleQuotable+ <|> liftM (\ x -> [bs, x]) anyChar readBraceEscaped = do bs <- backslash (linefeed >> return "")- <|> (bracedQuotable >>= return . return)- <|> (anyChar >>= (return . \x -> [bs, x]))+ <|> liftM return bracedQuotable+ <|> liftM (\ x -> [bs, x]) anyChar readGenericLiteral endChars = do- strings <- many (readGenericEscaped <|> (many1 $ noneOf ('\\':endChars)))+ strings <- many (readGenericEscaped <|> many1 (noneOf ('\\':endChars))) return $ concat strings readGenericLiteral1 endExp = do@@ -1059,12 +1038,12 @@ let strip (T_Literal _ s) = return ("\"" ++ s ++ "\"") id <- getNextId char '{'- str <- many1 ((readDoubleQuotedLiteral >>= (strip)) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace))+ str <- many1 ((readDoubleQuotedLiteral >>= strip) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace)) char '}' let result = concat str unless (',' `elem` result || ".." `isInfixOf` result) $ fail "Not a brace expression"- return $ T_BraceExpansion id $ result+ return $ T_BraceExpansion id result readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely@@ -1129,7 +1108,7 @@ try (string "$(") cmds <- readCompoundList char ')' <?> "end of $(..) expression"- return $ (T_DollarExpansion id cmds)+ return $ T_DollarExpansion id cmds prop_readDollarVariable = isOk readDollarVariable "$@" readDollarVariable = do@@ -1176,6 +1155,7 @@ prop_readHereDoc4 = isOk readHereDoc "<< foo\n`\nfoo" prop_readHereDoc5 = isOk readHereDoc "<<- !foo\nbar\n!foo" prop_readHereDoc6 = isOk readHereDoc "<< foo\\ bar\ncow\nfoo bar"+prop_readHereDoc7 = isOk readHereDoc "<< foo\n\\$(f ())\nfoo" readHereDoc = called "here document" $ do fid <- getNextId pos <- getPosition@@ -1189,8 +1169,8 @@ parseProblemAt pos ErrorC 1038 message hid <- getNextId (quoted, endToken) <-- (readDoubleQuotedLiteral >>= return . (\x -> (Quoted, stripLiteral x)))- <|> (readSingleQuotedLiteral >>= return . (\x -> (Quoted, x)))+ liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral+ <|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral <|> (readToken >>= (\x -> return (Unquoted, x))) spacing @@ -1214,7 +1194,7 @@ stripLiteral (T_Literal _ x) = x stripLiteral (T_SingleQuoted _ x) = x - readToken = do+ readToken = liftM concat $ many1 (escaped <|> quoted <|> normal) where quoted = liftM stripLiteral readDoubleQuotedLiteral <|> readSingleQuotedLiteral@@ -1226,16 +1206,16 @@ parseHereData Quoted startPos hereData = do id <- getNextIdAt startPos- return $ [T_Literal id hereData]+ return [T_Literal id hereData] - parseHereData Unquoted startPos hereData = do+ parseHereData Unquoted startPos hereData = subParse startPos readHereData hereData - readHereData = many $ try readNormalDollar <|> try readBackTicked <|> readHereLiteral+ readHereData = many $ try doubleQuotedPart <|> readHereLiteral readHereLiteral = do id <- getNextId- chars <- many1 $ noneOf "`$"+ chars <- many1 $ noneOf "`$\\" return $ T_Literal id chars verifyHereDoc dashed quoted spacing hereInfo = do@@ -1245,17 +1225,17 @@ parseNote ErrorC 1040 "When using <<-, you can only indent with tabs." return () - debugHereDoc pos endToken doc =- if endToken `isInfixOf` doc- then- let lookAt line = when (endToken `isInfixOf` line) $- parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")- in do- parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.")- mapM_ lookAt (lines doc)- else if (map toLower endToken) `isInfixOf` (map toLower doc)- then parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")- else parseProblemAt pos ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")+ debugHereDoc pos endToken doc+ | endToken `isInfixOf` doc =+ let lookAt line = when (endToken `isInfixOf` line) $+ parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")+ in do+ parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.")+ mapM_ lookAt (lines doc)+ | map toLower endToken `isInfixOf` map toLower doc =+ parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")+ | otherwise =+ parseProblemAt pos ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.") readFilename = readNormalWord@@ -1305,9 +1285,9 @@ prop_readSeparator1 = isWarning readScript "a &; b" prop_readSeparator2 = isOk readScript "a & b" readSeparatorOp = do- notFollowedBy2 (g_AND_IF <|> g_DSEMI)+ notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator) notFollowedBy2 (string "&>")- f <- (try $ do+ f <- try (do char '&' spacing pos <- getPosition@@ -1320,7 +1300,7 @@ spacing return f -readSequentialSep = (disregard $ g_Semi >> readLineBreak) <|> (disregard readNewlineList)+readSequentialSep = disregard (g_Semi >> readLineBreak) <|> disregard readNewlineList readSeparator = do separator <- readSeparatorOp@@ -1343,9 +1323,9 @@ in T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args where- assignment (T_Assignment _ _ _ _ _) = True+ assignment (T_Assignment {}) = True assignment _ = False- redirection (T_FdRedirect _ _ _) = True+ redirection (T_FdRedirect {}) = True redirection _ = False @@ -1365,20 +1345,20 @@ case cmd of Nothing -> return $ makeSimpleCommand id1 id2 prefix [] [] Just cmd -> do- suffix <- option [] $- if isModifierCommand cmd- then readModifierSuffix- else if isTimeCommand cmd- then readTimeSuffix- else readCmdSuffix+ suffix <- option [] $ getParser readCmdSuffix cmd [+ (["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),+ (["time"], readTimeSuffix),+ (["let"], readLetSuffix)+ ] return $ makeSimpleCommand id1 id2 prefix [cmd] suffix where- isModifierCommand (T_NormalWord _ [T_Literal _ s]) =- s `elem` ["declare", "export", "local", "readonly", "typeset"]- isModifierCommand _ = False- -- Might not belong in T_SimpleCommand. Fixme?- isTimeCommand (T_NormalWord _ [T_Literal _ "time"]) = True- isTimeCommand _ = False+ isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings+ isCommand _ _ = False+ getParser def cmd [] = def+ getParser def cmd ((list, action):rest) =+ if isCommand list cmd+ then action+ else getParser def cmd rest prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu" prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"@@ -1389,7 +1369,7 @@ (T_Bang id) <- g_Bang pipe <- readPipeSequence return $ T_Banged id pipe- <|> do+ <|> readPipeSequence prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"@@ -1399,7 +1379,7 @@ aid <- getNextId annotations <- readAnnotations - andOr <- withAnnotations annotations $ do+ andOr <- withAnnotations annotations $ chainr1 readPipeline $ do op <- g_AND_IF <|> g_OR_IF readLineBreak@@ -1419,11 +1399,11 @@ do id <- getNextId sep <- readSeparator- more <- (option (T_EOF id) readAndOr)+ more <- option (T_EOF id) readAndOr case more of (T_EOF _) -> return [transformWithSeparator id sep current] _ -> do list <- readTerm' more- return $ (transformWithSeparator id sep current : list)+ return (transformWithSeparator id sep current : list) <|> return [current] @@ -1453,7 +1433,7 @@ spacing return $ T_Pipe id ('|':qualifier) -readCommand = (readCompoundCommand <|> readSimpleCommand)+readCommand = readCompoundCommand <|> readSimpleCommand readCmdName = do f <- readNormalWord@@ -1512,7 +1492,7 @@ readElifPart = called "elif clause" $ do pos <- getPosition correctElif <- elif- when (not correctElif) $+ unless correctElif $ parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'." allspacing condition <- readTerm@@ -1524,7 +1504,7 @@ return (condition, action) where elif = (g_Elif >> return True) <|>- (try $ g_Else >> g_If >> return False)+ try (g_Else >> g_If >> return False) readElsePart = called "else clause" $ do pos <- getPosition@@ -1671,14 +1651,14 @@ readInClause = do g_In- things <- (readCmdWord) `reluctantlyTill`- (disregard (g_Semi) <|> disregard linefeed <|> disregard g_Do)+ things <- readCmdWord `reluctantlyTill`+ (disregard g_Semi <|> disregard linefeed <|> disregard g_Do) do {- lookAhead (g_Do);+ lookAhead g_Do; parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'."; } <|> do {- optional $ g_Semi;+ optional g_Semi; disregard allspacing; } @@ -1687,6 +1667,8 @@ prop_readCaseClause = isOk readCaseClause "case foo in a ) lol; cow;; b|d) fooo; esac" prop_readCaseClause2 = isOk readCaseClause "case foo\n in * ) echo bar;; esac" prop_readCaseClause3 = isOk readCaseClause "case foo\n in * ) echo bar & ;; esac"+prop_readCaseClause4 = isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac"+prop_readCaseClause5 = isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac" readCaseClause = called "case expression" $ do id <- getNextId g_Case@@ -1707,15 +1689,22 @@ pattern <- readPattern g_Rparen readLineBreak- list <- ((lookAhead g_DSEMI >> return []) <|> readCompoundList)- (g_DSEMI <|> lookAhead (readLineBreak >> g_Esac)) `attempting` do+ list <- (lookAhead readCaseSeparator >> return []) <|> readCompoundList+ separator <- readCaseSeparator `attempting` do pos <- getPosition lookAhead g_Rparen parseProblemAt pos ErrorC 1074 "Did you forget the ;; after the previous case item?" readLineBreak- return (pattern, list)+ return (separator, pattern, list) +readCaseSeparator = choice [+ tryToken ";;&" (const ()) >> return CaseContinue,+ tryToken ";&" (const ()) >> return CaseFallThrough,+ g_DSEMI >> return CaseBreak,+ lookAhead (readLineBreak >> g_Esac) >> return CaseBreak+ ]+ prop_readFunctionDefinition = isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }" prop_readFunctionDefinition1 = isOk readFunctionDefinition "foo (){ command foo --lol \"$@\"; }" prop_readFunctionDefinition4 = isWarning readFunctionDefinition "foo(a, b) { true; }"@@ -1726,11 +1715,11 @@ readFunctionDefinition = called "function" $ do functionSignature <- try readFunctionSignature allspacing- (disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition.")+ disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition." group <- readBraceGroup <|> readSubshell return $ functionSignature group where- readFunctionSignature = do+ readFunctionSignature = readWithFunction <|> readWithoutFunction where readWithFunction = do@@ -1770,10 +1759,10 @@ cmd <- choice [ readBraceGroup, readArithmeticExpression, readSubshell, readCondition, readWhileClause, readUntilClause, readIfClause, readForClause, readSelectClause, readCaseClause, readFunctionDefinition] optional spacing redirs <- many readIoRedirect- when (not . null $ redirs) $ optional $ do+ unless (null redirs) $ optional $ do lookAhead $ try (spacing >> needsSeparator) parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."- return $ T_Redirecting id redirs $ cmd+ return $ T_Redirecting id redirs cmd where needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ] @@ -1793,6 +1782,22 @@ lookAhead $ char '-' readCmdWord +-- Fixme: this is a hack that doesn't handle let '++c' or let a\>b+readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)+ where+ readLetExpression = do+ startPos <- getPosition+ expression <- readStringForParser readCmdWord+ subParse startPos readArithmeticContents expression++-- Get whatever a parser would parse as a string+readStringForParser parser = do+ pos <- lookAhead (parser >> getPosition)+ s <- readUntil pos+ return s+ where+ readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))+ prop_readAssignmentWord = isOk readAssignmentWord "a=42" prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)" prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"@@ -1802,7 +1807,8 @@ prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42" prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "-prop_readAssignmentWord0 = isWarning readAssignmentWord "foo$n=42"+prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"+prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" readAssignmentWord = try $ do id <- getNextId pos <- getPosition@@ -1853,10 +1859,25 @@ id <- getNextId char '(' allspacing- words <- (readNormalWord `thenSkip` allspacing) `reluctantlyTill` (char ')')+ words <- readElement `reluctantlyTill` char ')' char ')' return $ T_Array id words+ where+ readElement = (readIndexed <|> readRegular) `thenSkip` allspacing+ readIndexed = do+ id <- getNextId+ index <- try $ do+ x <- readArrayIndex+ char '='+ return x+ value <- readNormalWord <|> nothing+ return $ T_IndexedElement id index value+ readRegular = readNormalWord + nothing = do+ id <- getNextId+ return $ T_Literal id ""+ tryToken s t = try $ do id <- getNextId string s@@ -1876,14 +1897,14 @@ optional (do try . lookAhead $ char '[' parseProblem ErrorC 1069 "You need a space before the [.")- try $ lookAhead (keywordSeparator)+ 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+anycaseString =+ mapM anycaseChar where anycaseChar c = char (toLower c) <|> char (toUpper c) @@ -1930,11 +1951,11 @@ tryToken ";" T_Semi keywordSeparator =- eof <|> disregard whitespace <|> (disregard $ oneOf ";()[<>&|")+ eof <|> disregard whitespace <|> disregard (oneOf ";()[<>&|") readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ] -ifParse p t f = do+ifParse p t f = (lookAhead (try p) >> t) <|> f readShebang = do@@ -1953,24 +1974,24 @@ pos <- getPosition optional $ do readUtf8Bom- parseProblem ErrorC 1082 $+ parseProblem ErrorC 1082 "This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ." sb <- option "" readShebang verifyShell pos (getShell sb)- if (isValidShell $ getShell sb) /= Just False+ if isValidShell (getShell sb) /= Just False then do { allspacing; commands <- readTerm;- eof <|> (parseProblem ErrorC 1070 "Parsing stopped here because of parsing errors.");+ eof <|> parseProblem ErrorC 1070 "Parsing stopped here because of parsing errors."; 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 [T_EOF id]; } else do many anyChar- return $ T_Script id sb $ [T_EOF id];+ return $ T_Script id sb [T_EOF id]; where basename s = reverse . takeWhile (/= '/') . reverse $ s@@ -2018,8 +2039,8 @@ rp p filename contents = Ms.runState (runParserT p initialState filename contents) ([], []) -isWarning p s = (fst cs) && (not . null . snd $ cs) where cs = checkString p s-isOk p s = (fst cs) && (null . snd $ cs) where cs = checkString p s+isWarning p s = fst cs && (not . null . snd $ cs) where cs = checkString p s+isOk p s = fst cs && (null . snd $ cs) where cs = checkString p s checkString parser string = case rp (parser >> eof >> getState) "-" string of@@ -2043,7 +2064,7 @@ getStringFromParsec errors = case map snd $ sortWith fst $ map f errors of- r -> (intercalate " " $ take 1 $ nub r) ++ " Fix any mentioned problems and try again."+ r -> unwords (take 1 $ nub r) ++ " Fix any mentioned problems and try again." where f err = case err of UnExpect s -> (1, unexpected s)@@ -2052,15 +2073,15 @@ Message s -> (4, s ++ ".") wut "" = "eof" wut x = x- unexpected s = "Unexpected " ++ (wut s) ++ "."+ unexpected s = "Unexpected " ++ wut s ++ "." -parseShell filename contents = do+parseShell filename contents = case rp (parseWithNotes readScript) filename contents of (Right (script, map, notes), (parsenotes, _)) -> ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes) (Left err, (p, context)) -> ParseResult Nothing- (nub $ sortNotes $ p ++ (notesForContext context) ++ ([makeErrorFor err]))+ (nub $ sortNotes $ p ++ notesForContext context ++ [makeErrorFor err]) where isName (ContextName _ _) = True isName _ = False@@ -2071,4 +2092,8 @@ "The mentioned parser error was in this " ++ str ++ "." lt x = trace (show x) x-ltt t x = trace (show t) x+ltt t = trace (show t)++return []+runTests = $quickCheckAll+
ShellCheck/Simple.hs view
@@ -15,35 +15,20 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. -}-module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage) where+{-# LANGUAGE TemplateHaskell #-}+module ShellCheck.Simple (shellCheck, ShellCheckComment, scLine, scColumn, scSeverity, scCode, scMessage, runTests) where -import ShellCheck.Parser-import ShellCheck.Analytics+import ShellCheck.Parser hiding (runTests)+import ShellCheck.Analytics hiding (runTests) import Data.Maybe import Text.Parsec.Pos import Data.List---prop_findsParseIssue =- let comments = shellCheck "echo \"$12\"" [] in- (length comments) == 1 && (scCode $ head comments) == 1037-prop_commentDisablesParseIssue1 =- null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []-prop_commentDisablesParseIssue2 =- null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []--prop_findsAnalysisIssue =- let comments = shellCheck "echo $1" [] in- (length comments) == 1 && (scCode $ head comments) == 2086-prop_commentDisablesAnalysisIssue1 =- null $ shellCheck "#shellcheck disable=SC2086\necho $1" []-prop_commentDisablesAnalysisIssue2 =- null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []+import Test.QuickCheck.All (quickCheckAll) shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment] shellCheck script options = let (ParseResult result notes) = parseShell "-" script in- let allNotes = notes ++ (concat $ maybeToList $ do+ let allNotes = notes ++ concat (maybeToList $ do (tree, posMap) <- result let list = runAnalytics options tree return $ map (noteToParseNote posMap) $ filterByAnnotation tree list@@ -65,3 +50,23 @@ formatNote (ParseNote pos severity code text) = ShellCheckComment (sourceLine pos) (sourceColumn pos) (severityToString severity) (fromIntegral code) text++prop_findsParseIssue =+ let comments = shellCheck "echo \"$12\"" [] in+ length comments == 1 && scCode (head comments) == 1037+prop_commentDisablesParseIssue1 =+ null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" []+prop_commentDisablesParseIssue2 =+ null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" []++prop_findsAnalysisIssue =+ let comments = shellCheck "echo $1" [] in+ length comments == 1 && scCode (head comments) == 2086+prop_commentDisablesAnalysisIssue1 =+ null $ shellCheck "#shellcheck disable=SC2086\necho $1" []+prop_commentDisablesAnalysisIssue2 =+ null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" []++return []+runTests = $quickCheckAll+
+ shellcheck.1.md view
@@ -0,0 +1,121 @@+% SHELLCHECK(1) Shell script analysis tool++# NAME++shellcheck - Shell script analysis tool++# SYNOPSIS++**shellcheck** [*OPTIONS*...] *FILES*...++# DESCRIPTION++ShellCheck is a static analysis and linting tool for sh/bash scripts. It's+mainly focused on handling typical beginner and intermediate level syntax+errors and pitfalls where the shell just gives a cryptic error message or+strange behavior, but it also reports on a few more advanced issues where+corner cases can cause delayed failures.++# 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.++**-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.++# FORMATS++**tty**++: Plain text, human readable output. This is the default.++**gcc**++: GCC compatible output. Useful for editors that support compiling and+ showing syntax errors.++ For example, in Vim, `:set makeprg=shellcheck\ -f\ gcc\ %` will allow+ using `:make` to check the script, and `:cnext` to jump to the next error.++ <file>:<line>:<column>: <type>: <message>++**checkstyle**++: Checkstyle compatible XML output. Supported directly or through plugins+ by many IDEs and build monitoring systems.++ <?xml version='1.0' encoding='UTF-8'?>+ <checkstyle version='4.3'>+ <file name='file'>+ <error+ line='line'+ column='column'+ severity='severity'+ message='message'+ source='ShellCheck.SC####' />+ ...+ </file>+ ...+ </checkstyle>++**json**++: Json is a popular serialization format that is more suitable for web+ applications. ShellCheck's json is compact and contains only the bare+ minimum.++ [+ {+ "line": line,+ "column": column,+ "level": level,+ "code": ####,+ "message": message+ },+ ...+ ]++# DIRECTIVES+ShellCheck directives can be specified as comments in the shell script+before a command or block:++ # shellcheck key=value key=value+ command-or-structure++For example, to suppress SC2035 about using `./*.jpg`:++ # shellcheck disable=SC2035+ echo "Files: " *.jpg++Valid keys are:++**disable**+: Disables a comma separated list of error codes for the following command.+ The command can be a simple command like `echo foo`, or a compound command+ like a function definition, subshell block or loop.+++# AUTHOR+ShellCheck is written and maintained by Vidar Holen.++# REPORTING BUGS+Bugs and issues can be reported on GitHub:++https://github.com/koalaman/shellcheck/issues++# SEE ALSO++sh(1) bash(1)
+ test/shellcheck.hs view
@@ -0,0 +1,16 @@+module Main where++import Control.Monad+import System.Exit+import qualified ShellCheck.Simple+import qualified ShellCheck.Analytics+import qualified ShellCheck.Parser++main = do+ putStrLn "Running ShellCheck tests..."+ results <- sequence [ShellCheck.Simple.runTests,+ ShellCheck.Analytics.runTests,+ ShellCheck.Parser.runTests]+ if and results then exitSuccess+ else exitFailure+