packages feed

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 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 {