ShellCheck 0.4.4 → 0.4.5
raw patch · 12 files changed
+497/−142 lines, 12 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ ShellCheck.AST: T_IoDuplicate :: Id -> Token -> String -> Token
+ ShellCheck.AST: T_UnparsedIndex :: Id -> SourcePos -> String -> Token
+ ShellCheck.ASTLib: getAssociativeArrays :: Token -> [String]
+ ShellCheck.ASTLib: getCommand :: Token -> Maybe Token
+ ShellCheck.ASTLib: getCommandNameFromExpansion :: Token -> Maybe String
+ ShellCheck.AnalyzerLib: getBracedModifier :: [Char] -> [Char]
+ ShellCheck.AnalyzerLib: getOffsetReferences :: String -> [String]
+ ShellCheck.AnalyzerLib: prop_getBracedModifier1 :: Bool
+ ShellCheck.AnalyzerLib: prop_getBracedModifier2 :: Bool
+ ShellCheck.AnalyzerLib: prop_getBracedModifier3 :: Bool
+ ShellCheck.AnalyzerLib: prop_getBracedReference13 :: Bool
+ ShellCheck.Formatter.Format: endColNo :: PositionedComment -> Integer
+ ShellCheck.Formatter.Format: endLineNo :: PositionedComment -> Integer
- ShellCheck.AST: T_Assignment :: Id -> AssignmentMode -> String -> (Maybe Token) -> Token -> Token
+ ShellCheck.AST: T_Assignment :: Id -> AssignmentMode -> String -> [Token] -> Token -> Token
- ShellCheck.AST: T_IndexedElement :: Id -> Token -> Token -> Token
+ ShellCheck.AST: T_IndexedElement :: Id -> [Token] -> Token -> Token
- ShellCheck.AST: analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
+ ShellCheck.AST: analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
- ShellCheck.Interface: PositionedComment :: Position -> Comment -> PositionedComment
+ ShellCheck.Interface: PositionedComment :: Position -> Position -> Comment -> PositionedComment
Files
- README.md +4/−0
- ShellCheck.cabal +1/−1
- ShellCheck/AST.hs +20/−10
- ShellCheck/ASTLib.hs +48/−6
- ShellCheck/Analytics.hs +104/−22
- ShellCheck/AnalyzerLib.hs +41/−5
- ShellCheck/Checker.hs +5/−5
- ShellCheck/Checks/Commands.hs +67/−4
- ShellCheck/Formatter/Format.hs +15/−10
- ShellCheck/Formatter/JSON.hs +6/−4
- ShellCheck/Interface.hs +1/−1
- ShellCheck/Parser.hs +185/−74
README.md view
@@ -70,6 +70,10 @@ apt-get install shellcheck +On Gentoo based distros:++ emerge --ask shellcheck+ On Fedora based distros: dnf install ShellCheck
ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name: ShellCheck-Version: 0.4.4+Version: 0.4.5 Synopsis: Shell script analysis tool License: GPL-3 License-file: LICENSE
ShellCheck/AST.hs view
@@ -21,6 +21,7 @@ import Control.Monad import Control.Monad.Identity+import Text.Parsec import qualified ShellCheck.Regex as Re data Id = Id Int deriving (Show, Eq, Ord)@@ -50,8 +51,10 @@ | T_AndIf Id (Token) (Token) | T_Arithmetic Id Token | T_Array Id [Token]- | T_IndexedElement Id Token Token- | T_Assignment Id AssignmentMode String (Maybe Token) Token+ | T_IndexedElement Id [Token] Token+ -- Store the index as string, and parse as arithmetic or string later+ | T_UnparsedIndex Id SourcePos String+ | T_Assignment Id AssignmentMode String [Token] Token | T_Backgrounded Id Token | T_Backticked Id [Token] | T_Bang Id@@ -96,6 +99,7 @@ | T_IfExpression Id [([Token],[Token])] [Token] | T_In Id | T_IoFile Id Token Token+ | T_IoDuplicate Id Token String | T_LESSAND Id | T_LESSGREAT Id | T_Lbrace Id@@ -145,7 +149,7 @@ instance Eq Token where (==) = tokenEquals -analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token+analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token analyze f g i = round where@@ -153,7 +157,7 @@ f t newT <- delve t g t- return . i $ newT+ i newT roundAll = mapM round roundMaybe Nothing = return Nothing@@ -186,14 +190,18 @@ delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id delve (T_IoFile id op file) = d2 op file $ T_IoFile id+ delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num delve (T_HereString id word) = d1 word $ T_HereString id delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v- delve (T_Assignment id mode var index value) = do- a <- roundMaybe index+ delve (T_Assignment id mode var indices value) = do+ a <- roundAll indices 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_IndexedElement id indices t) = do+ a <- roundAll indices+ b <- round t+ return $ T_IndexedElement id a b delve (T_Redirecting id redirs cmd) = do a <- roundAll redirs b <- round cmd@@ -312,6 +320,7 @@ T_BraceExpansion id _ -> id T_DollarBraceCommandExpansion id _ -> id T_IoFile id _ _ -> id+ T_IoDuplicate id _ _ -> id T_HereDoc id _ _ _ _ -> id T_HereString id _ -> id T_FdRedirect id _ _ -> id@@ -363,10 +372,11 @@ T_CoProc id _ _ -> id T_CoProcBody id _ -> id T_Include id _ _ -> id+ T_UnparsedIndex id _ _ -> id blank :: Monad m => Token -> m () blank = const $ return ()-doAnalysis f = analyze f blank id-doStackAnalysis startToken endToken = analyze startToken endToken id-doTransform i = runIdentity . analyze blank blank i+doAnalysis f = analyze f blank (return . id)+doStackAnalysis startToken endToken = analyze startToken endToken (return . id)+doTransform i = runIdentity . analyze blank blank (return . i)
ShellCheck/ASTLib.hs view
@@ -21,6 +21,7 @@ import ShellCheck.AST +import Control.Monad.Writer import Control.Monad import Data.List import Data.Maybe@@ -54,6 +55,8 @@ -- Is this shell word a constant? isConstant token = case token of+ -- This ignores some cases like ~"foo":+ T_NormalWord _ (T_Literal _ ('~':_) : _) -> False T_NormalWord _ l -> all isConstant l T_DoubleQuoted _ l -> all isConstant l T_SingleQuoted _ _ -> True@@ -194,6 +197,8 @@ -- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] getWordParts (T_NormalWord _ l) = concatMap getWordParts l getWordParts (T_DoubleQuoted _ l) = l+-- TA_Expansion is basically T_NormalWord for arithmetic expressions+getWordParts (TA_Expansion _ l) = concatMap getWordParts l getWordParts other = [other] -- Return a list of NormalWords that would result from brace expansion@@ -206,13 +211,31 @@ braceExpand item part x = return x +-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections+getCommand t =+ case t of+ T_Redirecting _ _ w -> getCommand w+ T_SimpleCommand _ _ (w:_) -> return t+ T_Annotation _ _ t -> getCommand t+ otherwise -> Nothing+ -- Maybe get the command name of a token representing a command-getCommandName t =+getCommandName t = do+ (T_SimpleCommand _ _ (w:_)) <- getCommand t+ getLiteralString w++-- If a command substitution is a single command, get its name.+-- $(date +%s) = Just "date"+getCommandNameFromExpansion :: Token -> Maybe String+getCommandNameFromExpansion t = case t of- T_Redirecting _ _ w -> getCommandName w- T_SimpleCommand _ _ (w:_) -> getLiteralString w- T_Annotation _ _ t -> getCommandName t+ T_DollarExpansion _ [c] -> extract c+ T_Backticked _ [c] -> extract c+ T_DollarBraceCommandExpansion _ [c] -> extract c otherwise -> Nothing+ where+ extract (T_Pipeline _ _ [cmd]) = getCommandName cmd+ extract _ = Nothing -- Get the basename of a token representing a command getCommandBasename = liftM basename . getCommandName@@ -237,8 +260,8 @@ isFunction t = case t of T_Function {} -> True; _ -> False --- Get the list of commands from tokens that contain them, such as--- the body of while loops and if statements.+-- Get the lists of commands from tokens that contain them, such as+-- the body of while loops or branches of if statements. getCommandSequences t = case t of T_Script _ _ cmds -> [cmds]@@ -251,3 +274,22 @@ T_IfExpression _ thens elses -> map snd thens ++ [elses] otherwise -> [] +-- Get a list of names of associative arrays+getAssociativeArrays t =+ nub . execWriter $ doAnalysis f t+ where+ f :: Token -> Writer [String] ()+ f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do+ name <- getCommandName t+ guard $ name == "declare"+ let flags = getAllFlags t+ guard $ elem "A" $ map snd flags+ let args = map fst . filter ((==) "" . snd) $ flags+ let names = mapMaybe (getLiteralStringExt nameAssignments) args+ return $ tell names+ f _ = return ()++ nameAssignments t =+ case t of+ T_Assignment _ _ name _ _ -> return name+ otherwise -> Nothing
ShellCheck/Analytics.hs view
@@ -85,6 +85,7 @@ ,checkEchoSed ,checkForDecimals ,checkLocalScope+ ,checkMultiDimensionalArrays ] runAnalytics :: AnalysisSpec -> [TokenComment]@@ -178,6 +179,7 @@ ,checkReadWithoutR ,checkLoopVariableReassignment ,checkTrailingBracket+ ,checkReturnAgainstZero ] @@ -369,6 +371,8 @@ prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo" prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo" prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo"+prop_checkPipePitfalls8 = verify checkPipePitfalls "foo | grep bar | wc -l"+prop_checkPipePitfalls9 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -l" checkPipePitfalls _ (T_Pipeline id _ commands) = do for ["find", "xargs"] $ \(find:xargs:_) ->@@ -388,8 +392,12 @@ 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."+ for ["grep", "wc"] $+ \(grep:wc:_) ->+ let flags = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep+ in+ unless (any (`elem` ["o", "only-matching"]) flags) $+ style (getId grep) 2126 "Consider using grep -c instead of grep|wc." didLs <- liftM or . sequence $ [ for' ["ls", "grep"] $@@ -439,10 +447,16 @@ prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho cow" prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l " prop_checkShebang3 = verifyTree checkShebang "ls -l"-checkShebang params (T_Annotation _ _ t) = checkShebang params t+prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"+checkShebang params (T_Annotation _ list t) =+ if any isOverride list then [] else checkShebang params t+ where+ isOverride (ShellOverride _) = True+ isOverride _ = False checkShebang params (T_Script id sb _) =- [makeComment ErrorC id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."- | not (shellTypeSpecified params) && sb == "" ]+ [makeComment ErrorC id 2148+ "Tips depend on target shell and yours is unknown. Add a shebang."+ | not (shellTypeSpecified params) && sb == "" ] prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"@@ -493,6 +507,9 @@ prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null" prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO" prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"+prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"+prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"+prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2" checkBashisms params = bashism where isDash = shellType params == Dash@@ -524,6 +541,7 @@ warnMsg id $ filter (/= '|') op ++ " is" bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"+ bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is" bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are" bashism (T_FdRedirect id num _) | all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"@@ -765,19 +783,24 @@ 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"+prop_checkUnquotedExpansions8 = verifyNot checkUnquotedExpansions "set -- $(seq 1 4)"+prop_checkUnquotedExpansions9 = verifyNot checkUnquotedExpansions "echo foo `# inline comment`" checkUnquotedExpansions params = check where- check t@(T_DollarExpansion _ _) = examine t- check t@(T_Backticked _ _) = examine t- check t@(T_DollarBraceCommandExpansion _ _) = examine t+ check t@(T_DollarExpansion _ c) = examine t c+ check t@(T_Backticked _ c) = examine t c+ check t@(T_DollarBraceCommandExpansion _ c) = examine t c check _ = return () tree = parentMap params- examine t =- unless (isQuoteFree tree t || usedAsCommandName tree t) $+ examine t contents =+ unless (null contents || shouldBeSplit t || isQuoteFree tree t || usedAsCommandName tree t) $ warn (getId t) 2046 "Quote this to prevent word splitting." + shouldBeSplit t =+ getCommandNameFromExpansion t == Just "seq" + prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo" prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/g' > lol" prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol"@@ -933,7 +956,7 @@ "Expanding an array without an index only gives the first element." readF _ _ _ = return [] - writeF _ (T_Assignment id mode name Nothing _) _ (DataString _) = do+ writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do isArray <- gets (isJust . Map.lookup name) return $ if not isArray then [] else case mode of@@ -951,7 +974,7 @@ isIndexed expr = case expr of- T_Assignment _ _ _ (Just _) _ -> True+ T_Assignment _ _ _ (_:_) _ -> True _ -> False prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow"@@ -961,7 +984,7 @@ prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)" prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null" checkStderrRedirect params redir@(T_Redirecting _ [- T_FdRedirect id "2" (T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "1"])),+ T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"), T_FdRedirect _ _ (T_IoFile _ op _) ] _) = case op of T_Greater _ -> error@@ -1030,6 +1053,7 @@ ,"alias" ,"sudo" -- covering "sudo sh" and such ,"dpkg-query"+ ,"jq" -- could also check that user provides --arg ] || "awk" `isSuffixOf` commandName || "perl" `isPrefixOf` commandName@@ -1154,6 +1178,7 @@ prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]" prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]" prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done"+prop_checkSingleBracketOperators6 = verifyNot checkSingleBracketOperators "[ $foo '>' $bar ]" checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs) | typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] = err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]."@@ -1234,12 +1259,11 @@ prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]" prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]" prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"+prop_checkConstantIfs8 = verifyNot checkConstantIfs "[[ ~foo == '~foo' ]]" checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =- when (isJust lLit && isJust rLit) $+ when (isConstant lhs && isConstant rhs) $ warn id 2050 "This expression is constant. Did you forget the $ on a variable?" where- lLit = getLiteralString lhs- rLit = getLiteralString rhs isDynamic = op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ] && typ == DoubleBracket@@ -1322,7 +1346,9 @@ (`isUnqualifiedCommand` "eval") <$> getClosestCommand (parentMap params) t checkBraceExpansionVars _ _ = return () -prop_checkForDecimals = verify checkForDecimals "((3.14*c))"+prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"+prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"+prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" checkForDecimals params t@(TA_Expansion id _) = potentially $ do guard $ not (hasFloatingPoint params) str <- getLiteralString t@@ -1356,7 +1382,7 @@ unless (isException $ bracedString b) getWarning where isException [] = True- isException s = any (`elem` "/.:#%?*@$") s || isDigit (head s)+ isException s = any (`elem` "/.:#%?*@$-") s || isDigit (head s) getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t warningFor t = case t of@@ -2071,6 +2097,8 @@ prop_checkUnused31= verifyTree checkUnusedAssignments "let 'a=1'" prop_checkUnused32= verifyTree checkUnusedAssignments "let a=b=c; echo $a" prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]"+prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t"+prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}" checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) where flow = variableFlow params@@ -2115,6 +2143,11 @@ prop_checkUnassignedReferences20= verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo" prop_checkUnassignedReferences21= verifyTree checkUnassignedReferences "echo ${#foo}" prop_checkUnassignedReferences22= verifyNotTree checkUnassignedReferences "echo ${!os*}"+prop_checkUnassignedReferences23= verifyTree checkUnassignedReferences "declare -a foo; foo[bar]=42;"+prop_checkUnassignedReferences24= verifyNotTree checkUnassignedReferences "declare -A foo; foo[bar]=42;"+prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;"+prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b"+prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}" checkUnassignedReferences params t = warnings where (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)@@ -2262,7 +2295,7 @@ case t of T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars otherwise -> check rest- checkVar (T_Assignment aId mode aName Nothing value) |+ checkVar (T_Assignment aId mode aName [] value) | aName == name && (aId `notElem` idPath) = do warn aId 2097 "This assignment is only seen by the forked process." warn id 2098 "This expansion will not see the mentioned assignment."@@ -2542,7 +2575,7 @@ checkOverridingPath _ (T_SimpleCommand _ vars []) = mapM_ checkVar vars where- checkVar (T_Assignment id Assign "PATH" Nothing word) =+ checkVar (T_Assignment id Assign "PATH" [] word) = let string = concat $ oversimplify word in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do when ('/' `elem` string && ':' `notElem` string) $ notify id@@ -2557,7 +2590,7 @@ checkTildeInPath _ (T_SimpleCommand _ vars _) = mapM_ checkVar vars where- checkVar (T_Assignment id Assign "PATH" Nothing (T_NormalWord _ parts)) =+ checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts)) = when (any (\x -> isQuoted x && hasTilde x) parts) $ warn id 2147 "Literal tilde in PATH works poorly across programs." checkVar _ = return ()@@ -2618,7 +2651,7 @@ prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\"" prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'"-checkSuspiciousIFS params (T_Assignment id Assign "IFS" Nothing value) =+checkSuspiciousIFS params (T_Assignment id Assign "IFS" [] value) = potentially $ do str <- getLiteralString value return $ check str@@ -2789,6 +2822,55 @@ "]]" -> "[[" "]" -> "[" x -> x++prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"+prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"+prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"+prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"+prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"+prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"+checkMultiDimensionalArrays _ token =+ case token of+ T_Assignment _ _ name (first:second:_) _ -> about second+ T_IndexedElement _ (first:second:_) _ -> about second+ T_DollarBraced {} ->+ when (isMultiDim token) $ about token+ _ -> return ()+ where+ about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."++ re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well+ isMultiDim t = getBracedModifier (bracedString t) `matches` re++prop_checkReturnAgainstZero1 = verify checkReturnAgainstZero "[ $? -eq 0 ]"+prop_checkReturnAgainstZero2 = verify checkReturnAgainstZero "[[ \"$?\" -gt 0 ]]"+prop_checkReturnAgainstZero3 = verify checkReturnAgainstZero "[[ 0 -ne $? ]]"+prop_checkReturnAgainstZero4 = verifyNot checkReturnAgainstZero "[[ $? -eq 4 ]]"+prop_checkReturnAgainstZero5 = verify checkReturnAgainstZero "[[ 0 -eq $? ]]"+prop_checkReturnAgainstZero6 = verifyNot checkReturnAgainstZero "[[ $R -eq 0 ]]"+prop_checkReturnAgainstZero7 = verify checkReturnAgainstZero "(( $? == 0 ))"+prop_checkReturnAgainstZero8 = verify checkReturnAgainstZero "(( $? ))"+prop_checkReturnAgainstZero9 = verify checkReturnAgainstZero "(( ! $? ))"+checkReturnAgainstZero _ token =+ case token of+ TC_Binary id _ _ lhs rhs -> check lhs rhs+ TA_Binary id _ lhs rhs -> check lhs rhs+ TA_Unary id _ exp ->+ when (isExitCode exp) $ message (getId exp)+ TA_Sequence _ [exp] ->+ when (isExitCode exp) $ message (getId exp)+ otherwise -> return ()+ where+ check lhs rhs =+ if isZero rhs && isExitCode lhs+ then message (getId lhs)+ else when (isZero lhs && isExitCode rhs) $ message (getId rhs)+ isZero t = getLiteralString t == Just "0"+ isExitCode t =+ case getWordParts t of+ [exp@(T_DollarBraced {})] -> bracedString exp == "?"+ otherwise -> False+ message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?." return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
ShellCheck/AnalyzerLib.hs view
@@ -334,6 +334,12 @@ name <- getLiteralString lhs return (t, t, name, DataString $ SourceFrom [rhs]) + T_DollarBraced _ l -> maybeToList $ do+ let string = bracedString t+ let modifier = getBracedModifier string+ guard $ ":=" `isPrefixOf` modifier+ return (t, t, getBracedReference string, DataString $ SourceFrom [l])+ t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] @@ -361,7 +367,10 @@ "declare" -> if any (`elem` flags) ["x", "p"] then concatMap getReference rest else []- "readonly" -> concatMap getReference rest+ "readonly" ->+ if any (`elem` flags) ["f", "p"]+ then []+ else concatMap getReference rest "trap" -> case rest of head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head@@ -395,7 +404,10 @@ "typeset" -> declaredVars "local" -> concatMap getModifierParamString rest- "readonly" -> concatMap getModifierParamString rest+ "readonly" ->+ if any (`elem` flags) ["f", "p"]+ then []+ else concatMap getModifierParamString rest "set" -> maybeToList $ do params <- getSetParams rest return (base, base, "@", DataString $ SourceFrom params)@@ -474,11 +486,20 @@ where re = mkRegex "(\\[.*\\])" +getOffsetReferences mods = fromMaybe [] $ do+ match <- matchRegex re mods+ offsets <- match !!! 0+ return $ matchAllStrings variableNameRegex offsets+ where+ re = mkRegex "^ *:(.*)"+ getReferencedVariables parents t = case t of T_DollarBraced id l -> let str = bracedString t in (t, t, getBracedReference str) :- map (\x -> (l, l, x)) (getIndexReferences str)+ map (\x -> (l, l, x)) (+ getIndexReferences str+ ++ getOffsetReferences (getBracedModifier str)) TA_Expansion id _ -> if isArithmeticAssignment t then []@@ -520,7 +541,7 @@ isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) isArithmeticAssignment t = case getPath parents t of- this: TA_Assignment _ "=" _ _ :_ -> True+ this: TA_Assignment _ "=" lhs _ :_ -> lhs == t _ -> False dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]@@ -573,6 +594,7 @@ prop_getBracedReference10= getBracedReference "foo: -1" == "foo" prop_getBracedReference11= getBracedReference "!os*" == "" prop_getBracedReference12= getBracedReference "!os?bar**" == ""+prop_getBracedReference13= getBracedReference "foo[bar]" == "foo" getBracedReference s = fromMaybe s $ nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s where@@ -595,7 +617,21 @@ return "" nameExpansion _ = Nothing +prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"+prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"+prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"+getBracedModifier s = fromMaybe "" . listToMaybe $ do+ let var = getBracedReference s+ a <- dropModifier s+ dropPrefix var a+ where+ dropPrefix [] t = return t+ dropPrefix (a:b) (c:d) | a == c = dropPrefix b d+ dropPrefix _ _ = [] + dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]+ dropModifier x = [x]+ -- Useful generic functions potentially :: Monad m => Maybe (m ()) -> m () potentially = fromMaybe (return ())@@ -628,5 +664,5 @@ getCode (TokenComment _ (Comment _ c _)) = c -return [] +return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
ShellCheck/Checker.hs view
@@ -39,7 +39,7 @@ tokenToPosition map (TokenComment id c) = fromMaybe fail $ do position <- Map.lookup id map- return $ PositionedComment position c+ return $ PositionedComment position position c where fail = error "Internal shellcheck error: id doesn't exist. Please report!" @@ -65,13 +65,13 @@ return . nub . sortMessages . filter shouldInclude $ (parseMessages ++ map translator analysisMessages) - shouldInclude (PositionedComment _ (Comment _ code _)) =+ shouldInclude (PositionedComment _ _ (Comment _ code _)) = code `notElem` csExcludedWarnings spec sortMessages = sortBy (comparing order)- order (PositionedComment pos (Comment severity code message)) =+ order (PositionedComment pos _ (Comment severity code message)) = (posFile pos, posLine pos, posColumn pos, severity, code, message)- getPosition (PositionedComment pos _) = pos+ getPosition (PositionedComment pos _ _) = pos analysisSpec root = AnalysisSpec {@@ -84,7 +84,7 @@ sort . map getCode . crComments $ runIdentity (checkScript sys spec) where- getCode (PositionedComment _ (Comment _ code _)) = code+ getCode (PositionedComment _ _ (Comment _ code _)) = code check = checkWithIncludes []
ShellCheck/Checks/Commands.hs view
@@ -90,6 +90,8 @@ ,checkExportedExpansions ,checkAliasesUsesArgs ,checkAliasesExpandEarly+ ,checkUnsetGlobs+ ,checkFindWithoutPath ] buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)@@ -150,7 +152,7 @@ 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) && ('*' `notElem` s)) $- info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets."+ info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets." Nothing -> return () duplicated s =@@ -470,16 +472,49 @@ prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'" prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"+prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar"+prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"+prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"+prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""+prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"+prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz" checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where+ f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest- f (format:params) = check format+ f (format:params) = check format params f _ = return ()- check format =++ countFormats string =+ case string of+ '%':'%':rest -> countFormats rest+ '%':rest -> 1 + countFormats rest+ _:rest -> countFormats rest+ [] -> 0++ check format more = do+ fromMaybe (return ()) $ do+ string <- getLiteralString format+ let vars = countFormats string++ return $ do+ when (vars == 0 && more /= []) $+ err (getId format) 2182+ "This printf format string has no variables. Other arguments are ignored."++ when (vars > 0+ && length more < vars+ && all (not . mayBecomeMultipleArgs) more) $+ warn (getId format) 2183 $+ "This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."++ unless ('%' `elem` concat (oversimplify format) || isLiteral format) $- warn (getId format) 2059+ info (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`" prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""@@ -554,6 +589,34 @@ forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $ \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping." checkArg _ = return ()+++prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"+prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"+checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)+ where+ check arg =+ when (isGlob arg) $+ warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded."+++prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f"+prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find"+prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"+prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"+checkFindWithoutPath = CommandCheck (Basename "find") f+ where+ f (T_SimpleCommand _ _ (cmd:args)) =+ unless (hasPath args) $+ info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."++ -- This is a bit of a kludge. find supports flag arguments both before and after the path,+ -- as well as multiple non-flag arguments that are not the path. We assume that all the+ -- pre-path flags are single characters, which is generally the case.+ hasPath (first:rest) =+ let flag = fromJust $ getLiteralStringExt (const $ return "___") first in+ not ("-" `isPrefixOf` flag) || length flag <= 2 && hasPath rest+ hasPath [] = False return []
ShellCheck/Formatter/Format.hs view
@@ -30,13 +30,15 @@ footer :: IO () } -lineNo (PositionedComment pos _) = posLine pos-colNo (PositionedComment pos _) = posColumn pos-codeNo (PositionedComment _ (Comment _ code _)) = code-messageText (PositionedComment _ (Comment _ _ t)) = t+lineNo (PositionedComment pos _ _) = posLine pos+endLineNo (PositionedComment _ end _) = posLine end+colNo (PositionedComment pos _ _) = posColumn pos+endColNo (PositionedComment _ end _) = posColumn end+codeNo (PositionedComment _ _ (Comment _ code _)) = code+messageText (PositionedComment _ _ (Comment _ _ t)) = t severityText :: PositionedComment -> String-severityText (PositionedComment _ (Comment c _ _)) =+severityText (PositionedComment _ _ (Comment c _ _)) = case c of ErrorC -> "error" WarningC -> "warning"@@ -48,12 +50,15 @@ map fix comments where ls = lines contents- fix c@(PositionedComment pos comment) = PositionedComment pos {- posColumn =- if lineNo c > 0 && lineNo c <= fromIntegral (length ls)- then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)- else colNo c+ fix c@(PositionedComment start end comment) = PositionedComment start {+ posColumn = realignColumn lineNo colNo c+ } end {+ posColumn = realignColumn endLineNo endColNo c } comment+ realignColumn lineNo colNo c =+ if lineNo c > 0 && lineNo c <= fromIntegral (length ls)+ then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)+ else colNo c real _ r v target | target <= v = r real [] r v _ = r -- should never happen real ('\t':rest) r v target =
ShellCheck/Formatter/JSON.hs view
@@ -37,10 +37,12 @@ } instance JSON (PositionedComment) where- showJSON comment@(PositionedComment pos (Comment level code string)) = makeObj [- ("file", showJSON $ posFile pos),- ("line", showJSON $ posLine pos),- ("column", showJSON $ posColumn pos),+ showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [+ ("file", showJSON $ posFile start),+ ("line", showJSON $ posLine start),+ ("endLine", showJSON $ posLine end),+ ("column", showJSON $ posColumn start),+ ("endColumn", showJSON $ posColumn end), ("level", showJSON $ severityText comment), ("code", showJSON code), ("message", showJSON string)
ShellCheck/Interface.hs view
@@ -94,7 +94,7 @@ } deriving (Show, Eq) data Comment = Comment Severity Code String deriving (Show, Eq)-data PositionedComment = PositionedComment Position Comment deriving (Show, Eq)+data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq) data TokenComment = TokenComment Id Comment deriving (Show, Eq) data ColorOption =
ShellCheck/Parser.hs view
@@ -135,7 +135,7 @@ --------- Message/position annotation on top of user state data Note = Note Id Severity Code String deriving (Show, Eq)-data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq)+data ParseNote = ParseNote SourcePos SourcePos Severity Code String deriving (Show, Eq) data Context = ContextName SourcePos String | ContextAnnotation [Annotation]@@ -162,9 +162,9 @@ pendingHereDocs = [] } -codeForParseNote (ParseNote _ _ code _) = code+codeForParseNote (ParseNote _ _ _ code _) = code noteToParseNote map (Note id severity code message) =- ParseNote pos severity code message+ ParseNote pos pos severity code message where pos = fromJust $ Map.lookup id map @@ -181,6 +181,7 @@ return newId where incId (Id n) = Id $ n+1 +getNextId :: Monad m => SCParser m Id getNextId = do pos <- getPosition getNextIdAt pos@@ -320,23 +321,27 @@ v <- getCurrentContexts setCurrentContexts (c:v) -parseProblemAt pos level code msg = do+parseProblemAtWithEnd start end level code msg = do irrelevant <- shouldIgnoreCode code unless irrelevant $ Ms.modify (\state -> state { parseProblems = note:parseProblems state }) where- note = ParseNote pos level code msg+ note = ParseNote start end level code msg +parseProblemAt pos = parseProblemAtWithEnd pos pos+ -- Store non-parse problems inside parseNote c l a = do pos <- getPosition parseNoteAt pos c l a -parseNoteAt pos c l a = addParseNote $ ParseNote pos c l a+parseNoteAt pos c l a = addParseNote $ ParseNote pos pos c l a +parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a+ --------- Convenient combinators thenSkip main follow = do r <- main@@ -406,7 +411,7 @@ pos <- getPosition s <- many1 letter when (s `elem` commonCommands) $- parseProblemAt pos WarningC 1009 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")+ parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.") where spacingOrLf = condSpacing True@@ -442,7 +447,7 @@ c <- oneOf "'\"" s <- anyOp char c- return s+ return $ escaped s anyOp = flagOp <|> flaglessOp <|> fail "Expected comparison operator (don't wrap commands in []/[[]])"@@ -645,6 +650,7 @@ prop_a20= isOk readArithmeticContents "a ? b ? c : d : e" prop_a21= isOk readArithmeticContents "a ? b : c ? d : e" prop_a22= isOk readArithmeticContents "!!a"+readArithmeticContents :: Monad m => SCParser m Token readArithmeticContents = readSequence where@@ -658,12 +664,40 @@ id <- getNextId op <- choice (map (\x -> try $ do s <- string x- notFollowedBy2 $ oneOf "&|<>="+ failIfIncompleteOp return s ) op) spacing return $ token id op + failIfIncompleteOp = notFollowedBy2 $ oneOf "&|<>="++ -- Read binary minus, but also check for -lt, -gt and friends:+ readMinusOp = do+ id <- getNextId+ pos <- getPosition+ try $ do+ char '-'+ failIfIncompleteOp+ optional $ do+ (str, alt) <- lookAhead . choice $ map tryOp [+ ("lt", "<"),+ ("gt", ">"),+ ("le", "<="),+ ("ge", ">="),+ ("eq", "=="),+ ("ne", "!=")+ ]+ parseProblemAt pos ErrorC 1106 $ "In arithmetic contexts, use " ++ alt ++ " instead of -" ++ str+ spacing+ return $ TA_Binary id "-"+ where+ tryOp (str, alt) = try $ do+ string str+ spacing1+ return (str, alt)++ readArrayIndex = do id <- getNextId char '['@@ -733,7 +767,7 @@ readEquated = readCompared `splitBy` ["==", "!="] readCompared = readShift `splitBy` ["<=", ">=", "<", ">"] readShift = readAddition `splitBy` ["<<", ">>"]- readAddition = readMultiplication `splitBy` ["+", "-"]+ readAddition = chainl1 readMultiplication (readBinary ["+"] <|> readMinusOp) readMultiplication = readExponential `splitBy` ["*", "/", "%"] readExponential = readAnyNegated `splitBy` ["**"] @@ -810,7 +844,7 @@ pos <- getPosition space <- allspacing when (null space) $- parseProblemAt pos ErrorC 1035 $ "You need a space after the " +++ parseProblemAtWithEnd opos pos ErrorC 1035 $ "You need a space after the " ++ if single then "[ and before the ]." else "[[ and before the ]]."@@ -835,10 +869,11 @@ prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n" prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n" prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n"+prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n" readAnnotation = called "shellcheck annotation" $ do try readAnnotationPrefix many1 linewhitespace- values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride)+ values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey) linefeed many linewhitespace return $ concat values@@ -870,6 +905,13 @@ many linewhitespace return value + anyKey = do+ pos <- getPosition+ anyChar `reluctantlyTill1` whitespace+ many linewhitespace+ parseNoteAt pos WarningC 1107 "This directive is unknown. It will be ignored."+ return []+ readAnnotations = do annotations <- many (readAnnotation `thenSkip` allspacing) return $ concat annotations@@ -897,6 +939,20 @@ checkPossibleTermination pos x return $ T_NormalWord id x +readIndexSpan = do+ id <- getNextId+ x <- many (readNormalWordPart "]" <|> someSpace <|> otherLiteral)+ return $ T_NormalWord id x+ where+ someSpace = do+ id <- getNextId+ str <- spacing1+ return $ T_Literal id str+ otherLiteral = do+ id <- getNextId+ str <- many1 $ oneOf quotableChars+ return $ T_Literal id str+ checkPossibleTermination pos [T_Literal _ x] = when (x `elem` ["do", "done", "then", "fi", "esac"]) $ parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."@@ -1060,13 +1116,18 @@ setPosition lastPosition return result -inSeparateContext parser = do+-- Parse something, but forget all parseProblems+inSeparateContext = parseForgettingContext True+-- Parse something, but forget all parseProblems on failure+forgetOnFailure = parseForgettingContext False++parseForgettingContext alsoOnSuccess parser = do context <- Ms.get success context <|> failure context where success c = do res <- try parser- Ms.put c+ when alsoOnSuccess $ Ms.put c return res failure c = do Ms.put c@@ -1303,7 +1364,17 @@ prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))" prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)"-readDollarExpression = readTripleParenthesis "$" readDollarArithmetic readDollarExpansion <|> readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable+prop_readDollarExpression3 = isWarning readDollarExpression "$((\"$@\" &); foo;)"+readDollarExpression :: Monad m => SCParser m Token+readDollarExpression = do+ -- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but+ -- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char.+ -- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%:+ lookAhead $ char '$'+ arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable+ where+ arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->+ parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.") prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'" readDollarSingleQuote = called "$'..' expression" $ do@@ -1349,25 +1420,20 @@ string "))" return (T_Arithmetic id c) --- Check if maybe ((( was intended as ( (( rather than (( (-readTripleParenthesis prefix expected alternative = do- pos <- try . lookAhead $ do- string prefix- p <- getPosition- string "(((" -- should optimally be "((" but it's noisy and rarely helpful- return p-+-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used+readAmbiguous :: Monad m => String -> SCParser m p -> SCParser m p -> (SourcePos -> SCParser m ()) -> SCParser m p+readAmbiguous prefix expected alternative warner = do+ pos <- getPosition+ try . lookAhead $ string prefix -- If the expected parser fails, try the alt. -- If the alt fails, run the expected one again for the errors.- try expected <|> tryAlt pos <|> expected+ try expected <|> try (withAlt pos) <|> expected where- tryAlt pos = do- t <- try alternative- parseNoteAt pos WarningC 1102 $- "Shells differ in parsing ambiguous " ++ prefix ++ "(((. Use spaces: " ++ prefix ++ "( (( ."+ withAlt pos = do+ t <- forgetOnFailure alternative+ warner pos return t - prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }" prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}" readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do@@ -1481,7 +1547,7 @@ -- add empty tokens for now, read the rest in readPendingHereDocs let doc = T_HereDoc hid dashed quoted endToken [] addPendingHereDoc doc- return $ T_FdRedirect fid "" doc+ return doc where stripLiteral (T_Literal _ x) = x stripLiteral (T_SingleQuoted _ x) = x@@ -1552,43 +1618,45 @@ readFilename = readNormalWord-readIoFileOp = choice [g_LESSAND, g_GREATAND, g_DGREAT, g_LESSGREAT, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ]+readIoFileOp = choice [g_DGREAT, g_LESSGREAT, g_GREATAND, g_LESSAND, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ] +readIoDuplicate = try $ do+ id <- getNextId+ op <- g_GREATAND <|> g_LESSAND+ target <- readIoVariable <|> many1 digit <|> string "-"+ return $ T_IoDuplicate id op target+ prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\"" readIoFile = called "redirection" $ do id <- getNextId op <- readIoFileOp spacing file <- readFilename- return $ T_FdRedirect id "" $ T_IoFile id op file+ return $ T_IoFile id op file readIoVariable = try $ do char '{' x <- readVariableName char '}'- lookAhead readIoFileOp return $ "{" ++ x ++ "}" -readIoNumber = try $ do- x <- many1 digit <|> string "&"- lookAhead readIoFileOp+readIoSource = try $ do+ x <- string "&" <|> readIoVariable <|> many digit+ lookAhead $ void readIoFileOp <|> void (string "<<") return x -prop_readIoNumberRedirect = isOk readIoNumberRedirect "3>&2"-prop_readIoNumberRedirect2 = isOk readIoNumberRedirect "2> lol"-prop_readIoNumberRedirect3 = isOk readIoNumberRedirect "4>&-"-prop_readIoNumberRedirect4 = isOk readIoNumberRedirect "&> lol"-prop_readIoNumberRedirect5 = isOk readIoNumberRedirect "{foo}>&2"-prop_readIoNumberRedirect6 = isOk readIoNumberRedirect "{foo}<&-"-readIoNumberRedirect = do+prop_readIoRedirect = isOk readIoRedirect "3>&2"+prop_readIoRedirect2 = isOk readIoRedirect "2> lol"+prop_readIoRedirect3 = isOk readIoRedirect "4>&-"+prop_readIoRedirect4 = isOk readIoRedirect "&> lol"+prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2"+prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-"+readIoRedirect = do id <- getNextId- n <- readIoVariable <|> readIoNumber- op <- readHereString <|> readHereDoc <|> readIoFile- let actualOp = case op of T_FdRedirect _ "" x -> x+ n <- readIoSource+ redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile spacing- return $ T_FdRedirect id n actualOp--readIoRedirect = choice [ readIoNumberRedirect, readHereString, readHereDoc, readIoFile ] `thenSkip` spacing+ return $ T_FdRedirect id n redir readRedirectList = many1 readIoRedirect @@ -1599,7 +1667,7 @@ spacing id2 <- getNextId word <- readNormalWord- return $ T_FdRedirect id "" $ T_HereString id2 word+ return $ T_HereString id2 word readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) readLineBreak = optional readNewlineList@@ -1878,7 +1946,7 @@ pos <- getPosition correctElif <- elif unless correctElif $- parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'."+ parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)." allspacing condition <- readTerm @@ -2176,8 +2244,8 @@ id <- getNextId cmd <- choice [ readBraceGroup,- readTripleParenthesis "" readArithmeticExpression readSubshell,- readArithmeticExpression,+ readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->+ parseNoteAt pos WarningC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."), readSubshell, readCondition, readWhileClause,@@ -2251,7 +2319,7 @@ -- Get whatever a parser would parse as a string readStringForParser parser = do- pos <- lookAhead (parser >> getPosition)+ pos <- inSeparateContext $ lookAhead (parser >> getPosition) readUntil pos where readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))@@ -2278,7 +2346,7 @@ variable <- readVariableName optional (readNormalDollar >> parseNoteAt pos ErrorC 1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")- index <- optionMaybe readArrayIndex+ indices <- many readArrayIndex hasLeftSpace <- liftM (not . null) spacing pos <- getPosition op <- readAssignmentOp@@ -2290,13 +2358,13 @@ parseNoteAt pos WarningC 1007 "Remove space after = if trying to assign a value (for empty string, use var='' ... )." value <- readEmptyLiteral- return $ T_Assignment id op variable index value+ return $ T_Assignment id op variable indices value else do when (hasLeftSpace || hasRightSpace) $ parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments." value <- readArray <|> readNormalWord spacing- return $ T_Assignment id op variable index value+ return $ T_Assignment id op variable indices value where readAssignmentOp = do pos <- getPosition@@ -2316,12 +2384,14 @@ return $ T_Literal id "" readArrayIndex = do+ id <- getNextId char '['- optional space- x <- readArithmeticContents+ pos <- getPosition+ str <- readStringForParser readIndexSpan char ']'- return x+ return $ T_UnparsedIndex id pos str +readArray :: Monad m => SCParser m Token readArray = called "array assignment" $ do id <- getNextId char '('@@ -2334,7 +2404,7 @@ readIndexed = do id <- getNextId index <- try $ do- x <- readArrayIndex+ x <- many1 readArrayIndex char '=' return x value <- readNormalWord <|> nothing@@ -2477,12 +2547,12 @@ try (lookAhead p) action -prop_readScript1 = isOk readScript "#!/bin/bash\necho hello world\n"-prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"-prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"-prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("-prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"-readScript = do+prop_readScript1 = isOk readScriptFile "#!/bin/bash\necho hello world\n"+prop_readScript2 = isWarning readScriptFile "#!/bin/bash\r\necho hello world\n"+prop_readScript3 = isWarning readScriptFile "#!/bin/bash\necho hello\xA0world"+prop_readScript4 = isWarning readScriptFile "#!/usr/bin/perl\nfoo=("+prop_readScript5 = isOk readScriptFile "#!/bin/bash\n#This is an empty script\n\n"+readScriptFile = do id <- getNextId pos <- getPosition optional $ do@@ -2497,7 +2567,8 @@ annotations <- readAnnotations commands <- withAnnotations annotations readCompoundListOrEmpty verifyEof- return $ T_Annotation annotationId annotations $ T_Script id sb commands+ let script = T_Annotation annotationId annotations $ T_Script id sb commands+ reparseIndices script else do many anyChar return $ T_Script id sb []@@ -2549,6 +2620,9 @@ readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF" +readScript = do+ script <- readScriptFile+ reparseIndices script isWarning p s = parsesCleanly p s == Just False isOk p s = parsesCleanly p s == Just True@@ -2571,13 +2645,15 @@ state <- getState return (item, state) -compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2)+compareNotes (ParseNote pos1 pos1' level1 _ s1) (ParseNote pos2 pos2' level2 _ s2) = compare (pos1, pos1', level1) (pos2, pos2', level2) sortNotes = sortBy compareNotes makeErrorFor parsecError =- ParseNote (errorPos parsecError) ErrorC 1072 $+ ParseNote pos pos ErrorC 1072 $ getStringFromParsec $ errorMessages parsecError+ where+ pos = errorPos parsecError getStringFromParsec errors = case map f errors of@@ -2630,11 +2706,46 @@ isName (ContextName _ _) = True isName _ = False notesForContext list = zipWith ($) [first, second] $ filter isName list- first (ContextName pos str) = ParseNote pos ErrorC 1073 $+ first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $ "Couldn't parse this " ++ str ++ "."- second (ContextName pos str) = ParseNote pos InfoC 1009 $+ second (ContextName pos str) = ParseNote pos pos InfoC 1009 $ "The mentioned parser error was in this " ++ str ++ "." +-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text+-- depending on declare -A statements.+reparseIndices root =+ analyze blank blank f root+ where+ associative = getAssociativeArrays root+ isAssociative s = s `elem` associative+ f (T_Assignment id mode name indices value) = do+ newIndices <- mapM (fixAssignmentIndex name) indices+ newValue <- case value of+ (T_Array id2 words) -> do+ newWords <- mapM (fixIndexElement name) words+ return $ T_Array id2 newWords+ x -> return x+ return $ T_Assignment id mode name newIndices newValue+ f t = return t++ fixIndexElement name word =+ case word of+ T_IndexedElement id indices value -> do+ new <- mapM (fixAssignmentIndex name) indices+ return $ T_IndexedElement id new value+ otherwise -> return word++ fixAssignmentIndex name word =+ case word of+ T_UnparsedIndex id pos src -> do+ parsed name pos src+ otherwise -> return word++ parsed name pos src =+ if isAssociative name+ then subParse pos (called "associative array index" $ readIndexSpan) src+ else subParse pos (called "arithmetic array index expression" $ optional space >> readArithmeticContents) src+ reattachHereDocs root map = doTransform f root where@@ -2644,8 +2755,8 @@ f t = t toPositionedComment :: ParseNote -> PositionedComment-toPositionedComment (ParseNote pos severity code message) =- PositionedComment (posToPos pos) $ Comment severity code message+toPositionedComment (ParseNote start end severity code message) =+ PositionedComment (posToPos start) (posToPos end) $ Comment severity code message posToPos :: SourcePos -> Position posToPos sp = Position {