packages feed

ShellCheck 0.3.3 → 0.3.4

raw patch · 9 files changed

+850/−501 lines, 9 filesdep +QuickCheckdep ~basePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: QuickCheck

Dependency ranges changed: base

API changes (from Hackage documentation)

- ShellCheck.AST: TA_Base :: Id -> String -> Token -> Token
- ShellCheck.AST: TA_Literal :: Id -> String -> Token
- ShellCheck.AST: TA_Variable :: Id -> String -> Token
+ ShellCheck.AST: CaseBreak :: CaseType
+ ShellCheck.AST: CaseContinue :: CaseType
+ ShellCheck.AST: CaseFallThrough :: CaseType
+ ShellCheck.AST: T_ :: Id -> [Token] -> Token
+ ShellCheck.AST: T_IndexedElement :: Id -> Token -> Token -> Token
+ ShellCheck.AST: data CaseType
+ ShellCheck.AST: instance Eq CaseType
+ ShellCheck.AST: instance Show CaseType
+ ShellCheck.Analytics: runTests :: IO Bool
+ ShellCheck.Parser: instance Show Context
+ ShellCheck.Parser: runTests :: IO Bool
+ ShellCheck.Simple: runTests :: IO Bool
- ShellCheck.AST: TA_Expansion :: Id -> Token -> Token
+ ShellCheck.AST: TA_Expansion :: Id -> [Token] -> Token
- ShellCheck.AST: T_CaseExpression :: Id -> Token -> [([Token], [Token])] -> Token
+ ShellCheck.AST: T_CaseExpression :: Id -> Token -> [(CaseType, [Token], [Token])] -> Token
- ShellCheck.Data: shellcheckVersion :: [Char]
+ ShellCheck.Data: shellcheckVersion :: String

Files

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