packages feed

ShellCheck 0.3.5 → 0.3.6

raw patch · 12 files changed

+798/−261 lines, 12 filesdep ~basesetup-changedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base

API changes (from Hackage documentation)

- ShellCheck.AST: NormalForIn :: ForInType
- ShellCheck.AST: ShortForIn :: ForInType
- ShellCheck.AST: T_ :: Id -> [Token] -> Token
- ShellCheck.AST: data ForInType
- ShellCheck.AST: instance Eq ForInType
- ShellCheck.AST: instance Show ForInType
- ShellCheck.Options: Zsh :: Shell
+ ShellCheck.AST: T_CoProc :: Id -> (Maybe String) -> Token -> Token
+ ShellCheck.AST: T_CoProcBody :: Id -> Token -> Token
+ ShellCheck.Analytics: instance Eq DataType
+ ShellCheck.Analytics: instance Show DataType
- ShellCheck.AST: T_ForIn :: Id -> ForInType -> [String] -> [Token] -> [Token] -> Token
+ ShellCheck.AST: T_ForIn :: Id -> String -> [Token] -> [Token] -> Token
- ShellCheck.Parser: parseShell :: SourceName -> String -> ParseResult
+ ShellCheck.Parser: parseShell :: AnalysisOptions -> SourceName -> String -> ParseResult

Files

README.md view
@@ -2,7 +2,7 @@  http://www.shellcheck.net -Copyright 2012-2014, Vidar 'koala_man' Holen+Copyright 2012-2015, Vidar 'koala_man' Holen Licensed under the GNU Affero General Public License, v3  The goals of ShellCheck are:
Setup.hs view
@@ -1,2 +1,43 @@-import Distribution.Simple-main = defaultMain+import Distribution.PackageDescription (+  HookedBuildInfo,+  emptyHookedBuildInfo )+import Distribution.Simple (+  Args,+  UserHooks ( preSDist ),+  defaultMainWithHooks,+  simpleUserHooks )+import Distribution.Simple.Setup ( SDistFlags )++-- | This requires the process package from,+--+--   https://hackage.haskell.org/package/process+--+import System.Process ( callCommand )+++-- | This will use almost the default implementation, except we switch+--   out the default pre-sdist hook with our own, 'myPreSDist'.+--+main = defaultMainWithHooks myHooks+  where+    myHooks = simpleUserHooks { preSDist = myPreSDist }+++-- | This hook will be executed before e.g. @cabal sdist@. It runs+--   pandoc to create the man page from shellcheck.1.md. If the pandoc+--   command is not found, this will fail with an error message:+--+--     /bin/sh: pandoc: command not found+--+--   Since the man page is listed in the Extra-Source-Files section of+--   our cabal file, a failure here should result in a failure to+--   create the distribution tarball (that's a good thing).+--+myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo+myPreSDist _ _ = do+  putStrLn "Building the man page..."+  putStrLn pandoc_cmd+  callCommand pandoc_cmd+  return emptyHookedBuildInfo+  where+    pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name:             ShellCheck-Version:          0.3.5+Version:          0.3.6 Synopsis:         Shell script analysis tool License:          AGPL-3 License-file:     LICENSE@@ -26,6 +26,8 @@     -- documentation     README.md     shellcheck.1.md+    -- built with a cabal sdist hook+    shellcheck.1     -- tests     test/shellcheck.hs 
ShellCheck/AST.hs view
@@ -28,7 +28,6 @@ data AssignmentMode = Assign | Append deriving (Show, Eq) data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq) data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)-data ForInType = NormalForIn | ShortForIn deriving (Show, Eq) data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)  data Token =@@ -49,7 +48,6 @@     | T_Arithmetic Id Token     | T_Array Id [Token]     | T_IndexedElement Id Token Token-    | T_ Id [Token]     | T_Assignment Id AssignmentMode String (Maybe Token) Token     | T_Backgrounded Id Token     | T_Backticked Id [Token]@@ -83,7 +81,7 @@     | T_Fi Id     | T_For Id     | T_ForArithmetic Id Token Token Token [Token]-    | T_ForIn Id ForInType [String] [Token] [Token]+    | T_ForIn Id String [Token] [Token]     | T_Function Id FunctionKeyword FunctionParentheses String Token     | T_GREATAND Id     | T_Glob Id String@@ -123,6 +121,8 @@     | T_WhileExpression Id [Token] [Token]     | T_Annotation Id [Annotation] Token     | T_Pipe Id String+    | T_CoProc Id (Maybe String) Token+    | T_CoProcBody Id Token     deriving (Show)  data Annotation = DisableComment Integer deriving (Show, Eq)@@ -205,7 +205,7 @@     delve (T_BraceGroup id l) = dl l $ T_BraceGroup id     delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id     delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id-    delve (T_ForIn id t v w l) = dll w l $ T_ForIn id t v+    delve (T_ForIn id v w l) = dll w l $ T_ForIn id v     delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v     delve (T_CaseExpression id word cases) = do         newWord <- round word@@ -248,6 +248,8 @@     delve (TA_Expansion id t) = dl t $ TA_Expansion id     delve (TA_Index id t) = d1 t $ TA_Index id     delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns+    delve (T_CoProc id var body) = d1 body $ T_CoProc id var+    delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id     delve t = return t  getId t = case t of@@ -312,7 +314,7 @@         T_BraceGroup id _  -> id         T_WhileExpression id _ _  -> id         T_UntilExpression id _ _  -> id-        T_ForIn id _ _ _ _  -> id+        T_ForIn id _ _ _  -> id         T_SelectIn id _ _ _  -> id         T_CaseExpression id _ _ -> id         T_Function id _ _ _ _  -> id@@ -341,6 +343,8 @@         T_DollarBracket id _ -> id         T_Annotation id _ _ -> id         T_Pipe id _ -> id+        T_CoProc id _ _ -> id+        T_CoProcBody id _ -> id  blank :: Monad m => Token -> m () blank = const $ return ()
ShellCheck/Analytics.hs view
@@ -27,6 +27,7 @@ import Data.Function (on) import Data.List import Data.Maybe+import Data.Ord import Debug.Trace import ShellCheck.AST import ShellCheck.Options@@ -34,7 +35,8 @@ import ShellCheck.Parser hiding (runTests) import Text.Regex import qualified Data.Map as Map-import Test.QuickCheck.All (quickCheckAll)+import Test.QuickCheck.All (forAllProperties)+import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)  data Parameters = Parameters {     variableFlow :: [StackData],@@ -58,6 +60,7 @@     ,checkUnpassedInFunctions     ,checkArrayWithoutIndex     ,checkShebang+    ,checkUnassignedReferences     ]  checksFor Sh = [@@ -68,10 +71,6 @@ checksFor Ksh = [     checkEchoSed     ]-checksFor Zsh = [-    checkTimeParameters-    ,checkEchoSed-    ] checksFor Bash = [     checkTimeParameters     ,checkBraceExpansionVars@@ -113,7 +112,6 @@ shellForExecutable "ksh88" = return Ksh shellForExecutable "ksh93" = return Ksh -shellForExecutable "zsh" = return Zsh shellForExecutable "bash" = return Bash shellForExecutable _ = Nothing @@ -202,6 +200,9 @@     ,checkConcatenatedDollarAt     ,checkFindActionPrecedence     ,checkTildeInPath+    ,checkFindExecWithSingleArgument+    ,checkReturn+    ,checkMaskedReturns     ]  @@ -329,6 +330,8 @@ deadSimple (T_Redirecting _ _ foo) = deadSimple foo deadSimple (T_DollarSingleQuoted _ s) = [s] deadSimple (T_Annotation _ _ s) = deadSimple s+-- Workaround for let "foo = bar" parsing+deadSimple (TA_Sequence _ [TA_Expansion _ v]) = concatMap deadSimple v deadSimple _ = []  -- Turn a SimpleCommand foo -avz --bar=baz into args ["a", "v", "z", "bar"]@@ -340,7 +343,7 @@     flag ('-':args) = map (:[]) args     flag _ = [] -getFlags _ = []+getFlags _ = error "Internal shellcheck error, please report! (getFlags on non-command)"  (!!!) list i =     case drop i list of@@ -360,11 +363,34 @@ verifyNotTree f s = checkTree f s == Just False  checkNode f = checkTree (runNodeAnalysis f)-checkTree f s = case parseShell "-" s of+checkTree f s = case parseShell defaultAnalysisOptions "-" s of         (ParseResult (Just (t, m)) _) -> Just . not . null $ runList defaultAnalysisOptions t [f]         _ -> Nothing  +-- Copied from https://wiki.haskell.org/Edit_distance+dist :: Eq a => [a] -> [a] -> Int+dist a b+    = last (if lab == 0 then mainDiag+            else if lab > 0 then lowers !! (lab - 1)+                 else{- < 0 -}   uppers !! (-1 - lab))+    where mainDiag = oneDiag a b (head uppers) (-1 : head lowers)+          uppers = eachDiag a b (mainDiag : uppers) -- upper diagonals+          lowers = eachDiag b a (mainDiag : lowers) -- lower diagonals+          eachDiag a [] diags = []+          eachDiag a (bch:bs) (lastDiag:diags) = oneDiag a bs nextDiag lastDiag : eachDiag a bs diags+              where nextDiag = head (tail diags)+          oneDiag a b diagAbove diagBelow = thisdiag+              where doDiag [] b nw n w = []+                    doDiag a [] nw n w = []+                    doDiag (ach:as) (bch:bs) nw n w = me : (doDiag as bs me (tail n) (tail w))+                        where me = if ach == bch then nw else 1 + min3 (head w) nw (head n)+                    firstelt = 1 + head diagBelow+                    thisdiag = firstelt : doDiag a b firstelt diagAbove (tail diagBelow)+          lab = length a - length b+          min3 x y z = if x < y then x else min y z++ prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)" checkEchoWc _ (T_Pipeline id _ [a, b]) =     when (acmd == ["echo", "${VAR}"]) $@@ -594,7 +620,7 @@ prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh  -l " prop_checkShebang3 = verifyTree checkShebang "ls -l" checkShebang params (T_Script id sb _) =-    [Note id InfoC 2148 $ "Shebang (#!) missing. Assuming " ++ (show $ shellType params) ++ "."+    [Note id ErrorC 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)"@@ -615,6 +641,7 @@ prop_checkBashisms16= verify checkBashisms "echo $RANDOM" prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))" prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"+prop_checkBashisms19= verify checkBashisms "foo > file*.txt" checkBashisms _ = bashism   where     errMsg id s = err id 2040 $ "In sh, " ++ s ++ " not supported, even when sh is actually bash."@@ -665,6 +692,10 @@         warnMsg id "|& in place of 2>&1 | is"     bashism (T_Array id _) =         warnMsg id "arrays are"+    bashism (T_IoFile id _ t) | isGlob t =+        warnMsg id "redirecting to/from globs is"+    bashism (T_CoProc id _ _) =+        warnMsg id "coproc is"      bashism _ = return () @@ -687,17 +718,17 @@ prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done" prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done" prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done"-checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =+checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =     when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list             || (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $         err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once."-checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [T_SingleQuoted id s]] _) =+checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id s]] _) =     warn id 2041 $ "This is a literal string. To run as a command, use $(" ++ s ++ ")."-checkForInQuoted _ (T_ForIn _ _ f [T_NormalWord _ [T_Literal id s]] _) =+checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) =     if ',' `elem` s       then unless ('{' `elem` s) $             warn id 2042 "Use spaces, not commas, to separate loop elements."-      else warn id 2043 $ "This loop will only run once, with " ++ head f ++ "='" ++ s ++ "'."+      else warn id 2043 $ "This loop will only run once, with " ++ f ++ "='" ++ s ++ "'." checkForInQuoted _ _ = return ()  prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"@@ -705,7 +736,7 @@ prop_checkForInCat2 = verify checkForInCat "for f in $(cat foo | grep lol); do stuff; done" prop_checkForInCat2a= verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done" prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done"-checkForInCat _ (T_ForIn _ _ f [T_NormalWord _ w] _) = mapM_ checkF w+checkForInCat _ (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w   where     checkF (T_DollarExpansion id [T_Pipeline _ _ r])         | all isLineBased r =@@ -721,9 +752,9 @@ prop_checkForInLs3 = verify checkForInLs "for f in `find / -name '*.mp3'`; do mplayer \"$f\"; done" checkForInLs _ = try   where-   try (T_ForIn _ _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) =+   try (T_ForIn _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) =         check id f x-   try (T_ForIn _ _ f [T_NormalWord _ [T_Backticked id [x]]] _) =+   try (T_ForIn _ f [T_NormalWord _ [T_Backticked id [x]]] _) =         check id f x    try _ = return ()    check id f x =@@ -814,7 +845,10 @@             addNote $ note newId             addNote $ note exceptId     checkOccurrences _ _ = return ()-    getAllRedirs = concatMap (\(T_Redirecting _ ls _) -> concatMap getRedirs ls)+    getAllRedirs = concatMap (\t ->+        case t of+            T_Redirecting _ ls _ -> concatMap getRedirs ls+            _ -> [])     getRedirs (T_FdRedirect _ _ (T_IoFile _ op file)) =             case op of T_Greater _ -> [file]                        T_Less _    -> [file]@@ -910,6 +944,8 @@  prop_checkArrayWithoutIndex1 = verifyTree checkArrayWithoutIndex "foo=(a b); echo $foo" prop_checkArrayWithoutIndex2 = verifyNotTree checkArrayWithoutIndex "foo='bar baz'; foo=($foo); echo ${foo[0]}"+prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo"+prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC" checkArrayWithoutIndex params _ =     concat $ doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)   where@@ -922,7 +958,7 @@                     "Expanding an array without an index only gives the first element."]     readF _ _ _ = return [] -    writeF _ t name (DataFrom [T_Array {}]) = do+    writeF _ t name (DataArray _) = do         modify (Map.insert name t)         return []     writeF _ _ name _ = do@@ -1027,7 +1063,6 @@ prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]" prop_checkNumberComparisons8 = verify checkNumberComparisons "[[ foo <= bar ]]" prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"-prop_checkNumberComparisons10= verify checkNumberComparisons "#!/bin/zsh -x\n[ foo >= bar ]]" prop_checkNumberComparisons11= verify checkNumberComparisons "[[ $foo -eq 'N' ]]" prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]" checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do@@ -1056,9 +1091,7 @@       isLtGt = flip elem ["<", "\\<", ">", "\\>"]       isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="] -      supportsDecimals =-        let sh = shellType params in-            sh == Ksh || sh == Zsh+      supportsDecimals = (shellType params) == Ksh       checkDecimals hs =         when (isFraction hs && not supportsDecimals) $             err (getId hs) 2072 decimalError@@ -1359,11 +1392,12 @@             T_Redirecting {} -> return $                 if strict then False else                     -- Not true, just a hack to prevent warning about non-expansion refs-                    any (isCommand t) ["local", "declare", "typeset", "export", "trap"]+                    any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]             T_DoubleQuoted _ _ -> return True             T_DollarDoubleQuoted _ _ -> return True             T_CaseExpression {} -> return True             T_HereDoc {} -> return True+            T_HereString {} -> return True             T_DollarBraced {} -> return True             -- When non-strict, pragmatically assume it's desirable to split here             T_ForIn {} -> return (not strict)@@ -1516,6 +1550,7 @@ prop_checkUuoeVar5 = verify checkUuoeVar "foo \"$(echo \"$(date) value:\" $value)\"" prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\"" prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005+prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)" checkUuoeVar _ p =     case p of         T_Backticked id [cmd] -> check id cmd@@ -1533,10 +1568,13 @@     check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c     check _ _ = return ()     isCovered first rest = null rest && tokenIsJustCommandOutput first-    warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars@(first:rest) ->-        unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $-            when (all couldBeOptimized vars) $ style id 2116-                "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."+    warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars ->+        case vars of+          (first:rest) ->+            unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $+                when (all couldBeOptimized vars) $ style id 2116+                    "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."+          otherwise -> return ()  prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"@@ -1681,6 +1719,35 @@     special file = concat (deadSimple file) == "/dev/null" checkSudoRedirect _ _ = return () +prop_checkReturn1 = verifyNot checkReturn "return"+prop_checkReturn2 = verifyNot checkReturn "return 1"+prop_checkReturn3 = verifyNot checkReturn "return $var"+prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"+prop_checkReturn5 = verify checkReturn "return -1"+prop_checkReturn6 = verify checkReturn "return 1000"+prop_checkReturn7 = verify checkReturn "return 'hello world'"+checkReturn _ = checkCommand "return" (const f)+  where+    f (first:second:_) =+        err (getId second) 2151+            "Only one integer 0-255 can be returned. Use stdout for other data."+    f [value] =+        when (isInvalid $ literal value) $+            err (getId value) 2152+                "Can only return 0-255. Other data should be written to stdout."+    f _ = return ()++    isInvalid s = s == "" || any (not . isDigit) s || length s > 5+        || let value = (read s :: Integer) in value > 255++    literal token = fromJust $ getLiteralStringExt lit token+    lit (T_DollarBraced {}) = return "0"+    lit (T_DollarArithmetic {}) = return "0"+    lit (T_DollarExpansion {}) = return "0"+    lit (T_Backticked {}) = return "0"+    lit _ = return "WTF"++ prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"@@ -1707,7 +1774,7 @@ prop_checkBackticks1 = verify checkBackticks "echo `foo`" prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)" checkBackticks _ (T_Backticked id _) =-    style id 2006 "Use $(..) instead of deprecated `..`"+    style id 2006 "Use $(..) instead of legacy `..`." checkBackticks _ _ = return ()  prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"@@ -1739,20 +1806,32 @@ prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --user-agent='something'" prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\"" prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""+prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\"" checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)   where     check (T_SingleQuoted _ _:T_Literal id str:_)         | all isAlphaNum str =         info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? " -    check (T_DoubleQuoted _ _:trapped:T_DoubleQuoted _ _:_) =+    check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) =         case trapped of             T_DollarExpansion id _ -> warnAboutExpansion id             T_DollarBraced id _ -> warnAboutExpansion id-            T_Literal id s -> unless (s == "/" || s == "=") $ warnAboutLiteral id+            T_Literal id s ->+                unless (quotesSingleThing a && quotesSingleThing b) $+                    warnAboutLiteral id             _ -> return ()      check _ = return ()++    -- If the surrounding quotes quote single things, like "$foo"_and_then_some_"$stuff",+    -- the quotes were probably intentional and harmless.+    quotesSingleThing x = case x of+        [T_DollarExpansion _ _] -> True+        [T_DollarBraced _ _] -> True+        [T_Backticked _ _] -> True+        _ -> False+     warnAboutExpansion id =         warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them."     warnAboutLiteral id =@@ -1766,7 +1845,7 @@ prop_checkTildeInQuotes6 = verifyNot checkTildeInQuotes "awk '$0 ~ /foo/'" checkTildeInQuotes _ = check   where-    verify id ('~':_) = warn id 2088 "Note that ~ does not expand in quotes."+    verify id ('~':'/':_) = warn id 2088 "Note that ~ does not expand in quotes."     verify _ _ = return ()     check (T_NormalWord _ (T_SingleQuoted id str:_)) =         verify id str@@ -1795,7 +1874,7 @@     doLists (T_BraceGroup _ cmds) = doList cmds     doLists (T_WhileExpression _ _ cmds) = doList cmds     doLists (T_UntilExpression _ _ cmds) = doList cmds-    doLists (T_ForIn _ _ _ _ cmds) = doList cmds+    doLists (T_ForIn _ _ _ cmds) = doList cmds     doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds     doLists (T_IfExpression _ thens elses) = do         mapM_ (\(_, l) -> doList l) thens@@ -1923,7 +2002,8 @@ prop_subshellAssignmentCheck12 = verifyTree subshellAssignmentCheck "cat /etc/passwd | while read line; do let ++n; done\necho $n" prop_subshellAssignmentCheck13 = verifyTree subshellAssignmentCheck "#!/bin/bash\necho foo | read bar; echo $bar" prop_subshellAssignmentCheck14 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh93\necho foo | read bar; echo $bar"-prop_subshellAssignmentCheck15 = verifyNotTree subshellAssignmentCheck "#!/bin/zsh\ncat foo | while read bar; do a=$bar; done\necho \"$a\""+prop_subshellAssignmentCheck15 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh\ncat foo | while read bar; do a=$bar; done\necho \"$a\""+prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@" subshellAssignmentCheck params t =     let flow = variableFlow params         check = findSubshelled flow [("oops",[])] Map.empty@@ -1935,20 +2015,27 @@     StackScope Scope     | StackScopeEnd     -- (Base expression, specific position, var name, assigned values)-    | Assignment (Token, Token, String, DataSource)+    | Assignment (Token, Token, String, DataType)     | Reference (Token, Token, String)   deriving (Show, Eq)-data DataSource = DataFrom [Token] | DataExternal++data DataType = DataString DataSource | DataArray DataSource   deriving (Show, Eq) +data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration+  deriving (Show, Eq)+ data VariableState = Dead Token String | Alive deriving (Show, Eq) +dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]+ leadType shell parents t =     case t of         T_DollarExpansion _ _  -> SubshellScope "$(..) expansion"         T_Backticked _ _  -> SubshellScope "`..` expansion"         T_Backgrounded _ _  -> SubshellScope "backgrounding &"         T_Subshell _ _  -> SubshellScope "(..) group"+        T_CoProcBody _ _  -> SubshellScope "coproc"         T_Redirecting {}  ->             if fromMaybe False causesSubshell             then SubshellScope "pipeline"@@ -1974,14 +2061,13 @@             Bash -> True             Sh -> True             Ksh -> False-            Zsh -> False  getModifiedVariables t =     case t of         T_SimpleCommand _ vars [] ->             concatMap (\x -> case x of-                                T_Assignment id _ name _ w ->-                                    [(x, x, name, DataFrom [w])]+                                T_Assignment id _ name _ w  ->+                                    [(x, x, name, dataTypeFrom DataString w)]                                 _ -> []                       ) vars         c@(T_SimpleCommand {}) ->@@ -1989,27 +2075,33 @@          TA_Unary _ "++|" var -> maybeToList $ do             name <- getLiteralString var-            return (t, t, name, DataFrom [t])+            return (t, t, name, DataString $ SourceFrom [t])         TA_Unary _ "|++" var -> maybeToList $ do             name <- getLiteralString var-            return (t, t, name, DataFrom [t])+            return (t, t, name, DataString $ SourceFrom [t])         TA_Binary _ op lhs rhs -> maybeToList $ do             guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]             name <- getLiteralString lhs-            return (t, t, name, DataFrom [rhs])+            return (t, t, name, DataString $ SourceFrom [rhs]) +        t@(T_CoProc _ name _) ->+            [(t, t, fromMaybe "COPROC" name, DataArray SourceExternal)]+         --Points to 'for' rather than variable-        T_ForIn id _ strs words _ -> map (\str -> (t, t, str, DataFrom words)) strs-        T_SelectIn id str words _ -> [(t, t, str, DataFrom words)]+        T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]+        T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]         _ -> []  -- Consider 'export/declare -x' a reference, since it makes the var available getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =     case x of-        "export" -> concatMap getReference rest-        "declare" -> if "x" `elem` getFlags base+        "export" -> if "f" `elem` flags+            then []+            else concatMap getReference rest+        "declare" -> if "x" `elem` flags             then concatMap getReference rest             else []+        "readonly" -> concatMap getReference rest         "trap" ->             case rest of                 head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head@@ -2019,6 +2111,7 @@     getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]     getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]     getReference _ = []+    flags = getFlags base  getReferencedVariableCommand _ = [] @@ -2028,16 +2121,30 @@         "read" ->             let params = map getLiteral rest in                 catMaybes . takeWhile isJust . reverse $ params+        "getopts" ->+            case rest of+                opts:var:_ -> maybeToList $ getLiteral var+                _ -> []+         "let" -> concatMap letParamToLiteral rest -        "export" -> concatMap getModifierParam rest-        "declare" -> concatMap getModifierParam rest-        "typeset" -> concatMap getModifierParam rest-        "local" -> concatMap getModifierParam rest-        "set" -> [(base, base, "@", DataFrom rest)]+        "export" ->+            if "f" `elem` flags then [] else concatMap getModifierParamString rest +        "declare" -> declaredVars+        "typeset" -> declaredVars++        "local" -> concatMap getModifierParamString rest+        "readonly" -> concatMap getModifierParamString rest+        "set" -> maybeToList $ do+            params <- getSetParams rest+            return (base, base, "@", DataString $ SourceFrom params)++        "printf" -> maybeToList $ getPrintfVariable rest+         _ -> []   where+    flags = getFlags base     stripEquals s = let rest = dropWhile (/= '=') s in         if rest == "" then "" else tail rest     stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =@@ -2046,24 +2153,72 @@         T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]     stripEqualsFrom t = t +    declaredVars = concatMap (getModifierParam defaultType) rest+      where+        defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString+     getLiteral t = do         s <- getLiteralString t         when ("-" `isPrefixOf` s) $ fail "argument"-        return (base, t, s, DataExternal)+        return (base, t, s, DataString SourceExternal) -    getModifierParam t@(T_Assignment _ _ name _ value) =-        [(base, t, name, DataFrom [value])]-    getModifierParam _ = []+    getModifierParamString = getModifierParam DataString +    getModifierParam def t@(T_Assignment _ _ name _ value) =+        [(base, t, name, dataTypeFrom def value)]+    getModifierParam def t@(T_NormalWord {}) = maybeToList $ do+        name <- getLiteralString t+        guard $ isVariableName name+        return (base, t, name, def SourceDeclaration)+    getModifierParam _ _ = []+     letParamToLiteral token =           if var == ""             then []-            else [(base, token, var, DataFrom [stripEqualsFrom token])]+            else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]         where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ deadSimple token++    getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest+    getSetParams (t:rest) =+        let s = getLiteralString t in+            case s of+                Just "--" -> return rest+                Just ('-':_) -> getSetParams rest+                _ -> return (t:fromMaybe [] (getSetParams rest))+    getSetParams [] = Nothing++    getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list+      where+        f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)+        f (_:rest) = f rest+        f [] = fail "not found"+ getModifiedVariableCommand _ = [] --- TODO:-getBracedReference s = takeWhile (`notElem` ":[#%/^,") $ dropWhile (`elem` "#!") s+prop_getBracedReference1 = getBracedReference "foo" == "foo"+prop_getBracedReference2 = getBracedReference "#foo" == "foo"+prop_getBracedReference3 = getBracedReference "#" == "#"+prop_getBracedReference4 = getBracedReference "##" == "#"+prop_getBracedReference5 = getBracedReference "#!" == "!"+prop_getBracedReference6 = getBracedReference "!#" == "#"+prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"+prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"+prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"+prop_getBracedReference10= getBracedReference "foo: -1" == "foo"+getBracedReference s = fromMaybe s $+    takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s+  where+    noPrefix = dropPrefix s+    dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest+    dropPrefix "" = ""+    takeName s = do+        let name = takeWhile isVariableChar s+        guard . not $ null name+        return name+    getSpecial (c:_) =+        if c `elem` "*@#?-$!" then return [c] else fail "not special"+    getSpecial _ = fail "empty"+ getIndexReferences s = fromMaybe [] $ do     (_, index, _, _) <- matchRegexAll re s     return $ matchAll variableNameRegex index@@ -2075,13 +2230,12 @@         T_DollarBraced id l -> let str = bracedString l in             (t, t, getBracedReference str) :                 map (\x -> (l, l, x)) (getIndexReferences str)-        TA_Expansion id _ -> maybeToList $ do-            str <- getLiteralStringExt literalizer t-            guard . not $ null str-            when (isDigit $ head str) $ fail "is a number"-            return (t, t, getBracedReference str)+        TA_Expansion id _ -> getIfReference t t         T_Assignment id mode str _ word ->             [(t, t, str) | mode == Append] ++ specialReferences str t word++        TC_Unary id _ "-v" token -> getIfReference t token+        TC_Unary id _ "-R" token -> getIfReference t token         x -> getReferencedVariableCommand x   where     -- Try to reduce false positives for unused vars only referenced from evaluated vars@@ -2098,6 +2252,12 @@     literalizer (TA_Index {}) = return ""  -- x[0] becomes a reference of x     literalizer _ = Nothing +    getIfReference context token = maybeToList $ do+            str <- getLiteralStringExt literalizer token+            guard . not $ null str+            when (isDigit $ head str) $ fail "is a number"+            return (context, token, getBracedReference str)+ -- Try to get referenced variables from a literal string like "$foo" -- Ignores tons of cases like arithmetic evaluation and array indices. prop_getVariablesFromLiteral1 =@@ -2143,12 +2303,15 @@ findSubshelled (Assignment x@(_, _, str, _):rest) ((reason,scope):lol) deadVars =     findSubshelled rest ((reason, x:scope):lol) $ Map.insert str Alive deadVars findSubshelled (Reference (_, readToken, str):rest) scopes deadVars = do-    case Map.findWithDefault Alive str deadVars of+    unless (shouldIgnore str) $ case Map.findWithDefault Alive str deadVars of         Alive -> return ()         Dead writeToken reason -> do                     info (getId writeToken) 2030 $ "Modification of " ++ str ++ " is local (to subshell caused by "++ reason ++")."                     info (getId readToken) 2031 $ str ++ " was modified in a subshell. That change might be lost."     findSubshelled rest scopes deadVars+  where+    shouldIgnore str =+        str `elem` ["@", "*", "IFS"]  findSubshelled (StackScope (SubshellScope reason):rest) scopes deadVars =     findSubshelled rest ((reason,[]):scopes) deadVars@@ -2178,19 +2341,22 @@ prop_checkSpacefulness6 = verifyTree checkSpacefulness "a=foo$(lol); echo $a" prop_checkSpacefulness7 = verifyTree checkSpacefulness "a=foo\\ bar; rm $a" prop_checkSpacefulness8 = verifyNotTree checkSpacefulness "a=foo\\ bar; a=foo; rm $a"-prop_checkSpacefulnessA = verifyTree checkSpacefulness "rm $1"-prop_checkSpacefulnessB = verifyTree checkSpacefulness "rm ${10//foo/bar}"-prop_checkSpacefulnessC = verifyNotTree checkSpacefulness "(( $1 + 3 ))"-prop_checkSpacefulnessD = verifyNotTree checkSpacefulness "if [[ $2 -gt 14 ]]; then true; fi"-prop_checkSpacefulnessE = verifyNotTree checkSpacefulness "foo=$3 env"-prop_checkSpacefulnessF = verifyNotTree checkSpacefulness "local foo=$1"-prop_checkSpacefulnessG = verifyNotTree checkSpacefulness "declare foo=$1"-prop_checkSpacefulnessH = verifyTree checkSpacefulness "echo foo=$1"-prop_checkSpacefulnessI = verifyNotTree checkSpacefulness "$1 --flags"-prop_checkSpacefulnessJ = verifyTree checkSpacefulness "echo $PWD"-prop_checkSpacefulnessK = verifyNotTree checkSpacefulness "n+='foo bar'"-prop_checkSpacefulnessL = verifyNotTree checkSpacefulness "select foo in $bar; do true; done"-prop_checkSpacefulnessM = verifyNotTree checkSpacefulness "echo $\"$1\""+prop_checkSpacefulness10= verifyTree checkSpacefulness "rm $1"+prop_checkSpacefulness11= verifyTree checkSpacefulness "rm ${10//foo/bar}"+prop_checkSpacefulness12= verifyNotTree checkSpacefulness "(( $1 + 3 ))"+prop_checkSpacefulness13= verifyNotTree checkSpacefulness "if [[ $2 -gt 14 ]]; then true; fi"+prop_checkSpacefulness14= verifyNotTree checkSpacefulness "foo=$3 env"+prop_checkSpacefulness15= verifyNotTree checkSpacefulness "local foo=$1"+prop_checkSpacefulness16= verifyNotTree checkSpacefulness "declare foo=$1"+prop_checkSpacefulness17= verifyTree checkSpacefulness "echo foo=$1"+prop_checkSpacefulness18= verifyNotTree checkSpacefulness "$1 --flags"+prop_checkSpacefulness19= verifyTree checkSpacefulness "echo $PWD"+prop_checkSpacefulness20= verifyNotTree checkSpacefulness "n+='foo bar'"+prop_checkSpacefulness21= verifyNotTree checkSpacefulness "select foo in $bar; do true; done"+prop_checkSpacefulness22= verifyNotTree checkSpacefulness "echo $\"$1\""+prop_checkSpacefulness23= verifyNotTree checkSpacefulness "a=(1); echo ${a[@]}"+prop_checkSpacefulness24= verifyNotTree checkSpacefulness "a='a b'; cat <<< $a"+prop_checkSpacefulness25= verifyTree checkSpacefulness "a='s/[0-9]//g'; sed $a"  checkSpacefulness params t =     doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)@@ -2208,23 +2374,25 @@         spaced <- hasSpaces name         return [Note (getId token) InfoC 2086 warning |                   spaced-                  && not ("@" `isPrefixOf` name) -- There's another warning for this+                  && not (isArrayExpansion token) -- There's another warning for this                   && not (isCounting token)                   && not (isQuoteFree parents token)                   && not (usedAsCommandName parents token)]       where         warning = "Double quote to prevent globbing and word splitting." -    writeF _ _ name DataExternal = do+    writeF _ _ name (DataString SourceExternal) = do         setSpaces name True         return [] -    writeF _ _ name (DataFrom vals) = do+    writeF _ _ name (DataString (SourceFrom vals)) = do         map <- get         setSpaces name             (isSpacefulWord (\x -> Map.findWithDefault True x map) vals)         return [] +    writeF _ _ _ _ = return []+     parents = parentMap params      isCounting (T_DollarBraced id token) =@@ -2249,10 +2417,9 @@           T_DoubleQuoted _ w -> isSpacefulWord spacefulF w           _ -> False       where-        globspace = "*? \t\n"+        globspace = "*?[] \t\n"         containsAny s = any (`elem` s) - prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param" prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param" prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""@@ -2273,7 +2440,7 @@     quoteRegex = mkRegex "\"|([/= ]|^)'|'( |$)|\\\\ "     containsQuotes s = s `matches` quoteRegex -    writeF _ _ name (DataFrom values) = do+    writeF _ _ name (DataString (SourceFrom values)) = do         quoteMap <- get         let quotedVars = msum $ map (forToken quoteMap) values         case quotedVars of@@ -2376,6 +2543,8 @@ prop_checkUnused19= verifyNotTree checkUnusedAssignments "a=1; let b=a+1; echo $b" prop_checkUnused20= verifyNotTree checkUnusedAssignments "a=1; PS1='$a'" prop_checkUnused21= verifyNotTree checkUnusedAssignments "a=1; trap 'echo $a' INT"+prop_checkUnused22= verifyNotTree checkUnusedAssignments "a=1; [ -v a ]"+prop_checkUnused23= verifyNotTree checkUnusedAssignments "a=1; [ -R a ]" checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)   where     flow = variableFlow params@@ -2392,12 +2561,104 @@     unused = Map.assocs $ Map.difference assignments references      warnFor (name, token) =-        info (getId token) 2034 $+        warn (getId token) 2034 $             name ++ " appears unused. Verify it or export it."      stripSuffix = takeWhile isVariableChar     defaultMap = Map.fromList $ zip internalVariables $ repeat () +prop_checkUnassignedReferences1 = verifyTree checkUnassignedReferences "echo $foo"+prop_checkUnassignedReferences2 = verifyNotTree checkUnassignedReferences "foo=hello; echo $foo"+prop_checkUnassignedReferences3 = verifyTree checkUnassignedReferences "MY_VALUE=3; echo $MYVALUE"+prop_checkUnassignedReferences4 = verifyNotTree checkUnassignedReferences "RANDOM2=foo; echo $RANDOM"+prop_checkUnassignedReferences5 = verifyNotTree checkUnassignedReferences "declare -A foo=([bar]=baz); echo ${foo[bar]}"+prop_checkUnassignedReferences6 = verifyNotTree checkUnassignedReferences "foo=..; echo ${foo-bar}"+prop_checkUnassignedReferences7 = verifyNotTree checkUnassignedReferences "getopts ':h' foo; echo $foo"+prop_checkUnassignedReferences8 = verifyNotTree checkUnassignedReferences "let 'foo = 1'; echo $foo"+prop_checkUnassignedReferences9 = verifyNotTree checkUnassignedReferences "echo ${foo-bar}"+prop_checkUnassignedReferences10= verifyNotTree checkUnassignedReferences "echo ${foo:?}"+prop_checkUnassignedReferences11= verifyNotTree checkUnassignedReferences "declare -A foo; echo \"${foo[@]}\""+prop_checkUnassignedReferences12= verifyNotTree checkUnassignedReferences "typeset -a foo; echo \"${foo[@]}\""+prop_checkUnassignedReferences13= verifyNotTree checkUnassignedReferences "f() { local foo; echo $foo; }"+prop_checkUnassignedReferences14= verifyNotTree checkUnassignedReferences "foo=; echo $foo"+prop_checkUnassignedReferences15= verifyNotTree checkUnassignedReferences "f() { true; }; export -f f"+prop_checkUnassignedReferences16= verifyNotTree checkUnassignedReferences "declare -A foo=( [a b]=bar ); echo ${foo[a b]}"+prop_checkUnassignedReferences17= verifyNotTree checkUnassignedReferences "USERS=foo; echo $USER"+prop_checkUnassignedReferences18= verifyNotTree checkUnassignedReferences "FOOBAR=42; export FOOBAR="+prop_checkUnassignedReferences19= verifyNotTree checkUnassignedReferences "readonly foo=bar; echo $foo"+prop_checkUnassignedReferences20= verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo"+prop_checkUnassignedReferences21= verifyTree checkUnassignedReferences "echo ${#foo}"+checkUnassignedReferences params t = warnings+  where+    (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)+    defaultAssigned = Map.fromList $ map (\a -> (a, ())) $ filter (not . null) internalVariables++    tally (Assignment (_, _, name, _))  =+        modify (\(read, written) -> (read, Map.insert name () written))+    tally (Reference (_, place, name)) =+        modify (\(read, written) -> (Map.insertWith' (const id) name place read, written))+    tally _ = return ()++    unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned+    writtenVars = filter isVariableName $ Map.keys writeMap++    getBestMatch var = do+        (match, score) <- listToMaybe best+        guard $ goodMatch var match score+        return match+      where+        matches = map (\x -> (x, match var x)) writtenVars+        best = sortBy (comparing snd) matches+        goodMatch var match score =+            let l = length match in+                l > 3 && score <= 1+                || l > 7 && score <= 2++    isLocal = any isLower++    warningForGlobals var place = do+        match <- getBestMatch var+        return $ warn (getId place) 2153 $+            "Possible misspelling: " ++ var ++ " may not be assigned, but " ++ match ++ " is."++    warningForLocals var place =+        return $ warn (getId place) 2154 $+            var ++ " is referenced but not assigned" ++ optionalTip ++ "."+      where+        optionalTip =+            if var `elem` commonCommands+            then " (for output from commands, use \"$(" ++ var ++ " ..." ++ ")\" )"+            else fromMaybe "" $ do+                    match <- getBestMatch var+                    return $ " (did you mean '" ++ match ++ "'?)"++    warningFor var place = do+        guard . not $ isInArray var place || isGuarded place+        (if isLocal var then warningForLocals else warningForGlobals) var place++    warnings = execWriter . sequence $ mapMaybe (uncurry warningFor) unassigned++    -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.+    -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.+    isInArray var t = any isArray $ getPath (parentMap params) t+      where+        isArray (T_Array {}) = True+        isArray (T_DollarBraced _ l) | var /= getBracedReference (bracedString l) = True+        isArray _ = False++    isGuarded (T_DollarBraced _ v) =+        any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]+      where+        name = concat $ deadSimple v+        rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") $ name+    isGuarded _ = False++    match var candidate =+        if var /= candidate && (map toLower var) == (map toLower candidate)+        then 1+        else dist var candidate++ prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt" prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*" prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"@@ -2512,7 +2773,7 @@ checkCdAndBack params = doLists   where     shell = shellType params-    doLists (T_ForIn _ _ _ _ cmds) = doList cmds+    doLists (T_ForIn _ _ _ cmds) = doList cmds     doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds     doLists (T_WhileExpression _ _ cmds) = doList cmds     doLists (T_UntilExpression _ _ cmds) = doList cmds@@ -2536,7 +2797,7 @@                warn (getId $ head cds) 2103 message      message =-        if shell == Bash || shell == Zsh+        if shell == Bash         then "Consider using ( subshell ), 'cd foo||exit', or pushd/popd instead."         else "Consider using ( subshell ) or 'cd foo||exit' instead." @@ -2576,7 +2837,6 @@         (T_Function id (FunctionKeyword hasKeyword) (FunctionParentheses hasParens) _ _) =     case shellType params of         Bash -> return ()-        Zsh -> return ()         Ksh ->             when (hasKeyword && hasParens) $                 err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."@@ -2588,46 +2848,50 @@ checkFunctionDeclarations _ _ = return ()  --- This is a lot of code for little gain. Consider whether it's worth it. prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"-prop_checkCatastrophicRm2 = verify checkCatastrophicRm "foo=$(echo bar); rm -r /home/$foo"-prop_checkCatastrophicRm3 = verify checkCatastrophicRm "foo=/home; user=$(whoami); rm -r \"$foo/$user\""-prop_checkCatastrophicRm4 = verifyNot checkCatastrophicRm "foo=/home; user=cow; rm -r \"$foo/$user\""-prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "user=$(whoami); rm -r /home/${user:?Nope}"+prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"+prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"+prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"+prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"-prop_checkCatastrophicRm7 = verifyNot checkCatastrophicRm "var=$(cmd); if [ -n \"$var\" ]; then rm -r /etc/$var/*; fi" prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home" prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"+prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"+prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =     when (any isRecursiveFlag simpleArgs) $         mapM_ checkWord tokens   where-    -- This ugly hack is based on the fact that ids generally increase-    relevantMap (Id n) = liftM snd . listToMaybe . dropWhile (\(Id x, _) ->  x > n) $ flowMapR-    flowMapR = reverse $ (\x -> zip (scanl getScopeId (Id 0) x) (scanl addNulls defaultMap x)) $ variableFlow params     simpleArgs = deadSimple t-    defaultMap = Map.fromList (map (\x -> (x, Nothing)) variablesWithoutSpaces)      checkWord token =         case getLiteralString token of             Just str ->                 when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $-                    info (getId token) 2114 "Obligatory typo warning. Use 'rm --' to disable this message."+                    warn (getId token) 2114 "Warning: deletes a system directory. Use 'rm --' to disable this message."             Nothing ->                 checkWord' token      checkWord' token = fromMaybe (return ()) $ do-        m <- relevantMap id-        filename <- combine m token+        filename <- getPotentialPath token         let path = fixPath filename         return . when (path `elem` importantPaths) $-            warn (getId token) 2115 $ "Make sure this never accidentally expands to '" ++ path ++ "'."+            warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."      fixPath filename =         let normalized = skipRepeating '/' . skipRepeating '*' $ filename in             if normalized == "/" then normalized else stripTrailing '/' normalized -    unnullable = all isVariableChar . concat . deadSimple+    getPotentialPath = getLiteralStringExt f+      where+        f (T_Glob _ str) = return str+        f (T_DollarBraced _ word) =+            let var = onlyLiteralString word in+                if any (flip isInfixOf var) [":?", ":-", ":="]+                then Nothing+                else return ""+        f _ = return ""+     isRecursiveFlag "--recursive" = True     isRecursiveFlag ('-':'-':_) = False     isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str@@ -2638,55 +2902,12 @@     skipRepeating c (a:r) = a:skipRepeating c r     skipRepeating _ [] = [] -    addNulls map (Reference (_, token, name)) =-        if mightBeGuarded token-        then Map.insert name Nothing map-        else map-    addNulls map (Assignment (_, token, name, DataExternal)) =-        if mightBeGuarded token-        then Map.insert name Nothing map-        else Map.insert name (Just "") map-    addNulls m (Assignment (_, token, name, DataFrom [word]))-        | mightBeGuarded token = Map.insert name Nothing m-        | couldFail word       = m-        | otherwise            = Map.insert name (combine m word) m-    addNulls m (Assignment (_, token, name, DataFrom _)) =-        Map.insert name Nothing m-    addNulls map _ = map--    getScopeId n (Reference (_, token, _)) = getId token-    getScopeId n (Assignment (_, token, _, _)) = getId token-    getScopeId n _ = n--    joinMaybes :: [Maybe String] -> Maybe String-    joinMaybes = foldl (liftM2 (++)) (Just "")-    combine m = c-      where-        c (T_DollarBraced _ t) | unnullable t =-            Map.findWithDefault (Just "") (concat $ deadSimple t) m-        c (T_DoubleQuoted _ tokens) = joinMaybes $ map (combine m) tokens-        c (T_NormalWord _ tokens) = joinMaybes $ map (combine m) tokens-        c (T_Glob _ "*") = Just "*"-        c t = getLiteralString t--    couldFail (T_Backticked _ _) = True-    couldFail (T_DollarExpansion _ _) = True-    couldFail (T_DoubleQuoted _ foo) = any couldFail foo-    couldFail (T_NormalWord _ foo) = any couldFail foo-    couldFail _ = False--    mightBeGuarded token = any t (getPath (parentMap params) token)-      where-        t (T_Condition {}) = True-        t (T_OrIf {}) = True-        t (T_AndIf {}) = True-        t _ = False-     paths = [-        "/", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",-        "/var"+        "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",+        "/var", "/lib"         ]-    importantPaths = ["", "/*", "/*/*"] >>= (\x -> map (++x) paths)+    importantPaths = filter (not . null) $+        ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths) checkCatastrophicRm _ _ = return ()  @@ -2708,7 +2929,7 @@   prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar"-prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/zsh\nfoo |& bar"+prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar" checkStderrPipe params =     case shellType params of         Ksh -> match@@ -2727,6 +2948,7 @@ prop_checkUnpassedInFunctions7 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo; foo;" prop_checkUnpassedInFunctions8 = verifyNotTree checkUnpassedInFunctions "foo() { echo $((1)); }; foo;" prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;"+prop_checkUnpassedInFunctions10= verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;" checkUnpassedInFunctions params root =     execWriter $ mapM_ warnForGroup referenceGroups   where@@ -2844,12 +3066,10 @@     isQuoted _ = False checkTildeInPath _ _ = return () -prop_checkUnsupported1 = verifyNot checkUnsupported "#!/bin/zsh\nfunction { echo cow; }"-prop_checkUnsupported2 = verify checkUnsupported "#!/bin/sh\nfunction { echo cow; }" prop_checkUnsupported3 = verify checkUnsupported "#!/bin/sh\ncase foo in bar) baz ;& esac" prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac" checkUnsupported params t =-    when ((not $ null support) && (shellType params `notElem` support)) $+    when (not (null support) && (shellType params `notElem` support)) $         report name  where     (name, support) = shellSupport t@@ -2860,15 +3080,11 @@ -- TODO: Move more of these checks here shellSupport t =   case t of-    T_Function _ _ _ "" _ -> ("anonymous functions", [Zsh])-    T_ForIn _ _ (_:_:_) _ _ -> ("multi-index for loops", [Zsh])-    T_ForIn _ ShortForIn _ _ _ -> ("short form for loops", [Zsh])-    T_ProcSub _ "=" _ -> ("=(..) process substitution", [Zsh])     T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list)     otherwise -> ("", [])   where-    forCase seps | any (== CaseContinue) seps = ("cases with ;;&", [Bash])-    forCase seps | any (== CaseFallThrough) seps = ("cases with ;&", [Bash, Ksh, Zsh])+    forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash])+    forCase seps | CaseFallThrough `elem` seps = ("cases with ;&", [Bash, Ksh])     forCase _ = ("", [])  @@ -2877,7 +3093,7 @@ getCommandSequences (T_Subshell _ cmds) = [cmds] getCommandSequences (T_WhileExpression _ _ cmds) = [cmds] getCommandSequences (T_UntilExpression _ _ cmds) = [cmds]-getCommandSequences (T_ForIn _ _ _ _ cmds) = [cmds]+getCommandSequences (T_ForIn _ _ _ cmds) = [cmds] getCommandSequences (T_ForArithmetic _ _ _ _ cmds) = [cmds] getCommandSequences (T_IfExpression _ thens elses) = map snd thens ++ [elses] getCommandSequences _ = []@@ -2957,6 +3173,7 @@ prop_checkGrepQ3= verify checkShouldUseGrepQ "[ -n \"$(foo | zgrep lol)\" ]" prop_checkGrepQ4= verifyNot checkShouldUseGrepQ "[ -z $(grep bar | cmd) ]" prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"+prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]" checkShouldUseGrepQ params t =     potentially $ case t of         TC_Noary id _ token -> check id True token@@ -2969,8 +3186,8 @@         let op = if bool then "-n" else "-z"         let flip = if bool then "" else "! "         return . style id 2143 $-            "Instead of [ " ++ op ++ " $(foo | " ++ name ++ " bar) ], " ++-                "use " ++ flip ++ "foo | " ++ name ++ " -q bar ."+            "Use " ++ flip ++ name ++ " -q instead of " +++                "comparing output with [ " ++ op ++ " .. ]."      getFinalGrep t = do         cmds <- getPipeline t@@ -2985,7 +3202,7 @@             T_DollarExpansion _ [x] -> getPipeline x             T_Pipeline _ _ cmds -> return cmds             _ -> fail "unknown"-    isGrep = isSuffixOf "grep"+    isGrep = (`elem` ["grep", "egrep", "fgrep", "zgrep"])  prop_checkTestGlobs1 = verify checkTestGlobs "[ -e *.mp3 ]" prop_checkTestGlobs2 = verifyNot checkTestGlobs "[[ $a == *b* ]]"@@ -3003,7 +3220,7 @@     f list | length list < length pattern = return ()     f list@(_:rest) =         if all id (zipWith ($) pattern list)-        then warnFor (list !! ((length pattern)-1))+        then warnFor (list !! (length pattern - 1))         else f rest     isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]     isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]@@ -3012,6 +3229,46 @@         return $ param `elem` strs     warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group." -return []-runTests = $quickCheckAll +prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"+prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"+prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"+checkFindExecWithSingleArgument _ = checkCommand "find" (const f)+  where+    f = void . sequence . mapMaybe check . tails+    check (exec:arg:term:_) = do+        execS <- getLiteralString exec+        termS <- getLiteralString term+        cmdS <- getLiteralStringExt (const $ return " ") arg++        guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]+        guard $ cmdS `matches` commandRegex+        return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."+    check _ = Nothing+    commandRegex = mkRegex "[ |;]"+++prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }"+prop_checkMaskedReturns2 = verify checkMaskedReturns "declare a=$(false)"+prop_checkMaskedReturns3 = verify checkMaskedReturns "declare a=\"`false`\""+prop_checkMaskedReturns4 = verifyNot checkMaskedReturns "declare a; a=$(false)"+prop_checkMaskedReturns5 = verifyNot checkMaskedReturns "f() { local -r a=$(false); }"+checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = potentially $ do+    name <- getCommandName t+    guard $ name `elem` ["declare", "export"]+        || name == "local" && "r" `notElem` getFlags t+    return $ mapM_ checkArgs rest+  where+    checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =+        warn id 2155 "Declare and assign separately to avoid masking return values."+    checkArgs _ = return ()++    hasReturn t = case t of+        T_Backticked {} -> True+        T_DollarExpansion {} -> True+        _ -> False+checkMaskedReturns _ _ = return ()+++return []+runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
ShellCheck/Data.hs view
@@ -27,22 +27,10 @@     "LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",     "OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",     "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",-    "TMOUT", "TMPDIR", "auto_resume", "histchars",+    "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC", -    -- Zsh-    "ARGV0", "BAUD", "cdpath", "COLUMNS", "CORRECT_IGNORE",-    "DIRSTACKSIZE", "ENV", "FCEDIT", "fignore", "fpath", "histchars",-    "HISTCHARS", "HISTFILE", "HISTSIZE", "HOME", "IFS", "KEYBOARD_HACK",-    "KEYTIMEOUT", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",-    "LC_MESSAGES", "LC_NUMERIC", "LC_TIME", "LINES", "LISTMAX",-    "LOGCHECK", "MAIL", "MAILCHECK", "mailpath", "manpath", "module_path",-    "NULLCMD", "path", "POSTEDIT", "PROMPT", "PROMPT2", "PROMPT3",-    "PROMPT4", "prompt", "PROMPT_EOL_MARK", "PS1", "PS2", "PS3", "PS4",-    "psvar", "READNULLCMD", "REPORTTIME", "REPLY", "reply", "RPROMPT",-    "RPS1", "RPROMPT2", "RPS2", "SAVEHIST", "SPROMPT", "STTY", "TERM",-    "TERMINFO", "TIMEFMT", "TMOUT", "TMPPREFIX", "watch", "WATCHFMT",-    "WORDCHARS", "ZBEEP", "ZDOTDIR", "ZLE_LINE_ABORTED",-    "ZLE_REMOVE_SUFFIX_CHARS", "ZLE_SPACE_SUFFIX_CHARS"+    -- Other+    "USER", "TZ", "TERM"   ]  variablesWithoutSpaces = [
ShellCheck/Options.hs view
@@ -1,6 +1,6 @@ module ShellCheck.Options where -data Shell = Ksh | Zsh | Sh | Bash+data Shell = Ksh | Sh | Bash     deriving (Show, Eq)  data AnalysisOptions = AnalysisOptions {
ShellCheck/Parser.hs view
@@ -20,6 +20,7 @@  import ShellCheck.AST import ShellCheck.Data+import ShellCheck.Options import Text.Parsec import Debug.Trace import Control.Monad@@ -468,14 +469,15 @@ prop_a7 = isOk readArithmeticContents "3*2**10" prop_a8 = isOk readArithmeticContents "3" prop_a9 = isOk readArithmeticContents "a^!-b"-prop_aA = isOk readArithmeticContents "! $?"-prop_aB = isOk readArithmeticContents "10#08 * 16#f"-prop_aC = isOk readArithmeticContents "\"$((3+2))\" + '37'"-prop_aD = isOk readArithmeticContents "foo[9*y+x]++"-prop_aE = isOk readArithmeticContents "1+`echo 2`"-prop_aF = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"-prop_a10= isOk readArithmeticContents "$foo$bar"-prop_a11= isOk readArithmeticContents "i<(0+(1+1))"+prop_a10= isOk readArithmeticContents "! $?"+prop_a11= isOk readArithmeticContents "10#08 * 16#f"+prop_a12= isOk readArithmeticContents "\"$((3+2))\" + '37'"+prop_a13= isOk readArithmeticContents "foo[9*y+x]++"+prop_a14= isOk readArithmeticContents "1+`echo 2`"+prop_a15= isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"+prop_a16= isOk readArithmeticContents "$foo$bar"+prop_a17= isOk readArithmeticContents "i<(0+(1+1))"+prop_a18= isOk readArithmeticContents "a?b:c" readArithmeticContents =     readSequence   where@@ -517,7 +519,7 @@             readNormalDollar,             readBraced,             readBackTicked,-            readNormalLiteral "+-*/=%^,]"+            readNormalLiteral "+-*/=%^,]?:"             ]         spacing         return $ TA_Expansion id pieces@@ -763,11 +765,10 @@  prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)" prop_readProcSub2 = isOk readProcSub "<(  if true; then true; fi )"-prop_readProcSub3 = isOk readProcSub "=(ls)" readProcSub = called "process substitution" $ do     id <- getNextId     dir <- try $ do-                    x <- oneOf "<>="+                    x <- oneOf "<>"                     char '('                     return [x]     allspacing@@ -949,7 +950,7 @@     pos <- getPosition     backslash     do-        next <- quotable <|> oneOf "?*@!+[]{}.,"+        next <- quotable <|> oneOf "?*@!+[]{}.,~#"         return $ if next == '\n' then "" else [next]       <|>         do@@ -1094,6 +1095,7 @@     string "]"     return (T_DollarBracket id c) +prop_readArithmeticExpression = isOk readArithmeticExpression "((a?b:c))" readArithmeticExpression = called "((..)) command" $ do     id <- getNextId     try (string "((")@@ -1121,23 +1123,33 @@     return $ T_DollarExpansion id cmds  prop_readDollarVariable = isOk readDollarVariable "$@"+prop_readDollarVariable2 = isOk (readDollarVariable >> anyChar) "$?!"+prop_readDollarVariable3 = isWarning (readDollarVariable >> anyChar) "$10"+prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]"+ readDollarVariable = do     id <- getNextId+    pos <- getPosition+     let singleCharred p = do         n <- p         value <- wrap [n]-        return (T_DollarBraced id value) `attempting` do-            pos <- getPosition-            num <- lookAhead $ many1 p-            parseNoteAt pos ErrorC 1037 $ "$" ++ (n:num) ++ " is equivalent to ${" ++ [n] ++ "}"++ num ++"."+        return (T_DollarBraced id value) -    let positional = singleCharred digit+    let positional = do+        value <- singleCharred digit+        return value `attempting` do+            lookAhead digit+            parseNoteAt pos ErrorC 1037 "Braces are required for positionals over 9, e.g. ${10}."+     let special = singleCharred specialVariable      let regular = do         name <- readVariableName         value <- wrap name-        return $ T_DollarBraced id value+        return (T_DollarBraced id value) `attempting` do+            lookAhead $ void (string "[@]") <|> void (string "[*]") <|> void readArrayIndex+            parseNoteAt pos ErrorC 1087 "Braces are required when expanding arrays, as in ${array[idx]}."      try $ char '$' >> (positional <|> special <|> regular) @@ -1345,7 +1357,6 @@ prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)" prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi" prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"-prop_readSimpleCommand7 = isOk readSimpleCommand "cat =(ls)" readSimpleCommand = called "simple command" $ do     id1 <- getNextId     id2 <- getNextId@@ -1449,7 +1460,11 @@     spacing     return $ T_Pipe id ('|':qualifier) -readCommand = readCompoundCommand <|> readSimpleCommand+readCommand = choice [+    readCompoundCommand,+    readCoProc,+    readSimpleCommand+    ]  readCmdName = do     f <- readNormalWord@@ -1617,7 +1632,6 @@ prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone" prop_readForClause9 = isOk readForClause "for i do true; done" prop_readForClause10= isOk readForClause "for ((;;)) { true; }"-prop_readForClause11= isOk readForClause "for a b in *; do echo $a $b; done" prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done" readForClause = called "for loop" $ do     pos <- getPosition@@ -1646,25 +1660,10 @@     readRegular id pos = do         acceptButWarn (char '$') ErrorC 1086             "Don't use $ on the iterator name in for loops."-        names <- readNames-        readShort names <|> readLong names-      where-        readLong names = do-            values <- readInClause <|> (optional readSequentialSep >> return [])-            group <- readDoGroup pos-            return $ T_ForIn id NormalForIn names values group-        readShort names = do-            char '('-            allspacing-            words <- many (readNormalWord `thenSkip` allspacing)-            char ')'-            allspacing-            command <- readAndOr-            return $ T_ForIn id ShortForIn names words [command]--    readNames =-        reluctantlyTill1 (readVariableName `thenSkip` spacing) $-            disregard g_Do <|> disregard readInClause <|> disregard readSequentialSep+        name <- readVariableName `thenSkip` spacing+        values <- readInClause <|> (optional readSequentialSep >> return [])+        group <- readDoGroup pos+        return $ T_ForIn id name values group  prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done" prop_readSelectClause2 = isOk readSelectClause "select foo; do echo $foo; done"@@ -1785,8 +1784,32 @@                 g_Rparen             return () -        readFunctionName = many functionChars+        readFunctionName = many1 functionChars +prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }"+prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }"+prop_readCoProc3 = isOk readCoProc "coproc echo bar"+readCoProc = called "coproc" $ do+    id <- getNextId+    try $ do+        string "coproc"+        whitespace+    choice [ try $ readCompoundCoProc id, readSimpleCoProc id ]+  where+    readCompoundCoProc id = do+        var <- optionMaybe $+            readVariableName `thenSkip` whitespace+        body <- readBody readCompoundCommand+        return $ T_CoProc id var body+    readSimpleCoProc id = do+        body <- readBody readSimpleCommand+        return $ T_CoProc id Nothing body+    readBody parser = do+        id <- getNextId+        body <- parser+        return $ T_CoProcBody id body++ readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)  prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"@@ -1842,6 +1865,7 @@ prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42" prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "+prop_readAssignmentWord9a= isOk readAssignmentWord "foo=" prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"@@ -1850,25 +1874,23 @@     pos <- getPosition     optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")     variable <- readVariableName-    notFollowedBy2 $ do -- Special case for zsh =(..) syntax-        spacing1-        string "=("     optional (readNormalDollar >> parseNoteAt pos ErrorC                                 1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")     index <- optionMaybe readArrayIndex-    space <- spacing+    hasLeftSpace <- liftM (not . null) spacing     pos <- getPosition     op <- readAssignmentOp-    space2 <- spacing-    if space == "" && space2 /= ""+    hasRightSpace <- liftM (not . null) spacing+    isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof))+    if not hasLeftSpace && (hasRightSpace || isEndOfCommand)       then do-        when (variable /= "IFS") $+        when (variable /= "IFS" && hasRightSpace) $             parseNoteAt pos WarningC 1007                 "Remove space after = if trying to assign a value (for empty string, use var='' ... )."         value <- readEmptyLiteral         return $ T_Assignment id op variable index value       else do-        when (space /= "" || space2 /= "") $+        when (hasLeftSpace || hasRightSpace) $             parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments."         value <- readArray <|> readNormalWord         spacing@@ -2050,8 +2072,8 @@     verifyShell pos s =         case isValidShell s of             Just True -> return ()-            Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports Bourne based shell scripts, sorry!"-            Nothing -> parseProblemAt pos InfoC 1008 "This shebang was unrecognized. Note that ShellCheck only handles Bourne based shells."+            Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/ksh scripts. Sorry!"+            Nothing -> parseProblemAt pos InfoC 1008 "This shebang was unrecognized. Note that ShellCheck only handles sh/bash/ksh."      isValidShell s =         let good = s == "" || any (`isPrefixOf` s) goodShells@@ -2065,9 +2087,10 @@      goodShells = [         "sh",+        "ash",+        "dash",         "bash",-        "ksh",-        "zsh"+        "ksh"         ]     badShells = [         "awk",@@ -2075,7 +2098,8 @@         "perl",         "python",         "ruby",-        "tcsh"+        "tcsh",+        "zsh"         ]      readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF"@@ -2119,13 +2143,13 @@                 Message s     ->  if null s then Nothing else return $ s ++ "."         unexpected s = "Unexpected " ++ (if null s then "eof" else s) ++ "." -parseShell filename contents =+parseShell options filename contents =     case rp (parseWithNotes readScript) filename contents of         (Right (script, map, notes), (parsenotes, _)) ->-            ParseResult (Just (script, map)) (nub $ sortNotes $ notes ++ parsenotes)+            ParseResult (Just (script, map)) (nub . sortNotes . excludeNotes $ notes ++ parsenotes)         (Left err, (p, context)) ->             ParseResult Nothing-                (nub $ sortNotes $ p ++ notesForContext context ++ [makeErrorFor err])+                (nub . sortNotes . excludeNotes $ p ++ notesForContext context ++ [makeErrorFor err])   where     isName (ContextName _ _) = True     isName _ = False@@ -2134,6 +2158,7 @@         "Couldn't parse this " ++ str ++ "."     second (ContextName pos str) = ParseNote pos InfoC 1009 $         "The mentioned parser error was in this " ++ str ++ "."+    excludeNotes = filter (\c -> codeForParseNote c `notElem` optionExcludes options)  lt x = trace (show x) x ltt t = trace (show t)
ShellCheck/Simple.hs view
@@ -28,7 +28,7 @@  shellCheck :: AnalysisOptions -> String -> [ShellCheckComment] shellCheck options script =-    let (ParseResult result notes) = parseShell "-" script in+    let (ParseResult result notes) = parseShell options "-" script in         let allNotes = notes ++ concat (maybeToList $ do             (tree, posMap) <- result             let list = runAnalytics options tree@@ -71,6 +71,9 @@  prop_optionDisablesIssue1 =     null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2086, 2148] }) "echo $1"++prop_optionDisablesIssue2 =+    null $ shellCheck (defaultAnalysisOptions { optionExcludes = [2148, 1037] }) "echo \"$10\""  return [] runTests = $quickCheckAll
+ shellcheck.1 view
@@ -0,0 +1,188 @@+.TH SHELLCHECK 1 "" "Shell script analysis tool"+.SH NAME+.PP+shellcheck - Shell script analysis tool+.SH SYNOPSIS+.PP+\f[B]shellcheck\f[] [\f[I]OPTIONS\f[]...] \f[I]FILES\f[]...+.SH DESCRIPTION+.PP+ShellCheck is a static analysis and linting tool for sh/bash scripts.+It\[aq]s mainly focused on handling typical beginner and intermediate+level syntax errors and pitfalls where the shell just gives a cryptic+error message or strange behavior, but it also reports on a few more+advanced issues where corner cases can cause delayed failures.+.PP+ShellCheck gives shell specific advice.+Consider the line:+.IP+.nf+\f[C]+((\ area\ =\ 3.14*r*r\ ))+\f[]+.fi+.IP \[bu] 2+For scripts starting with \f[C]#!/bin/sh\f[] (or when using+\f[C]-s\ sh\f[]), ShellCheck will warn that \f[C]((\ ..\ ))\f[] is not+POSIX compliant (similar to checkbashisms).+.IP \[bu] 2+For scripts starting with \f[C]#!/bin/bash\f[] (or using+\f[C]-s\ bash\f[]), ShellCheck will warn that decimals are not+supported.+.IP \[bu] 2+For scripts starting with \f[C]#!/bin/ksh\f[] (or using+\f[C]-s\ ksh\f[]), ShellCheck will not warn at all, as \f[C]ksh\f[]+supports decimals in arithmetic contexts.+.SH OPTIONS+.TP+.B \f[B]-e\f[]\ \f[I]CODE1\f[][,\f[I]CODE2\f[]...],\ \f[B]--exclude=\f[]\f[I]CODE1\f[][,\f[I]CODE2\f[]...]+Explicitly exclude the specified codes from the report.+Subsequent \f[B]-e\f[] options are cumulative, but all the codes can be+specified at once, comma-separated as a single argument.+.RS+.RE+.TP+.B \f[B]-f\f[] \f[I]FORMAT\f[], \f[B]--format=\f[]\f[I]FORMAT\f[]+Specify the output format of shellcheck, which prints its results in the+standard output.+Subsequent \f[B]-f\f[] options are ignored, see \f[B]FORMATS\f[] below+for more information.+.RS+.RE+.TP+.B \f[B]-s\f[]\ \f[I]shell\f[],\ \f[B]--shell=\f[]\f[I]shell\f[]+Specify Bourne shell dialect.+Valid values are \f[I]sh\f[], \f[I]bash\f[] and \f[I]ksh\f[].+The default is to use the file\[aq]s shebang, or \f[I]bash\f[] if the+target shell can\[aq]t be determined.+.RS+.RE+.TP+.B \f[B]-V\f[]\ \f[I]version\f[],\ \f[B]--version\f[]+Print version and exit.+.RS+.RE+.SH FORMATS+.TP+.B \f[B]tty\f[]+Plain text, human readable output.+This is the default.+.RS+.RE+.TP+.B \f[B]gcc\f[]+GCC compatible output.+Useful for editors that support compiling and showing syntax errors.+.RS+.PP+For example, in Vim, \f[C]:set\ makeprg=shellcheck\\\ -f\\\ gcc\\\ %\f[]+will allow using \f[C]:make\f[] to check the script, and \f[C]:cnext\f[]+to jump to the next error.+.IP+.nf+\f[C]+<file>:<line>:<column>:\ <type>:\ <message>+\f[]+.fi+.RE+.TP+.B \f[B]checkstyle\f[]+Checkstyle compatible XML output.+Supported directly or through plugins by many IDEs and build monitoring+systems.+.RS+.IP+.nf+\f[C]+<?xml\ version=\[aq]1.0\[aq]\ encoding=\[aq]UTF-8\[aq]?>+<checkstyle\ version=\[aq]4.3\[aq]>+\ \ <file\ name=\[aq]file\[aq]>+\ \ \ \ <error+\ \ \ \ \ \ line=\[aq]line\[aq]+\ \ \ \ \ \ column=\[aq]column\[aq]+\ \ \ \ \ \ severity=\[aq]severity\[aq]+\ \ \ \ \ \ message=\[aq]message\[aq]+\ \ \ \ \ \ source=\[aq]ShellCheck.SC####\[aq]\ />+\ \ \ \ ...+\ \ </file>+\ \ ...+</checkstyle>+\f[]+.fi+.RE+.TP+.B \f[B]json\f[]+Json is a popular serialization format that is more suitable for web+applications.+ShellCheck\[aq]s json is compact and contains only the bare minimum.+.RS+.IP+.nf+\f[C]+[+\ \ {+\ \ \ \ "file":\ "filename",+\ \ \ \ "line":\ lineNumber,+\ \ \ \ "column":\ columnNumber,+\ \ \ \ "level":\ "severitylevel",+\ \ \ \ "code":\ errorCode,+\ \ \ \ "message":\ "warning\ message"+\ \ },+\ \ ...+]+\f[]+.fi+.RE+.SH DIRECTIVES+.PP+ShellCheck directives can be specified as comments in the shell script+before a command or block:+.IP+.nf+\f[C]+#\ shellcheck\ key=value\ key=value+command-or-structure+\f[]+.fi+.PP+For example, to suppress SC2035 about using \f[C]\&./*.jpg\f[]:+.IP+.nf+\f[C]+#\ shellcheck\ disable=SC2035+echo\ "Files:\ "\ *.jpg+\f[]+.fi+.PP+Here a shell brace group is used to suppress on multiple lines:+.IP+.nf+\f[C]+#\ shellcheck\ disable=SC2016+{+\ \ echo\ \[aq]Modifying\ $PATH\[aq]+\ \ echo\ \[aq]PATH=foo:$PATH\[aq]\ >>\ ~/.bashrc+}+\f[]+.fi+.PP+Valid keys are:+.TP+.B \f[B]disable\f[]+Disables a comma separated list of error codes for the following+command.+The command can be a simple command like \f[C]echo\ foo\f[], or a+compound command like a function definition, subshell block or loop.+.RS+.RE+.SH AUTHOR+.PP+ShellCheck is written and maintained by Vidar Holen.+.SH REPORTING BUGS+.PP+Bugs and issues can be reported on GitHub:+.PP+https://github.com/koalaman/shellcheck/issues+.SH SEE ALSO+.PP+sh(1) bash(1)
shellcheck.1.md view
@@ -16,6 +16,20 @@ strange behavior, but it also reports on a few more advanced issues where corner cases can cause delayed failures. +ShellCheck gives shell specific advice. Consider the line:++    (( area = 3.14*r*r ))+++ For scripts starting with `#!/bin/sh` (or when using `-s sh`), ShellCheck+will warn that `(( .. ))` is not POSIX compliant (similar to checkbashisms).+++ For scripts starting with `#!/bin/bash` (or using `-s bash`), ShellCheck+will warn that decimals are not supported.+++ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will+not warn at all, as `ksh` supports decimals in arithmetic contexts.++ # OPTIONS  **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]@@ -32,9 +46,9 @@  **-s**\ *shell*,\ **--shell=***shell* -:   Specify Bourne shell dialect. Valid values are *sh*, *bash*, *ksh* and-    *zsh*. The default is to use the file's shebang, or *bash* if the target-    shell can't be determined.+:   Specify Bourne shell dialect. Valid values are *sh*, *bash* and *ksh*.+    The default is to use the file's shebang, or *bash* if the target shell+    can't be determined.  **-V**\ *version*,\ **--version** @@ -83,11 +97,12 @@          [           {-            "line": line,-            "column": column,-            "level": level,-            "code": ####,-            "message": message+            "file": "filename",+            "line": lineNumber,+            "column": columnNumber,+            "level": "severitylevel",+            "code": errorCode,+            "message": "warning message"           },           ...         ]@@ -103,6 +118,14 @@      # shellcheck disable=SC2035     echo "Files: " *.jpg++Here a shell brace group is used to suppress on multiple lines:++    # shellcheck disable=SC2016+    {+      echo 'Modifying $PATH'+      echo 'PATH=foo:$PATH' >> ~/.bashrc+    }  Valid keys are: 
shellcheck.hs view
@@ -19,6 +19,7 @@ import Control.Monad import Control.Monad.Trans import Control.Monad.Trans.Error+import Control.Monad.Trans.List import Data.Char import Data.Maybe import Data.Monoid@@ -40,6 +41,8 @@ data Flag = Flag String String data Status = NoProblems | SomeProblems | BadInput | SupportFailure | SyntaxFailure | RuntimeException deriving (Ord, Eq) +data JsonComment = JsonComment FilePath ShellCheckComment+ instance Error Status where     noMsg = RuntimeException @@ -54,7 +57,7 @@     Option "f" ["format"]         (ReqArg (Flag "format") "FORMAT") "output format",     Option "s" ["shell"]-        (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)",+        (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)",     Option "V" ["version"]         (NoArg $ Flag "version" "true") "Print version information"     ]@@ -62,8 +65,9 @@ printErr = hPutStrLn stderr  -instance JSON ShellCheckComment where-  showJSON c = makeObj [+instance JSON (JsonComment) where+  showJSON (JsonComment filename c) = makeObj [+      ("file", showJSON $ filename),       ("line", showJSON $ scLine c),       ("column", showJSON $ scColumn c),       ("level", showJSON $ scSeverity c),@@ -152,10 +156,12 @@         term <- hIsTerminalDevice stdout         return $ if term then colorComment else const id --- This totally ignores the filenames. Fixme? forJson :: AnalysisOptions -> [FilePath] -> IO Status forJson options files = catchExceptions $ do-    comments <- liftM concat $ mapM (commentsFor options) files+    comments <- runListT $ do+        file <- ListT $ return files+        comment <- ListT $ commentsFor options file+        return $ JsonComment file comment     putStrLn $ encodeStrict comments     return $ checkComments comments