ShellCheck 0.3.2 → 0.3.3
raw patch · 5 files changed
+510/−87 lines, 5 files
Files
- ShellCheck.cabal +1/−1
- ShellCheck/AST.hs +4/−3
- ShellCheck/Analytics.hs +415/−65
- ShellCheck/Data.hs +9/−1
- ShellCheck/Parser.hs +81/−17
ShellCheck.cabal view
@@ -1,6 +1,6 @@ Name: ShellCheck -- Must also be updated in ShellCheck/Data.hs :-Version: 0.3.2+Version: 0.3.3 Synopsis: Shell script analysis tool License: OtherLicense License-file: LICENSE
ShellCheck/AST.hs view
@@ -28,6 +28,7 @@ data AssignmentMode = Assign | Append deriving (Show, Eq) data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq) data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)+data ForInType = NormalForIn | ShortForIn deriving (Show, Eq) data Token = TA_Base Id String Token@@ -81,7 +82,7 @@ | T_Fi Id | T_For Id | T_ForArithmetic Id Token Token Token [Token]- | T_ForIn Id String [Token] [Token]+ | T_ForIn Id ForInType [String] [Token] [Token] | T_Function Id FunctionKeyword FunctionParentheses String Token | T_GREATAND Id | T_Glob Id String@@ -202,7 +203,7 @@ delve (T_BraceGroup id l) = dl l $ T_BraceGroup id delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id- delve (T_ForIn id v w l) = dll w l $ T_ForIn id v+ delve (T_ForIn id t v w l) = dll w l $ T_ForIn id t v delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v delve (T_CaseExpression id word cases) = do newWord <- round word@@ -308,7 +309,7 @@ T_BraceGroup id _ -> id T_WhileExpression id _ _ -> id T_UntilExpression id _ _ -> id- T_ForIn id _ _ _ -> id+ T_ForIn id _ _ _ _ -> id T_SelectIn id _ _ _ -> id T_CaseExpression id _ _ -> id T_Function id _ _ _ _ -> id
ShellCheck/Analytics.hs view
@@ -55,6 +55,7 @@ ,checkFunctionsUsedExternally ,checkUnusedAssignments ,checkUnpassedInFunctions+ ,checkArrayWithoutIndex ] checksFor Sh = [@@ -120,6 +121,8 @@ -- Checks that are run on each node in the AST runNodeAnalysis f p t = execWriter (doAnalysis (f p) t)++nodeChecks :: [Parameters -> Token -> Writer [Note] ()] nodeChecks = [ checkUuoc ,checkPipePitfalls@@ -189,6 +192,15 @@ ,checkInteractiveSu ,checkStderrPipe ,checkSetAssignment+ ,checkOverridingPath+ ,checkArrayAsString+ ,checkUnsupported+ ,checkMultipleAppends+ ,checkAliasesExpandEarly+ ,checkSuspiciousIFS+ ,checkAliasesUsesArgs+ ,checkShouldUseGrepQ+ ,checkTestGlobs ] @@ -224,6 +236,8 @@ isVariableName (x:r) = isVariableStartChar x && all isVariableChar r isVariableName _ = False +potentially = fromMaybe (return ())+ matchAll re = unfoldr f where f str = do@@ -252,9 +266,17 @@ isConfusedGlobRegex [x,'*'] | x /= '\\' = True isConfusedGlobRegex _ = False -isPotentiallyConfusedGlobRegex =- let re = mkRegex "[a-z1-9]\\*" in- isJust . matchRegex re+getSuspiciousRegexWildcard str =+ if (not $ str `matches` contra)+ then do+ match <- matchRegex suspicious str+ str <- match !!! 0+ str !!! 0+ else+ fail "looks good"+ where+ suspicious = mkRegex "([A-Za-z1-9])\\*"+ contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]" matches string regex = isJust $ matchRegex regex string @@ -315,9 +337,16 @@ [] -> Nothing (r:_) -> Just r +verify :: (Parameters -> Token -> Writer [a] ()) -> String -> Bool verify f s = checkNode f s == Just True++verifyNot :: (Parameters -> Token -> Writer [a] ()) -> String -> Bool verifyNot f s = checkNode f s == Just False++verifyTree :: (Parameters -> Token -> [a]) -> String -> Bool verifyTree f s = checkTree f s == Just True++verifyNotTree :: (Parameters -> Token -> [a]) -> String -> Bool verifyNotTree f s = checkTree f s == Just False checkNode f = checkTree (runNodeAnalysis f)@@ -439,9 +468,14 @@ prop_checkNeedlessCommands = verify checkNeedlessCommands "foo=$(expr 3 + 2)" prop_checkNeedlessCommands2 = verify checkNeedlessCommands "foo=`echo \\`expr 3 + 2\\``" prop_checkNeedlessCommands3 = verifyNot checkNeedlessCommands "foo=$(expr foo : regex)"-checkNeedlessCommands _ cmd@(T_SimpleCommand id _ _) |- cmd `isCommand` "expr" && (not $ ":" `elem` deadSimple cmd) =+prop_checkNeedlessCommands4 = verifyNot checkNeedlessCommands "foo=$(expr foo \\< regex)"+checkNeedlessCommands _ cmd@(T_SimpleCommand id _ args) |+ cmd `isCommand` "expr" && (not $ any (`elem` words) exceptions) = style id 2003 "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."+ where+ -- These operators are hard to replicate in POSIX+ exceptions = [ ":", "<", ">", "<=", ">=" ]+ words = mapMaybe getLiteralString args checkNeedlessCommands _ _ = return () prop_checkPipePitfalls3 = verify checkPipePitfalls "ls | grep -v mp3"@@ -460,6 +494,9 @@ for' ["ps", "grep"] $ \x -> info x 2009 "Consider using pgrep instead of grepping ps output." + for' ["grep", "wc"] $+ \x -> style x 2126 "Consider using grep -c instead of grep|wc."+ didLs <- liftM or . sequence $ [ for' ["ls", "grep"] $ \x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.",@@ -498,11 +535,38 @@ bracedString l = concat $ deadSimple l-isMagicInQuotes (T_DollarBraced _ l) =++isArrayExpansion (T_DollarBraced _ l) = let string = bracedString l in- '@' `elem` string || "!" `isPrefixOf` string-isMagicInQuotes _ = False+ "@" `isPrefixOf` string ||+ not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string+isArrayExpansion _ = False +-- Is it certain that this arg will becomes multiple args?+willBecomeMultipleArgs t = willConcatInAssignment t || f t+ where+ f (T_Extglob {}) = True+ f (T_Glob {}) = True+ f (T_BraceExpansion {}) = True+ f (T_DoubleQuoted _ parts) = any f parts+ f (T_NormalWord _ parts) = any f parts+ f _ = False++willConcatInAssignment t@(T_DollarBraced {}) = isArrayExpansion t+willConcatInAssignment (T_DoubleQuoted _ parts) = any willConcatInAssignment parts+willConcatInAssignment (T_NormalWord _ parts) = any willConcatInAssignment parts+willConcatInAssignment _ = False++-- Is it possible that this arg becomes multiple args?+mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t+ where+ f (T_DollarBraced _ l) =+ let string = bracedString l in+ "!" `isPrefixOf` string+ f (T_DoubleQuoted _ parts) = any f parts+ f (T_NormalWord _ parts) = any f parts+ f _ = False+ prop_checkShebang1 = verifyTree checkShebang "#!/usr/bin/env bash -x\necho cow" prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l " checkShebang _ (T_Script id sb _) =@@ -598,17 +662,17 @@ prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done" prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done" prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done"-checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =- when (any (\x -> willSplit x && not (isMagicInQuotes x)) list+checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =+ when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list || (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $ err id 2066 $ "Since you double quoted this, it will not word split, and the loop will only run once."-checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id s]] _) =+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]] _) =+checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [T_Literal id s]] _) = if ',' `elem` s then unless ('{' `elem` s) $ warn id 2042 $ "Use spaces, not commas, to separate loop elements."- else warn id 2043 $ "This loop will only run once, with " ++ f ++ "='" ++ s ++ "'."+ 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"@@ -616,7 +680,7 @@ prop_checkForInCat2 = verify checkForInCat "for f in $(cat foo | grep lol); do stuff; done" prop_checkForInCat2a= verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done" prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done"-checkForInCat _ (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w+checkForInCat _ (T_ForIn _ _ f [T_NormalWord _ w] _) = mapM_ checkF w where checkF (T_DollarExpansion id [T_Pipeline _ _ r]) | all isLineBased r =@@ -632,17 +696,17 @@ prop_checkForInLs3 = verify checkForInLs "for f in `find / -name '*.mp3'`; do mplayer \"$f\"; done" checkForInLs _ t = try t where- try (T_ForIn _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) =+ try (T_ForIn _ _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) = check id f x- try (T_ForIn _ f [T_NormalWord _ [T_Backticked id [x]]] _) =+ try (T_ForIn _ _ f [T_NormalWord _ [T_Backticked id [x]]] _) = check id f x try _ = return () check id f x = case deadSimple x of ("ls":n) -> let warntype = if any ("-" `isPrefixOf`) n then warn else err in- warntype id 2045 $ "Iterate over globs whenever possible (e.g. 'for f in */*.wav'), as for loops over ls will fail for filenames like 'my file*.txt'."- ("find":_) -> warn id 2044 $ "Use find -exec or a while read loop instead, as for loops over find will fail for filenames like 'my file*.txt'."+ warntype id 2045 "Iterating over ls output is fragile. Use globs."+ ("find":_) -> warn id 2044 "For loops over find output are fragile. Use find -exec or a while read loop." _ -> return () @@ -754,14 +818,20 @@ where isOk [t] = isAssignment t || (fromMaybe False $ do name <- getCommandBasename t- return $ name `elem` ["echo", "exit"])+ return $ name `elem` ["echo", "exit", "return"]) isOk _ = False checkShorthandIf _ _ = return () prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done"-checkDollarStar _ (T_NormalWord _ [(T_DollarBraced id l)]) | (bracedString l) == "*" =- warn id 2048 $ "Use \"$@\" (with quotes) to prevent whitespace problems."+prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"+checkDollarStar p t@(T_NormalWord _ [(T_DollarBraced id l)])+ | (bracedString l) == "*" =+ unless isAssigned $+ warn id 2048 $ "Use \"$@\" (with quotes) to prevent whitespace problems."+ where+ path = getPath (parentMap p) t+ isAssigned = any isAssignment . take 2 $ path checkDollarStar _ _ = return () @@ -771,14 +841,56 @@ prop_checkUnquotedDollarAt3 = verifyNot checkUnquotedDollarAt "ls ${#foo[@]}" prop_checkUnquotedDollarAt4 = verifyNot checkUnquotedDollarAt "ls \"$@\"" prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }"-checkUnquotedDollarAt _ (T_NormalWord _ [T_DollarBraced id l]) =- let string = bracedString l- failing = err id 2068 $ "Add double quotes around ${" ++ string ++ "}, otherwise it's just like $* and breaks on spaces."- in do- when ("@" `isPrefixOf` string) failing- when (not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string) failing+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 $+ "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_checkArrayAsString1 = verify checkArrayAsString "a=$@"+prop_checkArrayAsString2 = verify checkArrayAsString "a=\"${arr[@]}\""+prop_checkArrayAsString3 = verify checkArrayAsString "a=*.png"+prop_checkArrayAsString4 = verify checkArrayAsString "a={1..10}"+prop_checkArrayAsString5 = verifyNot checkArrayAsString "a='*.gif'"+prop_checkArrayAsString6 = verifyNot checkArrayAsString "a=$*"+prop_checkArrayAsString7 = verifyNot checkArrayAsString "a=( $@ )"+checkArrayAsString _ (T_Assignment id _ _ _ word) =+ if willConcatInAssignment word+ then+ warn (getId word) 2124+ "Assigning an array to a string! Assign as array, or use * instead of @ to concatenate."+ else+ when (willBecomeMultipleArgs word) $+ warn (getId word) 2125+ "Brace expansions and globs are literal in assignments. Quote it or use an array."+checkArrayAsString _ _ = return ()++prop_checkArrayWithoutIndex1 = verifyTree checkArrayWithoutIndex "foo=(a b); echo $foo"+prop_checkArrayWithoutIndex2 = verifyNotTree checkArrayWithoutIndex "foo='bar baz'; foo=($foo); echo ${foo[0]}"+checkArrayWithoutIndex params _ =+ concat $ doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)+ where+ readF _ (T_DollarBraced id token) _ = do+ map <- get+ 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.")]+ readF _ _ _ = return []++ writeF _ t name (DataFrom [T_Array {}]) = do+ modify (Map.insert name t)+ return []+ writeF _ _ name _ = do+ modify (Map.delete name)+ return []+ prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow" prop_checkStderrRedirect2 = verifyNot checkStderrRedirect "test > cow 2>&1" checkStderrRedirect _ (T_Redirecting _ [@@ -805,6 +917,7 @@ prop_checkSingleQuotedVariables5 = verifyNot checkSingleQuotedVariables "trap 'echo $SECONDS' EXIT" prop_checkSingleQuotedVariables6 = verifyNot checkSingleQuotedVariables "sed -n '$p'" prop_checkSingleQuotedVariables6a= verify checkSingleQuotedVariables "sed -n '$pattern'"+prop_checkSingleQuotedVariables7 = verifyNot checkSingleQuotedVariables "PS1='$PWD \\$ '" checkSingleQuotedVariables params t@(T_SingleQuoted id s) = when (s `matches` re) $ if "sed" == commandName@@ -819,19 +932,29 @@ name <- getCommandBasename cmd return name - isProbablyOk = commandName `elem` [+ isProbablyOk =+ (any isOkAssignment $ take 3 $ getPath parents t)+ || commandName `elem` [ "trap" ,"sh" ,"bash" ,"ksh" ,"zsh" ,"ssh"+ ,"xprop"+ ,"alias" ] || "awk" `isSuffixOf` commandName || "perl" `isPrefixOf` commandName + commonlyQuoted = ["PS1", "PS2", "PS3", "PS4", "PROMPT_COMMAND"]+ isOkAssignment t =+ case t of+ T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted+ otherwise -> False+ re = mkRegex "\\$[{(0-9a-zA-Z_]"- sedContra = mkRegex "\\$[dp]($|[^a-zA-Z])"+ sedContra = mkRegex "\\$[dpsaic]($|[^a-zA-Z])" checkSingleQuotedVariables _ _ = return () @@ -852,8 +975,11 @@ prop_checkNumberComparisons8 = verify checkNumberComparisons "[[ foo <= bar ]]" prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]" prop_checkNumberComparisons10= verify checkNumberComparisons "#!/bin/zsh -x\n[ foo >= bar ]]"+prop_checkNumberComparisons11= verify checkNumberComparisons "[[ $foo -eq 'N' ]]"+prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]" checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do- if (isNum lhs || isNum rhs)+ if (isNum lhs && (not $ isNonNum rhs)+ || isNum rhs && (not $ isNonNum lhs)) then do when (isLtGt op) $ err id 2071 $@@ -871,6 +997,7 @@ when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do mapM_ checkDecimals [lhs, rhs]+ checkStrings [lhs, rhs] where isLtGt = flip elem ["<", "\\<", ">", "\\>"]@@ -885,6 +1012,17 @@ decimalError = "Decimals are not supported. " ++ "Either use integers only, or use bc or awk to compare." + checkStrings hs =+ mapM_ stringError . take 1 . filter isNonNum $ hs++ isNonNum t = fromMaybe False $ do+ s <- getLiteralStringExt (const $ return "") t+ return . not . all numChar $ s+ numChar x = isDigit x || x `elem` "+-. "++ stringError t = err (getId t) 2130 $+ op ++ " is for integer comparisons. Use " ++ (seqv op) ++ " instead."+ isNum t = case deadSimple t of [v] -> all isDigit v@@ -901,6 +1039,15 @@ eqv ">=" = "-ge" eqv _ = "the numerical equivalent" + esc = if typ == SingleBracket then "\\" else ""+ seqv "-ge" = "! a " ++ esc ++ "< b"+ seqv "-gt" = esc ++ ">"+ seqv "-le" = "! a " ++ esc ++ "> b"+ seqv "-lt" = esc ++ "<"+ seqv "-eq" = "="+ seqv "-ne" = "!="+ seqv _ = "the string equivalent"+ invert ('\\':s) = invert s invert "<=" = ">" invert ">=" = "<"@@ -994,9 +1141,10 @@ err id 2078 $ "This expression is constant. Did you forget a $ somewhere?" checkConstantNoary _ _ = return () -prop_checkBraceExpansionVars = verify checkBraceExpansionVars "echo {1..$n}"-checkBraceExpansionVars _ (T_BraceExpansion id s) | '$' `elem` s =- warn id 2051 $ "Bash doesn't support variables in brace expansions."+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." checkBraceExpansionVars _ _ = return () prop_checkForDecimals = verify checkForDecimals "((3.14*c))"@@ -1133,7 +1281,9 @@ T_CaseExpression _ _ _ -> return True T_HereDoc _ _ _ _ _ -> return True T_DollarBraced {} -> return True- T_ForIn _ _ _ _ -> return True -- Pragmatically assume it's desirable here+ -- Pragmatically assume it's desirable to split here+ T_ForIn {} -> return True+ T_SelectIn {} -> return True _ -> Nothing isParamTo tree cmd t =@@ -1204,6 +1354,13 @@ 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]+ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ("/" ++ str) `isSuffixOf` cmd) isUnqualifiedCommand token str = isCommandMatch token (\cmd -> cmd == str) @@ -1224,6 +1381,7 @@ isAssignment (T_Annotation _ _ w) = isAssignment w isAssignment (T_Redirecting _ _ w) = isAssignment w isAssignment (T_SimpleCommand _ (w:_) []) = True+isAssignment (T_Assignment {}) = True isAssignment _ = False prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""@@ -1338,24 +1496,31 @@ prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file" prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg" prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"+prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"+prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo" checkGrepRe _ = checkCommand "grep" (const f) where -- --regex=*(extglob) doesn't work. Fixme? skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s skippable _ = False f [] = return ()- f (x:r) | skippable (getLiteralString x) = f r+ 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." let string = concat $ deadSimple re if isConfusedGlobRegex string then warn (getId re) 2063 $ "Grep uses regex, but this looks like a glob."- else- if (isPotentiallyConfusedGlobRegex string)- then info (getId re) 2022 "Note that c* does not mean \"c followed by anything\" in regex."- else return ()+ else potentially $ do+ char <- getSuspiciousRegexWildcard string+ return $ info (getId re) 2022 $+ "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ (wordStartingWith char) ++ "'." +wordStartingWith c =+ head . filter ([c] `isPrefixOf`) $ candidates+ where+ candidates =+ sampleWords ++ (map (\(x:r) -> (toUpper x) : r) sampleWords) ++ [c:"test"] prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"@@ -1473,6 +1638,8 @@ prop_checkInexplicablyUnquoted1 = verify checkInexplicablyUnquoted "echo 'var='value';'" prop_checkInexplicablyUnquoted2 = verifyNot checkInexplicablyUnquoted "'foo'*" prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --user-agent='something'"+prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\""+prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\"" checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens) where check ((T_SingleQuoted _ _):(T_Literal id str):_)@@ -1481,13 +1648,16 @@ check ((T_DoubleQuoted _ _):trapped:(T_DoubleQuoted _ _):_) = case trapped of- T_DollarExpansion id _ -> warnAbout id- T_DollarBraced id _ -> warnAbout id+ T_DollarExpansion id _ -> warnAboutExpansion id+ T_DollarBraced id _ -> warnAboutExpansion id+ T_Literal id s -> unless (s == "/") $ warnAboutLiteral id _ -> return () check _ = return ()- warnAbout id =- info id 2027 $ "Surrounding quotes actually unquotes this (\"inside\"$outside\"inside\"). Did you forget your quote level?"+ warnAboutExpansion id =+ 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." checkInexplicablyUnquoted _ _ = return () prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""@@ -1526,7 +1696,7 @@ doLists (T_BraceGroup _ cmds) = doList cmds doLists (T_WhileExpression _ _ cmds) = doList cmds doLists (T_UntilExpression _ _ cmds) = doList cmds- doLists (T_ForIn _ _ _ cmds) = doList cmds+ doLists (T_ForIn _ _ _ _ cmds) = doList cmds doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds doLists (T_IfExpression _ thens elses) = do mapM_ (\(_, l) -> doList l) thens@@ -1576,11 +1746,14 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'" prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"+prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'" checkUnusedEchoEscapes _ = checkCommand "echo" (const f) where isDashE = mkRegex "^-.*e" hasEscapes = mkRegex "\\\\[rnt]"- f (arg:_) | (concat $ deadSimple arg) `matches` isDashE = return ()+ f args | (concat $ concatMap deadSimple allButLast) `matches` isDashE =+ return ()+ where allButLast = reverse . drop 1 . reverse $ args f args = mapM_ checkEscapes args checkEscapes (T_NormalWord _ args) =@@ -1723,7 +1896,7 @@ else [] --Points to 'for' rather than variable- T_ForIn id str words _ -> [(t, t, str, DataFrom words)]+ T_ForIn id _ strs words _ -> map (\str -> (t, t, str, DataFrom words)) strs T_SelectIn id str words _ -> [(t, t, str, DataFrom words)] _ -> [] @@ -1745,7 +1918,9 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ ((T_NormalWord _ ((T_Literal _ x):_)):rest)) = filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $ case x of- "read" -> concatMap getLiteral rest+ "read" ->+ let params = map getLiteral rest in+ catMaybes . takeWhile isJust . reverse $ params "let" -> concatMap letParamToLiteral rest "export" -> concatMap getModifierParam rest@@ -1763,13 +1938,10 @@ (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]) stripEqualsFrom t = t - getLiteral t@(T_NormalWord _ [T_Literal _ s]) =- [(base, t, s, DataExternal)]- getLiteral t@(T_NormalWord _ [T_SingleQuoted _ s]) =- [(base, t, s, DataExternal)]- getLiteral t@(T_NormalWord _ [T_DoubleQuoted _ [T_Literal id s]]) =- [(base, t, s, DataExternal)]- getLiteral x = []+ getLiteral t = do+ s <- getLiteralString t+ when ("-" `isPrefixOf` s) $ fail "argument"+ return (base, t, s, DataExternal) getModifierParam t@(T_Assignment _ _ name _ value) = [(base, t, name, DataFrom [value])]@@ -1817,8 +1989,8 @@ if assignFirst t then return () else setWritten t when (scopeType /= NoneScope) $ modify ((StackScopeEnd):) - assignFirst (T_ForIn _ _ _ _) = True- assignFirst (T_SelectIn _ _ _ _) = True+ assignFirst (T_ForIn {}) = True+ assignFirst (T_SelectIn {}) = True assignFirst _ = False setRead t =@@ -1879,6 +2051,7 @@ prop_checkSpacefulnessI = verifyNotTree checkSpacefulness "$1 --flags" prop_checkSpacefulnessJ = verifyTree checkSpacefulness "echo $PWD" prop_checkSpacefulnessK = verifyNotTree checkSpacefulness "n+='foo bar'"+prop_checkSpacefulnessL = verifyNotTree checkSpacefulness "select foo in $bar; do true; done" checkSpacefulness params t = doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)@@ -2056,6 +2229,7 @@ prop_checkUnused14= verifyNotTree checkUnusedAssignments "x=(1); n=0; echo ${x[n]}" prop_checkUnused15= verifyNotTree checkUnusedAssignments "x=(1); n=0; (( x[n] ))" prop_checkUnused16= verifyNotTree checkUnusedAssignments "foo=5; declare -x foo"+prop_checkUnused17= verifyNotTree checkUnusedAssignments "read -i 'foo' -e -p 'Input: ' bar; $bar;" checkUnusedAssignments params t = snd $ runWriter (mapM_ checkAssignment flow) where flow = variableFlow params@@ -2103,6 +2277,7 @@ prop_checkWhileReadPitfalls4 = verifyNot checkWhileReadPitfalls "while read foo; do ssh $foo hostname < /dev/null; done" prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done" prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo"+prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file" checkWhileReadPitfalls _ (T_WhileExpression id [command] contents) | isStdinReadCommand command = do@@ -2117,13 +2292,19 @@ && all (not . stdinRedirect) redirs isStdinReadCommand _ = False - checkMuncher (T_Pipeline _ _ ((T_Redirecting _ redirs cmd):_)) = do- let name = fromMaybe "" $ getCommandBasename cmd- when ((not . any stdinRedirect $ redirs) && (name `elem` munchers)) $ do- info id 2095 $- name ++ " may swallow stdin, preventing this loop from working properly."- warn (getId cmd) 2095 $- "Add < /dev/null to prevent " ++ name ++ " from swallowing stdin."+ checkMuncher (T_Pipeline _ _ ((T_Redirecting _ redirs cmd):_)) | not $ any stdinRedirect redirs = do+ case cmd of+ (T_IfExpression _ thens elses) ->+ mapM_ checkMuncher . concat $ (map fst thens) ++ (map snd thens) ++ [elses]++ _ -> potentially $ do+ name <- getCommandBasename cmd+ guard $ name `elem` munchers+ return $ do+ info id 2095 $+ name ++ " may swallow stdin, preventing this loop from working properly."+ warn (getId cmd) 2095 $+ "Add < /dev/null to prevent " ++ name ++ " from swallowing stdin." checkMuncher _ = return () stdinRedirect (T_FdRedirect _ fd _)@@ -2158,7 +2339,9 @@ prop_checkCharRangeGlob2 = verifyNot checkCharRangeGlob "ls *[[:digit:]].jpg" prop_checkCharRangeGlob3 = verify checkCharRangeGlob "ls [10-15]" prop_checkCharRangeGlob4 = verifyNot checkCharRangeGlob "ls [a-zA-Z]"-checkCharRangeGlob _ (T_Glob id str) | isCharClass str =+prop_checkCharRangeGlob5 = verifyNot checkCharRangeGlob "tr -d [a-zA-Z]" -- tr has 2060+checkCharRangeGlob p t@(T_Glob id str) |+ isCharClass str && not (isParamTo (parentMap p) "tr" t) = if ":" `isPrefixOf` contents && ":" `isSuffixOf` contents && contents /= ":"@@ -2181,7 +2364,7 @@ checkCdAndBack params = doLists where shell = shellType params- doLists (T_ForIn _ _ _ cmds) = doList cmds+ doLists (T_ForIn _ _ _ _ cmds) = doList cmds doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds doLists (T_WhileExpression _ _ cmds) = doList cmds doLists (T_UntilExpression _ _ cmds) = doList cmds@@ -2465,3 +2648,170 @@ literal (T_NormalWord _ l) = concatMap literal l literal (T_Literal _ str) = str literal _ = "*"+++prop_checkOverridingPath1 = verify checkOverridingPath "PATH=\"$var/$foo\""+prop_checkOverridingPath2 = verify checkOverridingPath "PATH=\"mydir\""+prop_checkOverridingPath3 = verify checkOverridingPath "PATH=/cow/foo"+prop_checkOverridingPath4 = verifyNot checkOverridingPath "PATH=/cow/foo/bin"+prop_checkOverridingPath5 = verifyNot checkOverridingPath "PATH='/bin:/sbin'"+prop_checkOverridingPath6 = verifyNot checkOverridingPath "PATH=\"$var/$foo\" cmd"+prop_checkOverridingPath7 = verifyNot checkOverridingPath "PATH=$OLDPATH"+prop_checkOverridingPath8 = verifyNot checkOverridingPath "PATH=$PATH:/stuff"+checkOverridingPath _ (T_SimpleCommand _ vars []) =+ mapM_ checkVar vars+ where+ checkVar (T_Assignment id Assign "PATH" Nothing word) =+ let string = concat $ deadSimple word+ in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do+ when ('/' `elem` string && ':' `notElem` string) $ notify id+ when (isLiteral word && ':' `notElem` string && '/' `notElem` string) $ notify id+ checkVar _ = return ()+ notify id = warn id 2123 "PATH is the shell search path. Use another name."+checkOverridingPath _ _ = return ()+++prop_checkUnsupported1 = verifyNot checkUnsupported "#!/bin/zsh\nfunction { echo cow; }"+prop_checkUnsupported2 = verify checkUnsupported "#!/bin/sh\nfunction { echo cow; }"+checkUnsupported params t =+ when (shellType params `notElem` support) $+ report name+ where+ (name, support) = shellSupport t+ report s = err (getId t) 2127 $+ "To use " ++ s ++ ", specify #!/usr/bin/env " +++ (map toLower . intercalate " or " . map show $ support)++-- TODO: Move more of these checks here+shellSupport t =+ case t of+ T_Function _ _ _ "" _ -> ("anonymous functions", [Zsh])+ 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+ 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 _ = []++groupWith f l = groupBy (\x y -> f x == f y) l++prop_checkMultipleAppends1 = verify checkMultipleAppends "foo >> file; bar >> file; baz >> file;"+prop_checkMultipleAppends2 = verify checkMultipleAppends "foo >> file; bar | grep f >> file; baz >> file;"+prop_checkMultipleAppends3 = verifyNot checkMultipleAppends "foo < file; bar < file; baz < file;"+checkMultipleAppends params t =+ mapM_ checkList $ getCommandSequences t+ where+ checkList list =+ mapM_ checkGroup groups+ where+ groups = groupWith (liftM fst) $ map getTarget list+ checkGroup (f:_:_:_) | isJust f =+ style (snd $ fromJust f) 2129+ "Consider using { cmd1; cmd2; } >> file instead of individual redirects."+ checkGroup _ = return ()+ getTarget (T_Pipeline _ _ args@(_:_)) = getTarget (last args)+ getTarget (T_Redirecting id list _) = do+ file <- (mapMaybe getAppend list) !!! 0+ return (file, id)+ getTarget _ = Nothing+ getAppend (T_FdRedirect _ _ (T_IoFile _ (T_DGREAT {}) f)) = return f+ getAppend _ = Nothing+++prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""+prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"+prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"+checkAliasesExpandEarly params =+ checkUnqualifiedCommand "alias" (const f)+ where+ f = mapM_ checkArg+ checkArg arg | '=' `elem` (concat $ deadSimple arg) =+ flip mapM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $+ \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."+ checkArg _ = return ()++prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\""+prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'"+checkSuspiciousIFS params (T_Assignment id Assign "IFS" Nothing value) =+ potentially $ do+ str <- getLiteralString value+ return $ check str+ where+ n = if (shellType params == Sh) then "'<literal linefeed here>'" else "$'\\n'"+ t = if (shellType params == Sh) then "\"$(printf '\\t')\"" else "$'\\t'"+ check value =+ case value of+ "\\n" -> suggest n+ "/n" -> suggest n+ "\\t" -> suggest t+ "/t" -> suggest t+ _ -> return ()+ suggest r = warn id 2141 $ "Did you mean IFS=" ++ r ++ " ?"+checkSuspiciousIFS _ _ = return ()++prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"+prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"+prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""+checkAliasesUsesArgs params =+ checkUnqualifiedCommand "alias" (const f)+ where+ re = mkRegex "\\$\\{?[0-9*@]"+ f = mapM_ checkArg+ checkArg arg =+ let string = fromJust $ getLiteralStringExt (const $ return "_") arg in+ when ('=' `elem` string && string `matches` re) $+ err (getId arg) 2142+ "Aliases can't use positional parameters. Use a function."++prop_checkGrepQ1= verify checkShouldUseGrepQ "[[ $(foo | grep bar) ]]"+prop_checkGrepQ2= verify checkShouldUseGrepQ "[ -z $(fgrep lol) ]"+prop_checkGrepQ3= verify checkShouldUseGrepQ "[ -n \"$(foo | zgrep lol)\" ]"+prop_checkGrepQ4= verifyNot checkShouldUseGrepQ "[ -z $(grep bar | cmd) ]"+prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"+checkShouldUseGrepQ params t =+ potentially $ case t of+ TC_Noary id _ token -> check id True token+ TC_Unary id _ "-n" token -> check id True token+ TC_Unary id _ "-z" token -> check id False token+ _ -> fail "not check"+ where+ check id bool token = do+ name <- getFinalGrep token+ let op = if bool then "-n" else "-z"+ let flip = if bool then "" else "! "+ return . style id 2143 $+ "Instead of [ " ++ op ++ " $(foo | " ++ name ++ " bar) ], " +++ "use " ++ flip ++ "foo | " ++ name ++ " -q bar ."++ getFinalGrep t = do+ cmds <- getPipeline t+ guard . not . null $ cmds+ name <- getCommandBasename $ last cmds+ guard . isGrep $ name+ return name+ getPipeline t =+ case t of+ T_NormalWord _ [x] -> getPipeline x+ T_DoubleQuoted _ [x] -> getPipeline x+ T_DollarExpansion _ [x] -> getPipeline x+ T_Pipeline _ _ cmds -> return cmds+ _ -> fail "unknown"+ isGrep = isSuffixOf "grep"++prop_checkTestGlobs1 = verify checkTestGlobs "[ -e *.mp3 ]"+prop_checkTestGlobs2 = verifyNot checkTestGlobs "[[ $a == *b* ]]"+checkTestGlobs params (TC_Unary _ _ op token) | isGlob token =+ err (getId token) 2144 $+ op ++ " doesn't work with globs. Use a for loop."+checkTestGlobs _ _ = return ()
ShellCheck/Data.hs view
@@ -1,6 +1,6 @@ module ShellCheck.Data where -shellcheckVersion = "0.3.2" -- Must also be updated in ShellCheck.cabal+shellcheckVersion = "0.3.3" -- Must also be updated in ShellCheck.cabal internalVariables = [ -- Generic@@ -73,4 +73,12 @@ "unlink", "unset", "uucp", "uudecode", "uuencode", "uustat", "uux", "val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc", "zcat"+ ]++sampleWords = [+ "alpha", "bravo", "charlie", "delta", "echo", "foxtrot",+ "golf", "hotel", "india", "juliett", "kilo", "lima", "mike",+ "november", "oscar", "papa", "quebec", "romeo", "sierra",+ "tango", "uniform", "victor", "whiskey", "xray", "yankee",+ "zulu" ]
ShellCheck/Parser.hs view
@@ -64,6 +64,11 @@ optional readComment return $ concat x +spacing1 = do+ spacing <- spacing+ when (null spacing) $ fail "no spacing"+ return spacing+ prop_allspacing = isOk allspacing "#foo" prop_allspacing2 = isOk allspacing " #foo\n # bar\n#baz\n" prop_allspacing3 = isOk allspacing "#foo\n#bar\n#baz\n"@@ -403,7 +408,7 @@ where readGlobLiteral = do id <- getNextId- s <- many1 (extglobStart <|> oneOf "[]$")+ s <- many1 (extglobStart <|> oneOf "{}[]$") return $ T_Literal id s readGroup = called "regex grouping" $ do id <- getNextId@@ -632,6 +637,7 @@ prop_readCondition10= isOk readCondition "[[ a == b \n || c == d ]]" prop_readCondition11= isOk readCondition "[[ a == b || \n c == d ]]" prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]"+prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]" readCondition = called "test expression" $ do opos <- getPosition id <- getNextId@@ -700,6 +706,8 @@ prop_readNormalWord2 = isOk readNormalWord "foo**(foo)!!!(@@(bar))" prop_readNormalWord3 = isOk readNormalWord "foo#" prop_readNormalWord4 = isOk readNormalWord "$\"foo\"$'foo\nbar'"+prop_readNormalWord5 = isWarning readNormalWord "${foo}}"+prop_readNormalWord6 = isOk readNormalWord "foo/{}" readNormalWord = readNormalishWord "" readNormalishWord end = do@@ -710,14 +718,24 @@ return $ T_NormalWord id x checkPossibleTermination pos [T_Literal _ x] =- if x `elem` ["do", "done", "then", "fi", "esac", "}"]+ 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 () checkPossibleTermination _ _ = return () readNormalWordPart end = do checkForParenthesis- readSingleQuoted <|> readDoubleQuoted <|> readGlob <|> readNormalDollar <|> readBraced <|> readBackTicked <|> readProcSub <|> (readNormalLiteral end)+ choice [+ readSingleQuoted,+ readDoubleQuoted,+ readGlob,+ readNormalDollar,+ readBraced,+ readBackTicked,+ readProcSub,+ readNormalLiteral end,+ readLiteralCurlyBraces+ ] where checkForParenthesis = do return () `attempting` do@@ -725,7 +743,20 @@ lookAhead $ char '(' parseProblemAt pos ErrorC 1036 "'(' is invalid here. Did you forget to escape it?" + readLiteralCurlyBraces = do+ id <- getNextId+ str <- findParam <|> literalBraces+ return $ T_Literal id str + findParam = try $ string "{}"+ literalBraces = do+ pos <- getPosition+ c <- oneOf "{}"+ parseProblemAt pos WarningC 1083 $+ "This " ++ [c] ++ " is literal. Check expression (missing ;/\\n?) or quote it."+ return [c]++ readSpacePart = do id <- getNextId x <- many1 whitespace@@ -745,10 +776,11 @@ prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)" prop_readProcSub2 = isOk readProcSub "<( if true; then true; fi )"+prop_readProcSub3 = isOk readProcSub "=(ls)" readProcSub = called "process substitution" $ do id <- getNextId dir <- try $ do- x <- oneOf "<>"+ x <- oneOf "<>=" char '(' return [x] allspacing@@ -922,7 +954,7 @@ return $ T_Literal id [c] readNormalLiteralPart end = do- readNormalEscaped <|> (many1 $ noneOf (end ++ quotableChars ++ extglobStartChars ++ "["))+ readNormalEscaped <|> (many1 $ noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}")) readNormalEscaped = called "escaped char" $ do pos <- getPosition@@ -934,10 +966,12 @@ do next <- anyChar case escapedChar next of- Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use \"$(printf \"\\" ++ [next] ++ "\")\"."+ 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+ alternative 'n' = "a quoted, literal line feed"+ alternative t = "\"$(printf \"\\" ++ [t] ++ "\")\"" escapedChar 'n' = Just "line feed" escapedChar 't' = Just "tab" escapedChar 'r' = Just "carriage return"@@ -1027,7 +1061,10 @@ char '{' str <- many1 ((readDoubleQuotedLiteral >>= (strip)) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace)) char '}'- return $ T_BraceExpansion id $ concat str+ let result = concat str+ unless (',' `elem` result || ".." `isInfixOf` result) $+ fail "Not a brace expression"+ return $ T_BraceExpansion id $ result readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely@@ -1077,7 +1114,7 @@ prop_readDollarBraced1 = isOk readDollarBraced "${foo//bar/baz}" prop_readDollarBraced2 = isOk readDollarBraced "${foo/'{cow}'}"-prop_readDollarBraced3 = isOk readDollarBraced "${foo%%$(echo cow})}"+prop_readDollarBraced3 = isOk readDollarBraced "${foo%%$(echo cow\\})}" prop_readDollarBraced4 = isOk readDollarBraced "${foo#\\}}" readDollarBraced = called "parameter expansion" $ do id <- getNextId@@ -1131,7 +1168,6 @@ pos <- getPosition char '$' n <- lookAhead (anyChar <|> (eof >> return '_'))- when (n /= '\'') $ parseNoteAt pos StyleC 1000 "$ is not used specially and should therefore be escaped." return $ T_Literal id "$" prop_readHereDoc = isOk readHereDoc "<< foo\nlol\ncow\nfoo"@@ -1266,8 +1302,8 @@ readNewlineList = many1 ((newline <|> carriageReturn) `thenSkip` spacing) readLineBreak = optional readNewlineList -prop_roflol = isWarning readScript "a &; b"-prop_roflol2 = isOk readScript "a & b"+prop_readSeparator1 = isWarning readScript "a &; b"+prop_readSeparator2 = isOk readScript "a & b" readSeparatorOp = do notFollowedBy2 (g_AND_IF <|> g_DSEMI) notFollowedBy2 (string "&>")@@ -1276,6 +1312,8 @@ spacing pos <- getPosition char ';'+ -- In case statements we might have foo & ;;+ notFollowedBy2 $ char ';' parseProblemAt pos ErrorC 1045 "It's not 'foo &; bar', just 'foo & bar'." return '&' ) <|> char ';' <|> char '&'@@ -1317,6 +1355,7 @@ prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)" prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi" prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"+prop_readSimpleCommand7 = isOk readSimpleCommand "cat =(ls)" readSimpleCommand = called "simple command" $ do id1 <- getNextId id2 <- getNextId@@ -1568,6 +1607,7 @@ prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone" prop_readForClause9 = isOk readForClause "for i do true; done" prop_readForClause10= isOk readForClause "for ((;;)) { true; }"+prop_readForClause11= isOk readForClause "for a b in *; do echo $a $b; done" readForClause = called "for loop" $ do pos <- getPosition (T_For id) <- g_For@@ -1593,12 +1633,26 @@ return list readRegular id pos = do- name <- readVariableName- spacing- values <- readInClause <|> (optional readSequentialSep >> return [])- group <- readDoGroup pos- return $ T_ForIn id name values group+ names <- readNames+ readShort names <|> readLong names+ where+ readLong names = do+ values <- readInClause <|> (optional readSequentialSep >> return [])+ group <- readDoGroup pos+ return $ T_ForIn id NormalForIn names values group+ readShort names = do+ char '('+ allspacing+ words <- many (readNormalWord `thenSkip` allspacing)+ char ')'+ allspacing+ command <- readAndOr+ return $ T_ForIn id ShortForIn names words [command] + readNames =+ reluctantlyTill1 (readVariableName `thenSkip` spacing) $+ disregard g_Do <|> disregard readInClause <|> disregard readSequentialSep+ prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done" prop_readSelectClause2 = isOk readSelectClause "select foo; do echo $foo; done" readSelectClause = called "select loop" $ do@@ -1632,6 +1686,7 @@ 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" readCaseClause = called "case expression" $ do id <- getNextId g_Case@@ -1705,7 +1760,7 @@ g_Rparen return () - readFunctionName = many1 functionChars+ readFunctionName = many functionChars readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing) @@ -1753,6 +1808,9 @@ pos <- getPosition optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.") variable <- readVariableName+ notFollowedBy2 $ do -- Special case for zsh =(..) syntax+ spacing1+ string "=(" optional (readNormalDollar >> parseNoteAt pos ErrorC 1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'") index <- optionMaybe readArrayIndex@@ -1893,6 +1951,10 @@ readScript = do id <- getNextId pos <- getPosition+ optional $ do+ readUtf8Bom+ 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@@ -1951,6 +2013,8 @@ "ruby", "tcsh" ]++ readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF" rp p filename contents = Ms.runState (runParserT p initialState filename contents) ([], [])