packages feed

ShellCheck 0.5.0 → 0.6.0

raw patch · 21 files changed

+1210/−476 lines, 21 filesdep ~mtlsetup-changedPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: mtl

API changes (from Hackage documentation)

- ShellCheck.AnalyzerLib: instance Data.Semigroup.Semigroup ShellCheck.AnalyzerLib.Checker
- ShellCheck.Interface: AnalysisResult :: [TokenComment] -> AnalysisResult
- ShellCheck.Interface: AnalysisSpec :: Token -> Maybe Shell -> ExecutionMode -> Bool -> AnalysisSpec
- ShellCheck.Interface: CheckResult :: String -> [PositionedComment] -> CheckResult
- ShellCheck.Interface: CheckSpec :: String -> String -> Bool -> [Integer] -> Maybe Shell -> CheckSpec
- ShellCheck.Interface: Comment :: Severity -> Code -> String -> Comment
- ShellCheck.Interface: FormatterOptions :: ColorOption -> FormatterOptions
- ShellCheck.Interface: ParseResult :: [PositionedComment] -> Map Id Position -> Maybe Token -> ParseResult
- ShellCheck.Interface: ParseSpec :: String -> String -> Bool -> ParseSpec
- ShellCheck.Interface: Position :: String -> Integer -> Integer -> Position
- ShellCheck.Interface: PositionedComment :: Position -> Position -> Comment -> PositionedComment
- ShellCheck.Interface: TokenComment :: Id -> Comment -> TokenComment
- ShellCheck.Interface: [arComments] :: AnalysisResult -> [TokenComment]
- ShellCheck.Interface: [asCheckSourced] :: AnalysisSpec -> Bool
- ShellCheck.Interface: [asExecutionMode] :: AnalysisSpec -> ExecutionMode
- ShellCheck.Interface: [asScript] :: AnalysisSpec -> Token
- ShellCheck.Interface: [asShellType] :: AnalysisSpec -> Maybe Shell
- ShellCheck.Interface: [crComments] :: CheckResult -> [PositionedComment]
- ShellCheck.Interface: [crFilename] :: CheckResult -> String
- ShellCheck.Interface: [csCheckSourced] :: CheckSpec -> Bool
- ShellCheck.Interface: [csExcludedWarnings] :: CheckSpec -> [Integer]
- ShellCheck.Interface: [csFilename] :: CheckSpec -> String
- ShellCheck.Interface: [csScript] :: CheckSpec -> String
- ShellCheck.Interface: [csShellTypeOverride] :: CheckSpec -> Maybe Shell
- ShellCheck.Interface: [foColorOption] :: FormatterOptions -> ColorOption
- ShellCheck.Interface: [posColumn] :: Position -> Integer
- ShellCheck.Interface: [posFile] :: Position -> String
- ShellCheck.Interface: [posLine] :: Position -> Integer
- ShellCheck.Interface: [prComments] :: ParseResult -> [PositionedComment]
- ShellCheck.Interface: [prRoot] :: ParseResult -> Maybe Token
- ShellCheck.Interface: [prTokenPositions] :: ParseResult -> Map Id Position
- ShellCheck.Interface: [psCheckSourced] :: ParseSpec -> Bool
- ShellCheck.Interface: [psFilename] :: ParseSpec -> String
- ShellCheck.Interface: [psScript] :: ParseSpec -> String
- ShellCheck.Interface: newtype AnalysisResult
- ShellCheck.Interface: newtype FormatterOptions
+ ShellCheck.ASTLib: getCommandNameAndToken :: Token -> (Maybe String, Token)
+ ShellCheck.ASTLib: getCommandTokenOrThis :: Token -> Token
+ ShellCheck.AnalyzerLib: instance GHC.Base.Semigroup ShellCheck.AnalyzerLib.Checker
+ ShellCheck.Interface: data AnalysisResult
+ ShellCheck.Interface: data FormatterOptions
+ ShellCheck.Interface: emptyCheckResult :: CheckResult
+ ShellCheck.Interface: newAnalysisResult :: AnalysisResult
+ ShellCheck.Interface: newAnalysisSpec :: Token -> AnalysisSpec
+ ShellCheck.Interface: newComment :: Comment
+ ShellCheck.Interface: newFormatterOptions :: FormatterOptions
+ ShellCheck.Interface: newParseResult :: ParseResult
+ ShellCheck.Interface: newParseSpec :: ParseSpec
+ ShellCheck.Interface: newPosition :: Position
+ ShellCheck.Interface: newPositionedComment :: PositionedComment
+ ShellCheck.Interface: newTokenComment :: TokenComment
- ShellCheck.AST: T_CoProc :: Id -> (Maybe String) -> Token -> Token
+ ShellCheck.AST: T_CoProc :: Id -> Maybe String -> Token -> Token
- ShellCheck.AnalyzerLib: getClosestCommandM :: MonadReader Parameters m => Token -> m Maybe Token
+ ShellCheck.AnalyzerLib: getClosestCommandM :: MonadReader Parameters m => Token -> m (Maybe Token)
- ShellCheck.AnalyzerLib: pScript :: String -> Maybe Token
+ ShellCheck.AnalyzerLib: pScript :: [Char] -> Maybe Token
- ShellCheck.AnalyzerLib: whenShell :: (Foldable t, MonadReader Parameters m) => t Shell -> m () -> m ()
+ ShellCheck.AnalyzerLib: whenShell :: (MonadReader Parameters m, Foldable t) => t Shell -> m () -> m ()
- ShellCheck.Formatter.Format: makeNonVirtual :: [PositionedComment] -> String -> [PositionedComment]
+ ShellCheck.Formatter.Format: makeNonVirtual :: [PositionedComment] -> [Char] -> [PositionedComment]

Files

LICENSE view
@@ -1,3 +1,13 @@+Employer mandated disclaimer:++  I am providing code in the repository to you under an open source license.+  Because this is my personal repository, the license you receive to my code is+  from me and other individual contributors, and not my employer (Facebook).++  - Vidar "koala_man" Holen++----+                     GNU GENERAL PUBLIC LICENSE                        Version 3, 29 June 2007 
README.md view
@@ -4,7 +4,7 @@  ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts: -![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png).+![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png)  The goals of ShellCheck are @@ -70,7 +70,7 @@  ![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png). -* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):+* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):  ![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png). @@ -133,6 +133,10 @@      brew install shellcheck +On OpenBSD:++    pkg_add shellcheck+ On openSUSE      zypper in ShellCheck@@ -163,27 +167,35 @@ Alternatively, you can download pre-compiled binaries for the latest release here:  * [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)+* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)  or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds. +Distro packages already come with a `man` page. If you are building from source, it can be installed with:++    pandoc -s -t man shellcheck.1.md -o shellcheck.1+    sudo mv shellcheck.1 /usr/share/man/man1+ ## Travis CI  Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. -If you still want to do so in order to upgrade at your leisure or ensure the latest release:+If you still want to do so in order to upgrade at your leisure or ensure the latest release, follow the steps to install the shellcheck binary, bellow. -    install:+## Installing the shellcheck binary -      # Install a custom version of shellcheck instead of Travis CI's default-      - scversion="stable" # or "v0.4.7", or "latest"-      - wget "https://storage.googleapis.com/shellcheck/shellcheck-$scversion.linux.x86_64.tar.xz"-      - tar --xz -xvf "shellcheck-$scversion.linux.x86_64.tar.xz"-      - shellcheck() { "shellcheck-$scversion/shellcheck" "$@"; }-      - shellcheck --version+*Pre-requisite*: the program 'xz' needs to be installed on the system.  +To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.  +To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.   -    script:-      - shellcheck *.sh+```bash+export scversion="stable" # or "v0.4.7", or "latest"+wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"+tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz+cp shellcheck-"${scversion}"/shellcheck /usr/bin/+shellcheck --version+```  ## Compiling from source @@ -442,3 +454,8 @@ Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.  Happy ShellChecking!+++## Other Resources                                                                          +* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).+* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
Setup.hs view
@@ -33,4 +33,4 @@   putStrLn $ "pandoc exited with " ++ show result   return emptyHookedBuildInfo   where-    pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"+    pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"
ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name:             ShellCheck-Version:          0.5.0+Version:          0.6.0 Synopsis:         Shell script analysis tool License:          GPL-3 License-file:     LICENSE@@ -28,6 +28,8 @@     shellcheck.1.md     -- built with a cabal sdist hook     shellcheck.1+    -- convenience script for stripping tests+    striptests     -- tests     test/shellcheck.hs @@ -35,7 +37,7 @@   setup-depends:     base    >= 4    && <5,     process >= 1.0  && <1.7,-    Cabal   >= 1.10 && <2.3+    Cabal   >= 1.10 && <2.5  source-repository head     type: git
shellcheck.1 view
@@ -1,4 +1,4 @@-.\" Automatically generated by Pandoc 1.19.2.4+.\" Automatically generated by Pandoc 2.2.1 .\" .TH "SHELLCHECK" "1" "" "Shell script analysis tool" "" .hy@@ -71,6 +71,14 @@ .RS .RE .TP+.B \f[B]\-S\f[]\ \f[I]SEVERITY\f[],\ \f[B]\-\-severity=\f[]\f[I]severity\f[]+Specify minimum severity of errors to consider.+Valid values are \f[I]error\f[], \f[I]warning\f[], \f[I]info\f[] and+\f[I]style\f[].+The default is \f[I]style\f[].+.RS+.RE+.TP .B \f[B]\-s\f[]\ \f[I]shell\f[],\ \f[B]\-\-shell=\f[]\f[I]shell\f[] Specify Bourne shell dialect. Valid values are \f[I]sh\f[], \f[I]bash\f[], \f[I]dash\f[] and@@ -82,6 +90,13 @@ .TP .B \f[B]\-V\f[],\ \f[B]\-\-version\f[] Print version information and exit.+.RS+.RE+.TP+.B \f[B]\-W\f[] \f[I]NUM\f[],\ \f[B]\-\-wiki\-link\-count=NUM\f[]+For TTY output, show \f[I]NUM\f[] wiki links to more information about+mentioned warnings.+Set to 0 to disable them entirely. .RS .RE .TP
shellcheck.1.md view
@@ -56,6 +56,11 @@     standard output. Subsequent **-f** options are ignored, see **FORMATS**     below for more information. +**-S**\ *SEVERITY*,\ **--severity=***severity*++:   Specify minimum severity of errors to consider. Valid values are *error*,+    *warning*, *info* and *style*.    The default is *style*.+ **-s**\ *shell*,\ **--shell=***shell*  :   Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.@@ -65,6 +70,11 @@ **-V**,\ **--version**  :   Print version information and exit.++**-W** *NUM*,\ **--wiki-link-count=NUM**++:   For TTY output, show *NUM* wiki links to more information about mentioned+    warnings. Set to 0 to disable them entirely.  **-x**,\ **--external-sources** 
shellcheck.hs view
@@ -67,15 +67,17 @@ data Options = Options {     checkSpec        :: CheckSpec,     externalSources  :: Bool,-    formatterOptions :: FormatterOptions+    formatterOptions :: FormatterOptions,+    minSeverity      :: Severity }  defaultOptions = Options {     checkSpec = emptyCheckSpec,     externalSources = False,-    formatterOptions = FormatterOptions {+    formatterOptions = newFormatterOptions {         foColorOption = ColorAuto-    }+    },+    minSeverity = StyleC }  usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."@@ -93,8 +95,14 @@     Option "s" ["shell"]         (ReqArg (Flag "shell") "SHELLNAME")         "Specify dialect (sh, bash, dash, ksh)",+    Option "S" ["severity"]+        (ReqArg (Flag "severity") "SEVERITY")+        "Minimum severity of errors to consider (error, warning, info, style)",     Option "V" ["version"]         (NoArg $ Flag "version" "true") "Print version information",+    Option "W" ["wiki-link-count"]+        (ReqArg (Flag "wiki-link-count") "NUM")+        "The number of wiki links to show, when applicable.",     Option "x" ["external-sources"]         (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"     ]@@ -137,12 +145,6 @@         else split' rest (a:element)     split' [] element = [reverse element] -getExclusions options =-    let elements = concatMap (split ',') $ getOptions options "exclude"-        clean = dropWhile (not . isDigit)-    in-        map (Prelude.read . clean) elements :: [Int]- toStatus = fmap (either id id) . runExceptT  getEnvArgs = do@@ -222,13 +224,29 @@                 then NoProblems                 else SomeProblems -parseColorOption colorOption =-    case colorOption of-        "auto"   -> ColorAuto-        "always" -> ColorAlways-        "never"  -> ColorNever-        _        -> error $ "Bad value for --color `" ++ colorOption ++ "'"+parseEnum name value list =+    case filter ((== value) . fst) list of+        [(name, value)] -> return value+        [] -> do+            printErr $ "Unknown value for --" ++ name ++ ". " +++                       "Valid options are: " ++ (intercalate ", " $ map fst list)+            throwError SupportFailure +parseColorOption value =+    parseEnum "color" value [+        ("auto",   ColorAuto),+        ("always", ColorAlways),+        ("never",  ColorNever)+        ]++parseSeverityOption value =+    parseEnum "severity" value [+        ("error",   ErrorC),+        ("warning", WarningC),+        ("info",    InfoC),+        ("style",   StyleC)+        ]+ parseOption flag options =     case flag of         Flag "shell" str ->@@ -241,7 +259,7 @@                         }          Flag "exclude" str -> do-            new <- mapM parseNum $ split ',' str+            new <- mapM parseNum $ filter (not . null) $ split ',' str             let old = csExcludedWarnings . checkSpec $ options             return options {                 checkSpec = (checkSpec options) {@@ -258,10 +276,11 @@                 externalSources = True             } -        Flag "color" color ->+        Flag "color" color -> do+            option <- parseColorOption color             return options {                 formatterOptions = (formatterOptions options) {-                    foColorOption = parseColorOption color+                    foColorOption = option                 }             } @@ -272,6 +291,22 @@                 }             } +        Flag "severity" severity -> do+            option <- parseSeverityOption severity+            return options {+                checkSpec = (checkSpec options) {+                    csMinSeverity = option+                }+            }++        Flag "wiki-link-count" countString -> do+            count <- parseNum countString+            return options {+                formatterOptions = (formatterOptions options) {+                    foWikiLinkCount = count+                }+            }+         _ -> return options   where     die s = do@@ -280,7 +315,7 @@     parseNum ('S':'C':str) = parseNum str     parseNum num = do         unless (all isDigit num) $ do-            printErr $ "Bad exclusion: " ++ num+            printErr $ "Invalid number: " ++ num             throwError SyntaxFailure         return (Prelude.read num :: Integer) 
src/ShellCheck/ASTLib.hs view
@@ -23,6 +23,7 @@  import Control.Monad.Writer import Control.Monad+import Data.Char import Data.Functor import Data.List import Data.Maybe@@ -226,8 +227,43 @@     g (T_SingleQuoted _ s) = return s     g (T_Literal _ s) = return s     g (T_ParamSubSpecialChar _ s) = return s+    g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s     g x = more x +    -- Bash style $'..' decoding+    decodeEscapes ('\\':c:cs) =+        case c of+            'a' -> '\a' : rest+            'b' -> '\b' : rest+            'e' -> '\x1B' : rest+            'f' -> '\f' : rest+            'n' -> '\n' : rest+            'r' -> '\r' : rest+            't' -> '\t' : rest+            'v' -> '\v' : rest+            '\'' -> '\'' : rest+            '"' -> '"' : rest+            '\\' -> '\\' : rest+            'x' ->+                case cs of+                    (x:y:more) ->+                        if isHexDigit x && isHexDigit y+                        then chr (16*(digitToInt x) + (digitToInt y)) : rest+                        else '\\':c:rest+            _ | isOctDigit c ->+                let digits = take 3 $ takeWhile isOctDigit (c:cs)+                    num = parseOct digits+                in (if num < 256 then chr num else '?') : rest+            _ -> '\\' : c : rest+      where+        rest = decodeEscapes cs+        parseOct = f 0+          where+            f n "" = n+            f n (c:rest) = f (n * 8 + digitToInt c) rest+    decodeEscapes (c:cs) = c : decodeEscapes cs+    decodeEscapes [] = []+ -- Is this token a string literal? isLiteral t = isJust $ getLiteralString t @@ -257,17 +293,27 @@         T_Annotation _ _ t -> getCommand t         _ -> Nothing --- Maybe get the command name of a token representing a command-getCommandName t = do+-- Maybe get the command name string of a token representing a command+getCommandName :: Token -> Maybe String+getCommandName = fst . getCommandNameAndToken++-- Get the command name token from a command, i.e.+-- the token representing 'ls' in 'ls -la 2> foo'.+-- If it can't be determined, return the original token.+getCommandTokenOrThis = snd . getCommandNameAndToken++getCommandNameAndToken :: Token -> (Maybe String, Token)+getCommandNameAndToken t = fromMaybe (Nothing, t) $ do     (T_SimpleCommand _ _ (w:rest)) <- getCommand t     s <- getLiteralString w     if "busybox" `isSuffixOf` s || "builtin" == s         then             case rest of-                (applet:_) -> getLiteralString applet-                _ -> return s+                (applet:_) -> return (getLiteralString applet, applet)+                _ -> return (Just s, w)         else-            return s+            return (Just s, w)+  -- If a command substitution is a single command, get its name. --  $(date +%s) = Just "date"
src/ShellCheck/Analytics.hs view
@@ -168,6 +168,8 @@     ,checkPipeToNowhere     ,checkForLoopGlobVariables     ,checkSubshelledTests+    ,checkInvertedStringTest+    ,checkRedirectionToCommand     ]  @@ -396,7 +398,7 @@             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 func (x:_) = func (getId $ getCommandTokenOrThis x)     first _ _ = return ()     hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)     hasParameter string =@@ -430,17 +432,22 @@ prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash" prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n" prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"+prop_checkShebang8 = verifyTree checkShebang "#!bin/sh\ntrue"+prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"+prop_checkShebang10= verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue" checkShebang params (T_Annotation _ list t) =     if any isOverride list then [] else checkShebang params t   where     isOverride (ShellOverride _) = True     isOverride _ = False-checkShebang params (T_Script id sb _) = execWriter $+checkShebang params (T_Script id sb _) = execWriter $ do     unless (shellTypeSpecified params) $ do         when (sb == "") $             err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."         when (executableFromShebang sb == "ash") $             warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."+    unless (null sb || "/" `isPrefixOf` sb) $+        err id 2239 "Ensure the shebang uses an absolute path to the interpreter."   prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"@@ -709,6 +716,7 @@ prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS" prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c" prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"+prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\"" checkArrayWithoutIndex params _ =     doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)   where@@ -1019,7 +1027,7 @@     error t =         unless (isConstantNonRe t) $             err (getId t) 2076-                "Don't quote rhs of =~, it'll match literally rather than as a regex."+                "Don't quote right-hand side of =~, it'll match literally rather than as a regex."     re = mkRegex "[][*.+()|]"     hasMetachars s = s `matches` re     isConstantNonRe t = fromMaybe False $ do@@ -1029,13 +1037,16 @@  prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]" prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"-prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]" prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]" prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"+prop_checkGlobbedRegex5 = verifyNot checkGlobbedRegex "[[ $foo =~ \\* ]]"+prop_checkGlobbedRegex6 = verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]"+prop_checkGlobbedRegex7 = verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]"+prop_checkGlobbedRegex8 = verifyNot checkGlobbedRegex "[[ $foo =~ x\\* ]]" checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =     let s = concat $ oversimplify rhs in         when (isConfusedGlobRegex s) $-            warn (getId rhs) 2049 "=~ is for regex. Use == for globs."+            warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead." checkGlobbedRegex _ _ = return ()  @@ -1186,11 +1197,12 @@ prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]" prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]" prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"+prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]" checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))     | op `elem` ["=", "==", "!="] =-        warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."+        warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching." checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)-        | (op == "=" || op == "==") && isGlob word =+        | op `elem` ["=", "==", "!="] && isGlob word =     err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement." checkComparisonAgainstGlob _ _ = return () @@ -1324,7 +1336,7 @@ prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)" prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo" checkBackticks _ (T_Backticked id list) | not (null list) =-    style id 2006 "Use $(..) instead of legacy `..`."+    style id 2006 "Use $(...) notation instead of legacy backticked `...`." checkBackticks _ _ = return ()  prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"@@ -1598,6 +1610,7 @@ prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done" prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1" prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"+prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"  checkSpacefulness params t =     doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)@@ -1879,6 +1892,7 @@ prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }" prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))" prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"+prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\"" checkUnassignedReferences params t = warnings   where     (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)@@ -2069,6 +2083,7 @@ prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done" prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done" prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"+prop_checkCdAndBack5 = verifyNot checkCdAndBack "cd ..; foo; cd .." checkCdAndBack params = doLists   where     shell = shellType params@@ -2091,10 +2106,20 @@     getCmd (T_Pipeline id _ [x]) = getCommandName x     getCmd _ = Nothing +    findCdPair list =+        case list of+            (a:b:rest) ->+                if isCdRevert b && not (isCdRevert a)+                then return $ getId b+                else findCdPair (b:rest)+            _ -> Nothing++     doList list =         let cds = filter ((== Just "cd") . getCmd) list in-            when (length cds >= 2 && isCdRevert (last cds)) $-               info (getId $ last cds) 2103 message+            potentially $ do+                cd <- findCdPair cds+                return $ info cd 2103 message      message = "Use a ( subshell ) to avoid having to cd back." @@ -2472,7 +2497,7 @@ prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo" checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =     unless ("r" `elem` map snd (getAllFlags t)) $-        info (getId t) 2162 "read without -r will mangle backslashes."+        info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes." checkReadWithoutR _ _ = return ()  prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"@@ -2501,6 +2526,7 @@ prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd" prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar" prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"+prop_checkUncheckedPopd9 = verifyNotTree checkUncheckedCdPushdPopd "popd -n foo"  checkUncheckedCdPushdPopd params root =     if hasSetE params then@@ -2510,7 +2536,7 @@     checkElement t@T_SimpleCommand {} =         when(name t `elem` ["cd", "pushd", "popd"]             && not (isSafeDir t)-            && not (name t == "pushd" && ("n" `elem` map snd (getAllFlags t)))+            && not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))             && not (isCondition $ getPath (parentMap params) t)) $                 warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."     checkElement _ = return ()@@ -2685,26 +2711,31 @@ prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac" prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac" prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac"+prop_checkUnmatchableCases9 = verifyNot checkUnmatchableCases "case $f in x) true;;& x) false;; esac" checkUnmatchableCases _ t =     case t of         T_CaseExpression _ word list -> do-            let patterns = concatMap snd3 list+            -- Check all patterns for whether they can ever match+            let allpatterns  = concatMap snd3 list+            -- Check only the non-fallthrough branches for shadowing+            let breakpatterns = concatMap snd3 $ filter (\x -> fst3 x == CaseBreak) list              if isConstant word                 then warn (getId word) 2194                         "This word is constant. Did you forget the $ on a variable?"                 else  potentially $ do                     pg <- wordToPseudoGlob word-                    return $ mapM_ (check pg) patterns+                    return $ mapM_ (check pg) allpatterns -            let exactGlobs = tupMap wordToExactPseudoGlob patterns-            let fuzzyGlobs = tupMap wordToPseudoGlob patterns+            let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns+            let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns             let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)              mapM_ checkDoms dominators          _ -> return ()   where+    fst3 (x,_,_) = x     snd3 (_,x,_) = x     check target candidate = potentially $ do         candidateGlob <- wordToPseudoGlob candidate@@ -2837,6 +2868,7 @@ prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)" prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls" prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"+prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin" checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity () checkPipeToNowhere _ t =     case t of@@ -2849,15 +2881,25 @@         name <- getCommandBasename cmd         guard $ name `elem` nonReadingCommands         guard . not $ hasAdditionalConsumers cmd+        -- Confusing echo for cat is so common that it's worth a special case+        let suggestion =+                if name == "echo"+                then "Did you want 'cat' instead?"+                else "Wrong command or missing xargs?"         return $ warn (getId cmd) 2216 $-            "Piping to '" ++ name ++ "', a command that doesn't read stdin. Wrong command or missing xargs?"+            "Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion      checkRedir cmd = potentially $ do         name <- getCommandBasename cmd         guard $ name `elem` nonReadingCommands         guard . not $ hasAdditionalConsumers cmd+        guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i"+        let suggestion =+                if name == "echo"+                then "Did you want 'cat' instead?"+                else "Bad quoting, wrong command or missing xargs?"         return $ warn (getId cmd) 2217 $-            "Redirecting to '" ++ name ++ "', a command that doesn't read stdin. Bad quoting or missing xargs?"+            "Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion      -- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?     hasAdditionalConsumers t = fromMaybe True $ do@@ -2926,9 +2968,10 @@ prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )" prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )" prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )"+prop_checkSubshelledTests4 = verify checkSubshelledTests "( [ a ] && { [ b ] && [ c ]; } )" checkSubshelledTests params t =     case t of-        T_Subshell id list | isSubshelledTest t ->+        T_Subshell id list | all isTestStructure list ->             case () of                 -- Special case for if (test) and while (test)                 _ | isCompoundCondition (getPath (parentMap params) t) ->@@ -2948,19 +2991,24 @@             [c] | isTestCommand c -> True             _ -> False -    isSubshelledTest t =+    isTestStructure t =         case t of-            T_Subshell _ list -> all isSubshelledTest list-            T_AndIf _ a b -> isSubshelledTest a && isSubshelledTest b-            T_OrIf  _ a b -> isSubshelledTest a && isSubshelledTest b-            T_Annotation _ _ t -> isSubshelledTest t+            T_Banged _ t -> isTestStructure t+            T_AndIf _ a b -> isTestStructure a && isTestStructure b+            T_OrIf  _ a b -> isTestStructure a && isTestStructure b+            T_Pipeline _ [] [T_Redirecting _ _ cmd] ->+                case cmd of+                    T_BraceGroup _ ts -> all isTestStructure ts+                    T_Subshell   _ ts -> all isTestStructure ts+                    _ -> isTestCommand t             _ -> isTestCommand t      isTestCommand t =         case t of-            T_Banged _ t -> isTestCommand t-            T_Pipeline _ [] [T_Redirecting _ _ T_Condition {}] -> True-            T_Pipeline _ [] [T_Redirecting _ _ cmd] -> cmd `isCommand` "test"+            T_Pipeline _ [] [T_Redirecting _ _ cmd] ->+                case cmd of+                    T_Condition {} -> True+                    _ -> cmd `isCommand` "test"             _ -> False      -- Check if a T_Subshell is used as a condition, e.g. if ( test )@@ -2980,6 +3028,36 @@             T_Pipeline _ [] _ -> True             T_Annotation {} -> True             _ -> False++prop_checkInvertedStringTest1 = verify checkInvertedStringTest "[ ! -z $var ]"+prop_checkInvertedStringTest2 = verify checkInvertedStringTest "! [[ -n $var ]]"+prop_checkInvertedStringTest3 = verifyNot checkInvertedStringTest "! [ -x $var ]"+prop_checkInvertedStringTest4 = verifyNot checkInvertedStringTest "[[ ! -w $var ]]"+prop_checkInvertedStringTest5 = verifyNot checkInvertedStringTest "[ -z $var ]"+checkInvertedStringTest _ t =+    case t of+        TC_Unary _ _ "!" (TC_Unary _ _ op _) ->+            case op of+                "-n" -> style (getId t) 2236 "Use -z instead of ! -n."+                "-z" -> style (getId t) 2236 "Use -n instead of ! -z."+                _ -> return ()+        T_Banged _ (T_Pipeline _ _+          [T_Redirecting _ _ (T_Condition _ _ (TC_Unary _ _ op _))]) ->+            case op of+                "-n" -> style (getId t) 2237 "Use [ -z .. ] instead of ! [ -n .. ]."+                "-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]."+                _ -> return ()+        _ -> return ()++prop_checkRedirectionToCommand1 = verify checkRedirectionToCommand "ls > rm"+prop_checkRedirectionToCommand2 = verifyNot checkRedirectionToCommand "ls > 'rm'"+prop_checkRedirectionToCommand3 = verifyNot checkRedirectionToCommand "ls > myfile"+checkRedirectionToCommand _ t =+    case t of+        T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands ->+            unless (str == "file") $ -- This would be confusing+                warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?"+        _ -> return ()  return [] runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
src/ShellCheck/Analyzer.hs view
@@ -30,7 +30,7 @@  -- TODO: Clean up the cruft this is layered on analyzeScript :: AnalysisSpec -> AnalysisResult-analyzeScript spec = AnalysisResult {+analyzeScript spec = newAnalysisResult {     arComments =         filterByAnnotation spec params . nub $             runAnalytics spec
src/ShellCheck/AnalyzerLib.hs view
@@ -109,19 +109,17 @@  data VariableState = Dead Token String | Alive deriving (Show) -defaultSpec root = AnalysisSpec {-    asScript = root,+defaultSpec root = spec {     asShellType = Nothing,     asCheckSourced = False,     asExecutionMode = Executed-}+} where spec = newAnalysisSpec root  pScript s =   let-    pSpec = ParseSpec {+    pSpec = newParseSpec {         psFilename = "script",-        psScript = s,-        psCheckSourced = False+        psScript = s     }   in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec @@ -135,7 +133,14 @@  makeComment :: Severity -> Id -> Code -> String -> TokenComment makeComment severity id code note =-    TokenComment id $ Comment severity code note+    newTokenComment {+        tcId = id,+        tcComment = newComment {+            cSeverity = severity,+            cCode = code,+            cMessage = note+        }+    }  addComment note = tell [note] @@ -235,9 +240,10 @@   where     pre t = modify (first ((:) t))     post t = do-        (_:rest, map) <- get-        case rest of []    -> put (rest, map)-                     (x:_) -> put (rest, Map.insert (getId t) x map)+        (x, map) <- get+        case x of+          _:rest -> case rest of []    -> put (rest, map)+                                 (x:_) -> put (rest, Map.insert (getId t) x map)  -- Given a root node, make a map from Id to Token getTokenMap :: Token -> Map.Map Id Token@@ -520,12 +526,22 @@  getReferencedVariableCommand _ = [] +-- The function returns a tuple consisting of four items describing an assignment.+-- Given e.g. declare foo=bar+-- (+--   BaseCommand :: Token,     -- The command/structure assigning the variable, i.e. declare foo=bar+--   AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar+--   VariableName :: String,   -- The variable name, i.e. foo+--   VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"+-- ) getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =    filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $     case x of         "read" ->-            let params = map getLiteral rest in-                catMaybes . takeWhile isJust . reverse $ params+            let params = map getLiteral rest+                readArrayVars = getReadArrayVariables rest+            in+                catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params         "getopts" ->             case rest of                 opts:var:_ -> maybeToList $ getLiteral var@@ -568,11 +584,15 @@       where         defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString -    getLiteral t = do+    getLiteralOfDataType t d = do         s <- getLiteralString t         when ("-" `isPrefixOf` s) $ fail "argument"-        return (base, t, s, DataString SourceExternal)+        return (base, t, s, d) +    getLiteral t = getLiteralOfDataType t (DataString SourceExternal)++    getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)+     getModifierParamString = getModifierParam DataString      getModifierParam def t@(T_Assignment _ _ name _ value) =@@ -613,6 +633,11 @@         guard $ isVariableName name         return (base, lastArg, name, DataArray SourceExternal) +    -- get all the array variables used in read, e.g. read -a arr+    getReadArrayVariables args = do+        map (getLiteralArray . snd)+            (filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))+ getModifiedVariableCommand _ = []  getIndexReferences s = fromMaybe [] $ do@@ -812,10 +837,9 @@     filter (not . shouldIgnore)   where     token = asScript asSpec-    idFor (TokenComment id _) = id     shouldIgnore note =         any (shouldIgnoreFor (getCode note)) $-            getPath parents (T_Bang $ idFor note)+            getPath parents (T_Bang $ tcId note)     shouldIgnoreFor num (T_Annotation _ anns _) =         any hasNum anns       where@@ -824,7 +848,7 @@     shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec     shouldIgnoreFor _ _ = False     parents = parentMap params-    getCode (TokenComment _ (Comment _ c _)) = c+    getCode = cCode . tcComment  -- Is this a ${#anything}, to get string length or array count? isCountingReference (T_DollarBraced id token) =
src/ShellCheck/Checker.hs view
@@ -37,25 +37,30 @@  import Test.QuickCheck.All -tokenToPosition map (TokenComment id c) = fromMaybe fail $ do-    position <- Map.lookup id map-    return $ PositionedComment position position c+tokenToPosition startMap t = fromMaybe fail $ do+    span <- Map.lookup (tcId t) startMap+    return $ newPositionedComment {+        pcStartPos = fst span,+        pcEndPos = snd span,+        pcComment = tcComment t+    }   where     fail = error "Internal shellcheck error: id doesn't exist. Please report!"  checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult checkScript sys spec = do     results <- checkScript (csScript spec)-    return CheckResult {+    return emptyCheckResult {         crFilename = csFilename spec,         crComments = results     }   where     checkScript contents = do-        result <- parseScript sys ParseSpec {+        result <- parseScript sys newParseSpec {             psFilename = csFilename spec,             psScript = contents,-            psCheckSourced = csCheckSourced spec+            psCheckSourced = csCheckSourced spec,+            psShellTypeOverride = csShellTypeOverride spec         }         let parseMessages = prComments result         let analysisMessages =@@ -66,27 +71,38 @@         return . nub . sortMessages . filter shouldInclude $             (parseMessages ++ map translator analysisMessages) -    shouldInclude (PositionedComment _ _ (Comment _ code _)) =-        code `notElem` csExcludedWarnings spec+    shouldInclude pc =+        let code     = cCode (pcComment pc)+            severity = cSeverity (pcComment pc)+        in+            code `notElem` csExcludedWarnings spec &&+            severity <= csMinSeverity spec      sortMessages = sortBy (comparing order)-    order (PositionedComment pos _ (Comment severity code message)) =-        (posFile pos, posLine pos, posColumn pos, severity, code, message)-    getPosition (PositionedComment pos _ _) = pos+    order pc =+        let pos = pcStartPos pc+            comment = pcComment pc in+        (posFile pos,+         posLine pos,+         posColumn pos,+         cSeverity comment,+         cCode comment,+         cMessage comment)+    getPosition = pcStartPos      analysisSpec root =-        AnalysisSpec {+        as {             asScript = root,             asShellType = csShellTypeOverride spec,             asCheckSourced = csCheckSourced spec,             asExecutionMode = Executed-         }+         } where as = newAnalysisSpec root  getErrors sys spec =     sort . map getCode . crComments $         runIdentity (checkScript sys spec)   where-    getCode (PositionedComment _ _ (Comment _ code _)) = code+    getCode = cCode . pcComment  check = checkWithIncludes [] @@ -136,6 +152,21 @@                     csExcludedWarnings = [2148, 1037]                 } +prop_wontParseBadShell =+    [1071] == check "#!/usr/bin/python\ntrue $1\n"++prop_optionDisablesBadShebang =+    null $ getErrors+                (mockedSystemInterface [])+                emptyCheckSpec {+                    csScript = "#!/usr/bin/python\ntrue\n",+                    csShellTypeOverride = Just Sh+                }++prop_annotationDisablesBadShebang =+    [] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"++ prop_canParseDevNull =     [] == check "source /dev/null" @@ -180,7 +211,7 @@ prop_filewideAnnotation2 = null $     check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1" prop_filewideAnnotation3 = null $-    check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"+    check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1" prop_filewideAnnotation4 = null $     check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" prop_filewideAnnotation5 = null $@@ -196,7 +227,6 @@  prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'     2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"-  return [] runTests = $quickCheckAll
src/ShellCheck/Checks/Commands.hs view
@@ -61,6 +61,7 @@     ,checkGrepRe     ,checkTrapQuotes     ,checkReturn+    ,checkExit     ,checkFindExecWithSingleArgument     ,checkUnusedEchoEscapes     ,checkInjectableFindSh@@ -92,6 +93,7 @@     ,checkWhich     ,checkSudoRedirect     ,checkSudoArgs+    ,checkSourceArgs     ]  buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)@@ -141,6 +143,7 @@ prop_checkTr9 = verifyNot checkTr "a-z n-za-m" prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr" prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"+prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'" checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)   where     f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?@@ -152,7 +155,7 @@         Just s -> do  -- Eliminate false positives by only looking for dupes in SET2?           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) $+          unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $             when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $               info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."         Nothing -> return ()@@ -183,7 +186,7 @@ checkNeedlessExpr = CommandCheck (Basename "expr") f where     f t =         when (all (`notElem` exceptions) (words $ arguments t)) $-            style (getId t) 2003+            style (getId $ getCommandTokenOrThis t) 2003                 "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."     -- These operators are hard to replicate in POSIX     exceptions = [ ":", "<", ">", "<=", ">=" ]@@ -279,15 +282,28 @@ prop_checkReturn5 = verify checkReturn "return -1" prop_checkReturn6 = verify checkReturn "return 1000" prop_checkReturn7 = verify checkReturn "return 'hello world'"-checkReturn = CommandCheck (Exactly "return") (f . arguments)+checkReturn = CommandCheck (Exactly "return") (returnOrExit+        (\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")+        (\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))++prop_checkExit1 = verifyNot checkExit "exit"+prop_checkExit2 = verifyNot checkExit "exit 1"+prop_checkExit3 = verifyNot checkExit "exit $var"+prop_checkExit4 = verifyNot checkExit "exit $((a|b))"+prop_checkExit5 = verify checkExit "exit -1"+prop_checkExit6 = verify checkExit "exit 1000"+prop_checkExit7 = verify checkExit "exit 'hello world'"+checkExit = CommandCheck (Exactly "exit") (returnOrExit+        (\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")+        (\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))++returnOrExit multi invalid = (f . arguments)   where     f (first:second:_) =-        err (getId second) 2151-            "Only one integer 0-255 can be returned. Use stdout for other data."+        multi (getId first)     f [value] =         when (isInvalid $ literal value) $-            err (getId value) 2152-                "Can only return 0-255. Other data should be written to stdout."+            invalid (getId value)     f _ = return ()      isInvalid s = s == "" || any (not . isDigit) s || length s > 5@@ -324,26 +340,18 @@ prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"-checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)+checkUnusedEchoEscapes = CommandCheck (Basename "echo") f   where-    isDashE = mkRegex "^-.*e"     hasEscapes = mkRegex "\\\\[rnt]"-    f args | concat (concatMap oversimplify allButLast) `matches` isDashE =-        return ()-      where allButLast = reverse . drop 1 . reverse $ args-    f args = mapM_ checkEscapes args--    checkEscapes (T_NormalWord _ args) =-        mapM_ checkEscapes args-    checkEscapes (T_DoubleQuoted id args) =-        mapM_ checkEscapes args-    checkEscapes (T_Literal id str) = examine id str-    checkEscapes (T_SingleQuoted id str) = examine id str-    checkEscapes _ = return ()+    f cmd =+        whenShell [Sh, Bash, Ksh] $+            unless (cmd `hasFlag` "e") $+                mapM_ examine $ arguments cmd -    examine id str =+    examine token = do+        let str = onlyLiteralString token         when (str `matches` hasEscapes) $-            info id 2028 "echo won't expand escape sequences. Consider printf."+            info (getId token) 2028 "echo may not expand escape sequences. Use printf."   prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"@@ -515,6 +523,9 @@ prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4" prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1" prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"+prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"+prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"+prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2" checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where     f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest     f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest@@ -525,11 +536,20 @@         case string of             '%':'%':rest -> countFormats rest             '%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)-            '%':'*':rest -> 2 + countFormats rest -- width is specified as an argument-            '%':rest -> 1 + countFormats rest+            '%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)             _:rest -> countFormats rest             [] -> 0 +    regexBasedCountFormats rest =+        maybe 1 (foldl (\acc group -> acc + (if group == "*" then 1 else 0)) 1) (matchRegex re rest)+      where+        -- constructed based on specifications in "man printf"+        re = mkRegex "#?-?\\+? ?0?(\\*|\\d*).?(\\d*|\\*)[diouxXfFeEgGaAcsb]"+        --            \____ _____/\___ ____/ \____ ____/\________ ________/+        --                 V          V           V               V+        --               flags    field width  precision   format character+        -- field width and precision can be specified with a '*' instead of a digit,+        -- in which case printf will accept one more argument for each '*' used     check format more = do         fromMaybe (return ()) $ do             string <- getLiteralString format@@ -678,20 +698,24 @@ prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print" prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ." prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."+prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help"+prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print" checkFindWithoutPath = CommandCheck (Basename "find") f   where-    f (T_SimpleCommand _ _ (cmd:args)) =-        unless (hasPath args) $+    f t@(T_SimpleCommand _ _ (cmd:args)) =+        unless (t `hasFlag` "help" || hasPath args) $             info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly." -    -- This is a bit of a kludge. find supports flag arguments both before and after the path,-    -- as well as multiple non-flag arguments that are not the path. We assume that all the-    -- pre-path flags are single characters, which is generally the case except for -O3.+    -- This is a bit of a kludge. find supports flag arguments both before and+    -- after the path, as well as multiple non-flag arguments that are not the+    -- path. We assume that all the pre-path flags are single characters from a+    -- list of GNU and macOS flags.     hasPath (first:rest) =         let flag = fromJust $ getLiteralStringExt (const $ return "___") first in             not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest     hasPath [] = False-    isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag+    isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag+    leadingFlagChars="-EHLPXdfsxO0123456789"   prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"@@ -740,20 +764,20 @@     whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local         path <- getPathM t         unless (any isFunction path) $-            err (getId t) 2168 "'local' is only valid in functions."+            err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."  prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)" prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)" checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $-    \t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."+    \t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."  prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'" checkDeprecatedEgrep = CommandCheck (Basename "egrep") $-    \t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."+    \t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."  prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files" checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $-    \t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."+    \t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."  prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done" prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"@@ -947,7 +971,7 @@  prop_checkWhich = verify checkWhich "which '.+'" checkWhich = CommandCheck (Basename "which") $-    \t -> info (getId t) 2230 "which is non-standard. Use builtin 'command -v' instead."+    \t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."  prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"@@ -998,6 +1022,17 @@     builtins = [ "cd", "eval", "export", "history", "read", "source", "wait" ]     -- This mess is why ShellCheck prefers not to know.     parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"++prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg"+prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script"+prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg"+checkSourceArgs = CommandCheck (Exactly ".") f+  where+    f t = whenShell [Sh, Dash] $+        case arguments t of+            (file:arg1:_) -> warn (getId arg1) 2240 $+                "The dot command does not support arguments in sh/dash. Set them as variables."+            _ -> return ()  return [] runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
src/ShellCheck/Checks/ShellSupport.hs view
@@ -127,7 +127,7 @@ prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT" prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT" prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"-prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"+prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO" prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE" prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file" prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"@@ -302,11 +302,11 @@         (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")         ]     bashVars = [-        "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",+        "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",         "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"         ]     bashDynamicVars = [ "RANDOM", "SECONDS" ]-    dashVars = [ "LINENO" ]+    dashVars = [ ]     isBashVariable var =         (var `elem` bashDynamicVars             || var `elem` bashVars && not (isAssigned var))
src/ShellCheck/Data.hs view
@@ -39,7 +39,7 @@   ]  variablesWithoutSpaces = [-    "$", "-", "?", "!",+    "$", "-", "?", "!", "#",     "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",     "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",     "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
src/ShellCheck/Formatter/Format.hs view
@@ -30,17 +30,17 @@     footer :: IO () } -sourceFile (PositionedComment pos _ _) = posFile pos-lineNo (PositionedComment pos _ _) = posLine pos-endLineNo (PositionedComment _ end _) = posLine end-colNo  (PositionedComment pos _ _) = posColumn pos-endColNo  (PositionedComment _ end _) = posColumn end-codeNo (PositionedComment _ _ (Comment _ code _)) = code-messageText (PositionedComment _ _ (Comment _ _ t)) = t+sourceFile = posFile . pcStartPos+lineNo = posLine . pcStartPos+endLineNo = posLine . pcEndPos+colNo  = posColumn . pcStartPos+endColNo = posColumn . pcEndPos+codeNo = cCode . pcComment+messageText = cMessage . pcComment  severityText :: PositionedComment -> String-severityText (PositionedComment _ _ (Comment c _ _)) =-    case c of+severityText pc =+    case cSeverity (pcComment pc) of         ErrorC   -> "error"         WarningC -> "warning"         InfoC    -> "info"@@ -51,11 +51,14 @@     map fix comments   where     ls = lines contents-    fix c@(PositionedComment start end comment) = PositionedComment start {-        posColumn = realignColumn lineNo colNo c-    } end {-        posColumn = realignColumn endLineNo endColNo c-    } comment+    fix c = c {+        pcStartPos = (pcStartPos c) {+            posColumn = realignColumn lineNo colNo c+        }+      , pcEndPos = (pcEndPos c) {+            posColumn = realignColumn endLineNo endColNo c+        }+    }     realignColumn lineNo colNo c =       if lineNo c > 0 && lineNo c <= fromIntegral (length ls)       then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
src/ShellCheck/Formatter/JSON.hs view
@@ -40,7 +40,10 @@     }  instance ToJSON (PositionedComment) where-  toJSON comment@(PositionedComment start end (Comment level code string)) =+  toJSON comment =+    let start = pcStartPos comment+        end = pcEndPos comment+        c = pcComment comment in     object [       "file" .= posFile start,       "line" .= posLine start,@@ -48,11 +51,14 @@       "column" .= posColumn start,       "endColumn" .= posColumn end,       "level" .= severityText comment,-      "code" .= code,-      "message" .= string+      "code" .= cCode c,+      "message" .= cMessage c     ] -  toEncoding comment@(PositionedComment start end (Comment level code string)) =+  toEncoding comment =+    let start = pcStartPos comment+        end = pcEndPos comment+        c = pcComment comment in     pairs (          "file" .= posFile start       <> "line" .= posLine start@@ -60,8 +66,8 @@       <> "column" .= posColumn start       <> "endColumn" .= posColumn end       <> "level" .= severityText comment-      <> "code" .= code-      <> "message" .= string+      <> "code" .= cCode c+      <> "message" .= cMessage c     )  outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
src/ShellCheck/Formatter/TTY.hs view
@@ -22,18 +22,27 @@ import ShellCheck.Interface import ShellCheck.Formatter.Format +import Control.Monad+import Data.IORef import Data.List import GHC.Exts-import System.Info import System.IO+import System.Info +wikiLink = "https://www.shellcheck.net/wiki/"++-- An arbitrary Ord thing to order warnings+type Ranking = (Char, Severity, Integer)+ format :: FormatterOptions -> IO Formatter-format options = return Formatter {-    header = return (),-    footer = return (),-    onFailure = outputError options,-    onResult = outputResult options-}+format options = do+    topErrorRef <- newIORef []+    return Formatter {+        header = return (),+        footer = outputWiki topErrorRef,+        onFailure = outputError options,+        onResult = outputResult options topErrorRef+    }  colorForLevel level =     case level of@@ -45,13 +54,60 @@         "source"  -> 0 -- none         _ -> 0         -- none +rankError :: PositionedComment -> Ranking+rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)+  where+    ranking =+        if cCode (pcComment err) `elem` uninteresting+        then 'Z'+        else 'A'++    -- A list of the most generic, least directly helpful+    -- error codes to downrank.+    uninteresting = [+        1009, -- Mentioned parser error was..+        1019, -- Expected this to be an argument+        1036, -- ( is invalid here+        1047, -- Expected 'fi'+        1062, -- Expected 'done'+        1070, -- Parsing stopped here (generic)+        1072, -- Missing/unexpected ..+        1073, -- Couldn't parse this ..+        1088, -- Parsing stopped here (paren)+        1089  -- Parsing stopped here (keyword)+        ]++appendComments errRef comments max = do+    previous <- readIORef errRef+    let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments+    writeIORef errRef . take max . nubBy equal . sort $ previous ++ current+  where+    fst3 (x,_,_) = x+    equal x y = fst3 x == fst3 y++outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()+outputWiki errRef = do+    issues <- readIORef errRef+    unless (null issues) $ do+        putStrLn "For more information:"+        mapM_ showErr issues+  where+    showErr (_, code, msg) =+        putStrLn $ "  " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg+    limit = 36+    shorten msg =+        if length msg < limit+        then msg+        else (take (limit-3) msg) ++ "..."+ outputError options file error = do     color <- getColorFunc $ foColorOption options     hPutStrLn stderr $ color "error" $ file ++ ": " ++ error -outputResult options result sys = do+outputResult options ref result sys = do     color <- getColorFunc $ foColorOption options     let comments = crComments result+    appendComments ref comments (fromIntegral $ foWikiLinkCount options)     let fileGroups = groupWith sourceFile comments     mapM_ (outputForFile color sys) fileGroups @@ -78,9 +134,16 @@ cuteIndent :: PositionedComment -> String cuteIndent comment =     replicate (fromIntegral $ colNo comment - 1) ' ' ++-        "^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment+        makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment+  where+    arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"+    makeArrow =+        let sameLine = lineNo comment == endLineNo comment+            delta = endColNo comment - colNo comment+        in+            if sameLine && delta > 2 && delta < 32 then arrow delta else "^--" -code code = "SC" ++ show code+code num = "SC" ++ show num  getColorFunc colorOption = do     term <- hIsTerminalDevice stdout
src/ShellCheck/Interface.hs view
@@ -17,7 +17,39 @@     You should have received a copy of the GNU General Public License     along with this program.  If not, see <https://www.gnu.org/licenses/>. -}-module ShellCheck.Interface where+module ShellCheck.Interface+    (+    SystemInterface(..)+    , CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)+    , CheckResult(crFilename, crComments)+    , ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)+    , ParseResult(prComments, prTokenPositions, prRoot)+    , AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced)+    , AnalysisResult(arComments)+    , FormatterOptions(foColorOption, foWikiLinkCount)+    , Shell(Ksh, Sh, Bash, Dash)+    , ExecutionMode(Executed, Sourced)+    , ErrorMessage+    , Code+    , Severity(ErrorC, WarningC, InfoC, StyleC)+    , Position(posFile, posLine, posColumn)+    , Comment(cSeverity, cCode, cMessage)+    , PositionedComment(pcStartPos , pcEndPos , pcComment)+    , ColorOption(ColorAuto, ColorAlways, ColorNever)+    , TokenComment(tcId, tcComment)+    , emptyCheckResult+    , newParseResult+    , newAnalysisSpec+    , newAnalysisResult+    , newFormatterOptions+    , newPosition+    , newTokenComment+    , mockedSystemInterface+    , newParseSpec+    , emptyCheckSpec+    , newPositionedComment+    , newComment+    ) where  import ShellCheck.AST import Control.Monad.Identity@@ -35,7 +67,8 @@     csScript :: String,     csCheckSourced :: Bool,     csExcludedWarnings :: [Integer],-    csShellTypeOverride :: Maybe Shell+    csShellTypeOverride :: Maybe Shell,+    csMinSeverity :: Severity } deriving (Show, Eq)  data CheckResult = CheckResult {@@ -43,28 +76,51 @@     crComments :: [PositionedComment] } deriving (Show, Eq) +emptyCheckResult :: CheckResult+emptyCheckResult = CheckResult {+    crFilename = "",+    crComments = []+}+ emptyCheckSpec :: CheckSpec emptyCheckSpec = CheckSpec {     csFilename = "",     csScript = "",     csCheckSourced = False,     csExcludedWarnings = [],-    csShellTypeOverride = Nothing+    csShellTypeOverride = Nothing,+    csMinSeverity = StyleC } +newParseSpec :: ParseSpec+newParseSpec = ParseSpec {+    psFilename = "",+    psScript = "",+    psCheckSourced = False,+    psShellTypeOverride = Nothing+}+ -- Parser input and output data ParseSpec = ParseSpec {     psFilename :: String,     psScript :: String,-    psCheckSourced :: Bool+    psCheckSourced :: Bool,+    psShellTypeOverride :: Maybe Shell } deriving (Show, Eq)  data ParseResult = ParseResult {     prComments :: [PositionedComment],-    prTokenPositions :: Map.Map Id Position,+    prTokenPositions :: Map.Map Id (Position, Position),     prRoot :: Maybe Token } deriving (Show, Eq) +newParseResult :: ParseResult+newParseResult = ParseResult {+    prComments = [],+    prTokenPositions = Map.empty,+    prRoot = Nothing+}+ -- Analyzer input and output data AnalysisSpec = AnalysisSpec {     asScript :: Token,@@ -73,17 +129,33 @@     asCheckSourced :: Bool } +newAnalysisSpec token = AnalysisSpec {+    asScript = token,+    asShellType = Nothing,+    asExecutionMode = Executed,+    asCheckSourced = False+}+ newtype AnalysisResult = AnalysisResult {     arComments :: [TokenComment] } +newAnalysisResult = AnalysisResult {+    arComments = []+}  -- Formatter options-newtype FormatterOptions = FormatterOptions {-    foColorOption :: ColorOption+data FormatterOptions = FormatterOptions {+    foColorOption :: ColorOption,+    foWikiLinkCount :: Integer } +newFormatterOptions = FormatterOptions {+    foColorOption = ColorAuto,+    foWikiLinkCount = 3+} + -- Supporting data types data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq)@@ -98,9 +170,48 @@     posColumn :: Integer  -- 1 based source column, where tabs are 8 } deriving (Show, Eq) -data Comment = Comment Severity Code String deriving (Show, Eq)-data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)-data TokenComment = TokenComment Id Comment deriving (Show, Eq)+newPosition :: Position+newPosition = Position {+    posFile   = "",+    posLine   = 1,+    posColumn = 1+}++data Comment = Comment {+    cSeverity :: Severity,+    cCode     :: Code,+    cMessage  :: String+} deriving (Show, Eq)++newComment :: Comment+newComment = Comment {+    cSeverity = StyleC,+    cCode     = 0,+    cMessage  = ""+}++data PositionedComment = PositionedComment {+    pcStartPos :: Position,+    pcEndPos   :: Position,+    pcComment  :: Comment+} deriving (Show, Eq)++newPositionedComment :: PositionedComment+newPositionedComment = PositionedComment {+    pcStartPos = newPosition,+    pcEndPos   = newPosition,+    pcComment  = newComment+}++data TokenComment = TokenComment {+    tcId :: Id,+    tcComment :: Comment+} deriving (Show, Eq)++newTokenComment = TokenComment {+    tcId = Id 0,+    tcComment = newComment+}  data ColorOption =     ColorAuto
src/ShellCheck/Parser.hs view
@@ -66,7 +66,7 @@ variableStart = upper <|> lower <|> oneOf "_" variableChars = upper <|> lower <|> digit <|> oneOf "_" -- Chars to allow in function names-functionChars = variableChars <|> oneOf ":+?-./^"+functionChars = variableChars <|> oneOf ":+?-./^@" -- Chars to allow in functions using the 'function' keyword extendedFunctionChars = functionChars <|> oneOf "[]*=!" specialVariable = oneOf "@*#?-$!"@@ -115,10 +115,10 @@     when (null s) $ fail "Expected whitespace"  readUnicodeQuote = do-    pos <- getPosition+    start <- startSpan     c <- oneOf (unicodeSingleQuotes ++ unicodeDoubleQuotes)-    parseProblemAt pos WarningC 1110 "This is a unicode quote. Delete and retype it (or quote to make literal)."-    id <- getNextIdAt pos+    id <- endSpan start+    parseProblemAtId id WarningC 1110 "This is a unicode quote. Delete and retype it (or quote to make literal)."     return $ T_Literal id [c]  carriageReturn = do@@ -151,7 +151,7 @@  data UserState = UserState {     lastId :: Id,-    positionMap :: Map.Map Id SourcePos,+    positionMap :: Map.Map Id (SourcePos, SourcePos),     parseNotes :: [ParseNote],     hereDocMap :: Map.Map Id [Token],     pendingHereDocs :: [HereDocContext]@@ -172,10 +172,10 @@  getLastId = lastId <$> getState -getNextIdAt sourcepos = do+getNextIdBetween startPos endPos = do     state <- getState     let newId = incId (lastId state)-    let newMap = Map.insert newId sourcepos (positionMap state)+    let newMap = Map.insert newId (startPos, endPos) (positionMap state)     putState $ state {         lastId = newId,         positionMap = newMap@@ -183,11 +183,39 @@     return newId   where incId (Id n) = Id $ n+1 -getNextId :: Monad m => SCParser m Id-getNextId = do-    pos <- getPosition-    getNextIdAt pos+getNextIdSpanningTokens startTok endTok = do+    (start, _) <- getSpanForId (getId startTok)+    (_, end)   <- getSpanForId (getId endTok)+    getNextIdBetween start end +-- Get an ID starting from the first token of the list, and ending after the last+getNextIdSpanningTokenList list =+    if null list+    then do+        pos <- getPosition+        getNextIdBetween pos pos+    else+        getNextIdSpanningTokens (head list) (last list)++-- Get the span covered by an id+getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)+getSpanForId id =+    Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$>+        getMap++-- Create a new id with the same span as an existing one+getNewIdFor :: Monad m => Id -> SCParser m Id+getNewIdFor id = getSpanForId id >>= uncurry getNextIdBetween++data IncompleteInterval = IncompleteInterval SourcePos++startSpan = IncompleteInterval <$> getPosition++endSpan (IncompleteInterval start) = do+    endPos <- getPosition+    id <- getNextIdBetween start endPos+    return id+ addToHereDocMap id list = do     state <- getState     let map = hereDocMap state@@ -277,7 +305,8 @@  data Environment m = Environment {     systemInterface :: SystemInterface m,-    checkSourced :: Bool+    checkSourced :: Bool,+    shellTypeOverride :: Maybe Shell }  parseProblem level code msg = do@@ -317,10 +346,8 @@  parseProblemAtId :: Monad m => Id -> Severity -> Integer -> String -> SCParser m () parseProblemAtId id level code msg = do-    map <- getMap-    let pos = Map.findWithDefault-                (error "Internal error (no position for id). Please report.") id map-    parseProblemAt pos level code msg+    (start, end) <- getSpanForId id+    parseProblemAtWithEnd start end level code msg  -- Store non-parse problems inside @@ -329,6 +356,9 @@     parseNoteAt pos c l a  parseNoteAt pos c l a = addParseNote $ ParseNote pos pos c l a+parseNoteAtId id c l a = do+    (start, end) <- getSpanForId id+    addParseNote $ ParseNote start end c l a  parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a @@ -364,8 +394,9 @@  -- Construct a node with a parser, e.g. T_Literal `withParser` (readGenericLiteral ",") withParser node parser = do-    id <- getNextId+    start <- startSpan     contents <- parser+    id <- endSpan start     return $ node id contents  wasIncluded p = option False (p >> return True)@@ -416,7 +447,6 @@     typ = if single then SingleBracket else DoubleBracket     readCondBinaryOp = try $ do         optional guardArithmetic-        id <- getNextId         op <- getOp         spacingOrLf         return op@@ -424,8 +454,9 @@         flaglessOps = [ "==", "!=", "<=", ">=", "=~", ">", "<", "=" ]          getOp = do-            id <- getNextId+            start <- startSpan             op <- readRegularOrEscaped anyOp+            id <- endSpan start             return $ TC_Binary id typ op          anyOp = flagOp <|> flaglessOp <|> fail@@ -468,8 +499,9 @@           return "Expected an argument for the unary operator"      readCondUnaryOp = try $ do-        id <- getNextId+        start <- startSpan         s <- readOp+        id <- endSpan start         spacingOrLf         return $ TC_Unary id typ s @@ -489,7 +521,7 @@         notFollowedBy2 (try (spacing >> string "]"))         x <- readNormalWord         pos <- getPosition-        when (endedWith "]" x) $ do+        when (endedWith "]" x && notArrayIndex x) $ do             parseProblemAt pos ErrorC 1020 $                 "You need a space before the " ++ (if single then "]" else "]]") ++ "."             fail "Missing space before ]"@@ -503,26 +535,25 @@                 case last s of T_Literal id s -> str `isSuffixOf` s                                _ -> False             endedWith _ _ = False+            notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["+            notArrayIndex _ = True -    readCondAndOp = do-        id <- getNextId-        x <- try (readAndOrOp "&&" False <|> readAndOrOp "-a" True)-        return $ TC_And id typ x+    readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True      readCondOrOp = do         optional guardArithmetic-        id <- getNextId-        x <- try (readAndOrOp "||" False <|> readAndOrOp "-o" True)-        return $ TC_Or id typ x+        readAndOrOp TC_Or "||" False <|> readAndOrOp TC_Or "-o" True -    readAndOrOp op requiresSpacing = do+    readAndOrOp node op requiresSpacing = do         optional $ lookAhead weirdDash-        x <- string op+        start <- startSpan+        x <- try $ string op+        id <- endSpan start         condSpacing requiresSpacing-        return x+        return $ node id typ x      readCondNullaryOrBinary = do-      id <- getNextId+      start <- startSpan       x <- readCondWord `attempting` (do               pos <- getPosition               lookAhead (char '[')@@ -530,6 +561,7 @@                   then "If grouping expressions inside [..], use \\( ..\\)."                   else "If grouping expressions inside [[..]], use ( .. )."             )+      id <- endSpan start       (do             pos <- getPosition             isRegex <- regexOperatorAhead@@ -550,7 +582,7 @@             "You need a space before and after the " ++ trailingOp ++ " ."      readCondGroup = do-        id <- getNextId+        start <- startSpan         pos <- getPosition         lparen <- try $ readRegularOrEscaped (string "(")         when (single && lparen == "(") $@@ -561,6 +593,7 @@         x <- readCondContents         cpos <- getPosition         rparen <- readRegularOrEscaped (string ")")+        id <- endSpan start         condSpacing single         when (single && rparen == ")") $             singleWarning cpos@@ -581,8 +614,9 @@         return True)           <|> return False     readRegex = called "regex" $ do-        id <- getNextId+        start <- startSpan         parts <- many1 readPart+        id <- endSpan start         void spacing         return $ T_NormalWord id parts       where@@ -591,27 +625,31 @@             readSingleQuoted,             readDoubleQuoted,             readDollarExpression,-            readNormalLiteral "( ",-            readPipeLiteral,+            readLiteralForParser $ readNormalLiteral "( ",+            readLiteralString "|",             readGlobLiteral             ]         readGlobLiteral = do-            id <- getNextId+            start <- startSpan             s <- extglobStart <|> oneOf "{}[]$"+            id <- endSpan start             return $ T_Literal id [s]         readGroup = called "regex grouping" $ do-            id <- getNextId-            char '('+            start <- startSpan+            p1 <- readLiteralString "("             parts <- many (readPart <|> readRegexLiteral)-            char ')'-            return $ T_NormalWord id parts+            p2 <- readLiteralString ")"+            id <- endSpan start+            return $ T_NormalWord id (p1:(parts ++ [p2]))         readRegexLiteral = do-            id <- getNextId+            start <- startSpan             str <- readGenericLiteral1 (singleQuote <|> doubleQuotable <|> oneOf "()")+            id <- endSpan start             return $ T_Literal id str-        readPipeLiteral = do-            id <- getNextId-            str <- string "|"+        readLiteralString s = do+            start <- startSpan+            str <- string s+            id <- endSpan start             return $ T_Literal id str      readCondTerm = do@@ -620,8 +658,9 @@         return term      readCondNot = do-        id <- getNextId+        start <- startSpan         char '!'+        id <- endSpan start         spacingOrLf         expr <- readCondExpr         return $ TC_Unary id typ "!" expr@@ -656,6 +695,7 @@ prop_a20= isOk readArithmeticContents "a ? b ? c : d : e" prop_a21= isOk readArithmeticContents "a ? b : c ? d : e" prop_a22= isOk readArithmeticContents "!!a"+prop_a23= isOk readArithmeticContents "~0" readArithmeticContents :: Monad m => SCParser m Token readArithmeticContents =     readSequence@@ -667,12 +707,13 @@     splitBy x ops = chainl1 x (readBinary ops)     readBinary ops = readComboOp ops TA_Binary     readComboOp op token = do-        id <- getNextId+        start <- startSpan         op <- choice (map (\x -> try $ do                                         s <- string x                                         failIfIncompleteOp                                         return s                             ) op)+        id <- endSpan start         spacing         return $ token id op @@ -680,7 +721,7 @@      -- Read binary minus, but also check for -lt, -gt and friends:     readMinusOp = do-        id <- getNextId+        start <- startSpan         pos <- getPosition         try $ do             char '-'@@ -695,6 +736,7 @@                 ("ne", "!=")               ]             parseProblemAt pos ErrorC 1106 $ "In arithmetic contexts, use " ++ alt ++ " instead of -" ++ str+        id <- endSpan start         spacing         return $ TA_Binary id "-"       where@@ -704,27 +746,30 @@             return (str, alt)      readArrayIndex = do-        id <- getNextId+        start <- startSpan         char '['         pos <- getPosition         middle <- readStringForParser readArithmeticContents         char ']'+        id <- endSpan start         return $ T_UnparsedIndex id pos middle      literal s = do-        id <- getNextId+        start <- startSpan         string s+        id <- endSpan start         return $ T_Literal id s      readVariable = do-        id <- getNextId+        start <- startSpan         name <- readVariableName         indices <- many readArrayIndex+        id <- endSpan start         spacing         return $ TA_Variable id name indices      readExpansion = do-        id <- getNextId+        start <- startSpan         pieces <- many1 $ choice [             readSingleQuoted,             readDoubleQuoted,@@ -734,6 +779,7 @@             literal "#",             readNormalLiteral "+-*/=%^,]?:"             ]+        id <- endSpan start         spacing         return $ TA_Expansion id pieces @@ -748,8 +794,9 @@      readSequence = do         spacing-        id <- getNextId+        start <- startSpan         l <- readAssignment `sepBy` (char ',' >> spacing)+        id <- endSpan start         return $ TA_Sequence id l      readAssignment = chainr1 readTrinary readAssignmentOp@@ -758,13 +805,14 @@     readTrinary = do         x <- readLogicalOr         do-            id <- getNextId+            start <- startSpan             string "?"             spacing             y <- readTrinary             string ":"             spacing             z <- readTrinary+            id <- endSpan start             return $ TA_Trinary id x y z          <|>           return x@@ -783,16 +831,18 @@      readAnyNegated = readNegated <|> readAnySigned     readNegated = do-        id <- getNextId+        start <- startSpan         op <- oneOf "!~"+        id <- endSpan start         spacing         x <- readAnyNegated         return $ TA_Unary id [op] x      readAnySigned = readSigned <|> readAnycremented     readSigned = do-        id <- getNextId+        start <- startSpan         op <- choice (map readSignOp "+-")+        id <- endSpan start         spacing         x <- readAnycremented         return $ TA_Unary id [op] x@@ -805,8 +855,9 @@      readAnycremented = readNormalOrPostfixIncremented <|> readPrefixIncremented     readPrefixIncremented = do-        id <- getNextId+        start <- startSpan         op <- try $ string "++" <|> string "--"+        id <- endSpan start         spacing         x <- readArithTerm         return $ TA_Unary id (op ++ "|") x@@ -815,8 +866,9 @@         x <- readArithTerm         spacing         do-            id <- getNextId+            start <- startSpan             op <- try $ string "++" <|> string "--"+            id <- endSpan start             spacing             return $ TA_Unary id ('|':op) x          <|>@@ -824,35 +876,36 @@   -prop_readCondition = isOk readCondition "[ \\( a = b \\) -a \\( c = d \\) ]"-prop_readCondition2 = isOk readCondition "[[ (a = b) || (c = d) ]]"-prop_readCondition3 = isOk readCondition "[[ $c = [[:alpha:].~-] ]]"-prop_readCondition4 = isOk readCondition "[[ $c =~ *foo* ]]"-prop_readCondition5 = isOk readCondition "[[ $c =~ f( ]] )* ]]"-prop_readCondition5a= isOk readCondition "[[ $c =~ a(b) ]]"-prop_readCondition5b= isOk readCondition "[[ $c =~ f( ($var ]]) )* ]]"-prop_readCondition6 = isOk readCondition "[[ $c =~ ^[yY]$ ]]"-prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"-prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"-prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"-prop_readCondition10= isOk readCondition "[[\na == b\n||\nc == d ]]"+prop_readCondition   = isOk readCondition "[ \\( a = b \\) -a \\( c = d \\) ]"+prop_readCondition2  = isOk readCondition "[[ (a = b) || (c = d) ]]"+prop_readCondition3  = isOk readCondition "[[ $c = [[:alpha:].~-] ]]"+prop_readCondition4  = isOk readCondition "[[ $c =~ *foo* ]]"+prop_readCondition5  = isOk readCondition "[[ $c =~ f( ]] )* ]]"+prop_readCondition5a = isOk readCondition "[[ $c =~ a(b) ]]"+prop_readCondition5b = isOk readCondition "[[ $c =~ f( ($var ]]) )* ]]"+prop_readCondition6  = isOk readCondition "[[ $c =~ ^[yY]$ ]]"+prop_readCondition7  = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"+prop_readCondition8  = isOk readCondition "[[ $l =~ ogg|flac ]]"+prop_readCondition9  = isOk readCondition "[ foo -a -f bar ]"+prop_readCondition10 = isOk readCondition "[[\na == b\n||\nc == d ]]" prop_readCondition10a= isOk readCondition "[[\na == b  ||\nc == d ]]" prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"-prop_readCondition11= isOk readCondition "[[ a == b ||\n c == d ]]"-prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]"-prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"-prop_readCondition14= isOk readCondition "[ foo '>' bar ]"-prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]"-prop_readCondition16= isOk readCondition "[ foo \\< bar ]"-prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"-prop_readCondition18= isOk readCondition "[ ]"-prop_readCondition19= isOk readCondition "[ '(' x \")\" ]"-prop_readCondition20= isOk readCondition "[[ echo_rc -eq 0 ]]"-prop_readCondition21= isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"-prop_readCondition22= isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"+prop_readCondition11 = isOk readCondition "[[ a == b ||\n c == d ]]"+prop_readCondition12 = isWarning readCondition "[ a == b \n -o c == d ]"+prop_readCondition13 = isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"+prop_readCondition14 = isOk readCondition "[ foo '>' bar ]"+prop_readCondition15 = isOk readCondition "[ foo \">=\" bar ]"+prop_readCondition16 = isOk readCondition "[ foo \\< bar ]"+prop_readCondition17 = isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"+prop_readCondition18 = isOk readCondition "[ ]"+prop_readCondition19 = isOk readCondition "[ '(' x \")\" ]"+prop_readCondition20 = isOk readCondition "[[ echo_rc -eq 0 ]]"+prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"+prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"+prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]" readCondition = called "test expression" $ do     opos <- getPosition-    id <- getNextId+    start <- startSpan     open <- try (string "[[") <|> string "["     let single = open == "["     let typ = if single then SingleBracket else DoubleBracket@@ -870,11 +923,12 @@     condition <- readConditionContents single <|> do         guard . not . null $ space         lookAhead $ string "]"-        id <- getNextIdAt pos+        id <- endSpan start         return $ TC_Empty id typ      cpos <- getPosition     close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"+    id <- endSpan start     when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"     when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"     spacing@@ -963,24 +1017,28 @@ readNormalWord = readNormalishWord ""  readNormalishWord end = do-    id <- getNextId+    start <- startSpan     pos <- getPosition     x <- many1 (readNormalWordPart end)+    id <- endSpan start     checkPossibleTermination pos x     return $ T_NormalWord id x  readIndexSpan = do-    id <- getNextId+    start <- startSpan     x <- many (readNormalWordPart "]" <|> someSpace <|> otherLiteral)+    id <- endSpan start     return $ T_NormalWord id x   where     someSpace = do-        id <- getNextId+        start <- startSpan         str <- spacing1+        id <- endSpan start         return $ T_Literal id str     otherLiteral = do-        id <- getNextId+        start <- startSpan         str <- many1 $ oneOf quotableChars+        id <- endSpan start         return $ T_Literal id str  checkPossibleTermination pos [T_Literal _ x] =@@ -1011,8 +1069,9 @@             parseProblemAt pos ErrorC 1036 "'(' is invalid here. Did you forget to escape it?"      readLiteralCurlyBraces = do-        id <- getNextId+        start <- startSpan         str <- findParam <|> literalBraces+        id <- endSpan start         return $ T_Literal id str      findParam = try $ string "{}"@@ -1025,13 +1084,15 @@   readSpacePart = do-    id <- getNextId+    start <- startSpan     x <- many1 whitespace+    id <- endSpan start     return $ T_Literal id x  readDollarBracedWord = do-    id <- getNextId+    start <- startSpan     list <- many readDollarBracedPart+    id <- endSpan start     return $ T_NormalWord id list  readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>@@ -1039,19 +1100,22 @@                        readUnquotedBackTicked <|> readDollarBracedLiteral  readDollarBracedLiteral = do-    id <- getNextId+    start <- startSpan     vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable+    id <- endSpan start     return $ T_Literal id $ concat vars  readParamSubSpecialChar = do-    id <- getNextId-    T_ParamSubSpecialChar id <$> many1 paramSubSpecialChars+    start <- startSpan+    x <- many1 paramSubSpecialChars+    id <- endSpan start+    return $ T_ParamSubSpecialChar id x  prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)" prop_readProcSub2 = isOk readProcSub "<(  if true; then true; fi )" prop_readProcSub3 = isOk readProcSub "<( # nothing here \n)" readProcSub = called "process substitution" $ do-    id <- getNextId+    start <- startSpan     dir <- try $ do                     x <- oneOf "<>"                     char '('@@ -1059,6 +1123,7 @@     list <- readCompoundListOrEmpty     allspacing     char ')'+    id <- endSpan start     return $ T_ProcSub id dir list  prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'"@@ -1069,7 +1134,7 @@ prop_readSingleQuoted7 = isOk readSingleQuoted "'foo\x201C\&bar'" prop_readSingleQuoted8 = isWarning readSingleQuoted "'foo\x2018\&bar'" readSingleQuoted = called "single quoted string" $ do-    id <- getNextId+    start <- startSpan     startPos <- getPosition     singleQuote     s <- many readSingleQuotedPart@@ -1087,6 +1152,7 @@             when ('\n' `elem` string && not ("\n" `isPrefixOf` string)) $                 suggestForgotClosingQuote startPos endPos "single quoted string" +    id <- endSpan start     return (T_SingleQuoted id string)  readSingleQuotedLiteral = do@@ -1119,13 +1185,14 @@ readQuotedBackTicked = readBackTicked True readUnquotedBackTicked = readBackTicked False readBackTicked quoted = called "backtick expansion" $ do-    id <- getNextId+    start <- startSpan     startPos <- getPosition     backtick     subStart <- getPosition     subString <- readGenericLiteral "`´"     endPos <- getPosition     backtick+    id <- endSpan start      optional $ do         c <- try . lookAhead $ suspectCharAfterQuotes@@ -1188,15 +1255,15 @@ prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\"" prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\"" prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\""-prop_readDoubleQuoted9 = isWarning readDoubleQuoted "\"foo\\n\"" prop_readDoubleQuoted10 = isOk readDoubleQuoted "\"foo\\\\n\"" readDoubleQuoted = called "double quoted string" $ do-    id <- getNextId+    start <- startSpan     startPos <- getPosition     doubleQuote     x <- many doubleQuotedPart     endPos <- getPosition     doubleQuote <|> fail "Expected end of double quoted string"+    id <- endSpan start     optional $ do         try . lookAhead $ suspectCharAfterQuotes <|> oneOf "$\""         when (any hasLineFeed x && not (startsWithLineFeed x)) $@@ -1218,15 +1285,17 @@   where     readUnicodeQuote = do         pos <- getPosition-        id <- getNextId+        start <- startSpan         c <- oneOf unicodeDoubleQuotes+        id <- endSpan start         parseProblemAt pos WarningC 1111             "This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)."         return $ T_Literal id [c]  readDoubleLiteral = do-    id <- getNextId+    start <- startSpan     s <- many1 readDoubleLiteralPart+    id <- endSpan start     return $ T_Literal id (concat s)  readDoubleLiteralPart = do@@ -1234,8 +1303,9 @@     return $ concat x  readNormalLiteral end = do-    id <- getNextId+    start <- startSpan     s <- many1 (readNormalLiteralPart end)+    id <- endSpan start     return $ T_Literal id (concat s)  prop_readGlob1 = isOk readGlob "*"@@ -1249,18 +1319,20 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral     where         readSimple = do-            id <- getNextId+            start <- startSpan             c <- oneOf "*?"+            id <- endSpan start             return $ T_Glob id [c]         -- Doesn't handle weird things like [^]a] and [$foo]. fixme?         readClass = try $ do-            id <- getNextId+            start <- startSpan             char '['             s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars)             char ']'+            id <- endSpan start             return $ T_Glob id $ "[" ++ concat s ++ "]"           where-           globchars = liftM return . oneOf $ "!$[" ++ extglobStartChars+           globchars = fmap return . oneOf $ "!$[" ++ extglobStartChars            predefined = do               try $ string "[:"               s <- many1 letter@@ -1268,8 +1340,9 @@               return $ "[:" ++ s ++ ":]"          readGlobbyLiteral = do-            id <- getNextId+            start <- startSpan             c <- extglobStart <|> char '['+            id <- endSpan start             return $ T_Literal id [c]  readNormalLiteralPart customEnd =@@ -1298,7 +1371,7 @@             return [next]   where     alternative 'n' = "a quoted, literal line feed"-    alternative t = "\"$(printf \"\\" ++ [t] ++ "\")\""+    alternative t = "\"$(printf '\\" ++ [t] ++ "')\""     escapedChar 'n' = Just "line feed"     escapedChar 't' = Just "tab"     escapedChar 'r' = Just "carriage return"@@ -1318,29 +1391,33 @@ prop_readExtglob7 = isOk readExtglob "*(<>)" prop_readExtglob8 = isOk readExtglob "@(|*())" readExtglob = called "extglob" $ do-    id <- getNextId+    start <- startSpan     c <- try $ do             f <- extglobStart             char '('             return f     contents <- readExtglobPart `sepBy` char '|'     char ')'+    id <- endSpan start     return $ T_Extglob id [c] contents  readExtglobPart = do-    id <- getNextId+    start <- startSpan     x <- many (readExtglobGroup <|> readNormalWordPart "" <|> readSpacePart <|> readExtglobLiteral)+    id <- endSpan start     return $ T_NormalWord id x   where     readExtglobGroup = do-        id <- getNextId         char '('+        start <- startSpan         contents <- readExtglobPart `sepBy` char '|'+        id <- endSpan start         char ')'         return $ T_Extglob id "" contents     readExtglobLiteral = do-        id <- getNextId+        start <- startSpan         str <- many1 (oneOf "<>#;&")+        id <- endSpan start         return $ T_Literal id str  @@ -1360,18 +1437,19 @@     pos <- getPosition     bs <- backslash     (linefeed >> return "")-        <|> liftM return doubleQuotable+        <|> fmap return doubleQuotable         <|> do             c <- anyChar-            parseNoteAt pos StyleC 1117 $-                "Backslash is literal in \"\\" ++ [c] ++ "\". Prefer explicit escaping: \"\\\\" ++ [c] ++ "\"."+            -- This is an invalid escape sequence where the \ is literal.+            -- Previously this caused a SC1117, which may be re-enabled as+            -- as a pedantic warning.             return [bs, c]  readBraceEscaped = do     bs <- backslash     (linefeed >> return "")-        <|> liftM return bracedQuotable-        <|> liftM (\ x -> [bs, x]) anyChar+        <|> fmap return bracedQuotable+        <|> fmap (\ x -> [bs, x]) anyChar   readGenericLiteral endChars = do@@ -1449,46 +1527,51 @@  prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'" readDollarSingleQuote = called "$'..' expression" $ do-    id <- getNextId+    start <- startSpan     try $ string "$'"     str <- readGenericLiteral "'"     char '\''+    id <- endSpan start     return $ T_DollarSingleQuoted id str  prop_readDollarDoubleQuote = isOk readDollarDoubleQuote "$\"hello\"" readDollarDoubleQuote = do     lookAhead . try $ string "$\""-    id <- getNextId+    start <- startSpan     char '$'     doubleQuote     x <- many doubleQuotedPart     doubleQuote <|> fail "Expected end of translated double quoted string"+    id <- endSpan start     return $ T_DollarDoubleQuoted id x  prop_readDollarArithmetic = isOk readDollarArithmetic "$(( 3 * 4 +5))" prop_readDollarArithmetic2 = isOk readDollarArithmetic "$(((3*4)+(1*2+(3-1))))" readDollarArithmetic = called "$((..)) expression" $ do-    id <- getNextId+    start <- startSpan     try (string "$((")     c <- readArithmeticContents     pos <- getPosition     char ')'     char ')' <|> fail "Expected a double )) to end the $((..))"+    id <- endSpan start     return (T_DollarArithmetic id c)  readDollarBracket = called "$[..] expression" $ do-    id <- getNextId+    start <- startSpan     try (string "$[")     c <- readArithmeticContents     string "]"+    id <- endSpan start     return (T_DollarBracket id c)  prop_readArithmeticExpression = isOk readArithmeticExpression "((a?b:c))" readArithmeticExpression = called "((..)) command" $ do-    id <- getNextId+    start <- startSpan     try (string "((")     c <- readArithmeticContents     string "))"+    id <- endSpan start     return (T_Arithmetic id c)  -- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used@@ -1508,13 +1591,14 @@ prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }" prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}" readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do-    id <- getNextId+    start <- startSpan     try $ do         string "${"         whitespace     allspacing     term <- readTerm     char '}' <|> fail "Expected } to end the ksh ${ ..; } command expansion"+    id <- endSpan start     return $ T_DollarBraceCommandExpansion id term  prop_readDollarBraced1 = isOk readDollarBraced "${foo//bar/baz}"@@ -1522,20 +1606,22 @@ prop_readDollarBraced3 = isOk readDollarBraced "${foo%%$(echo cow\\})}" prop_readDollarBraced4 = isOk readDollarBraced "${foo#\\}}" readDollarBraced = called "parameter expansion" $ do-    id <- getNextId+    start <- startSpan     try (string "${")     word <- readDollarBracedWord     char '}'+    id <- endSpan start     return $ T_DollarBraced id word  prop_readDollarExpansion1= isOk readDollarExpansion "$(echo foo; ls\n)" prop_readDollarExpansion2= isOk readDollarExpansion "$(  )" prop_readDollarExpansion3= isOk readDollarExpansion "$( command \n#comment \n)" readDollarExpansion = called "command expansion" $ do-    id <- getNextId+    start <- startSpan     try (string "$(")     cmds <- readCompoundListOrEmpty     char ')' <|> fail "Expected end of $(..) expression"+    id <- endSpan start     return $ T_DollarExpansion id cmds  prop_readDollarVariable = isOk readDollarVariable "$@"@@ -1544,14 +1630,15 @@ prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]" prop_readDollarVariable5 = isWarning (readDollarVariable >> string "[f") "$arr[f" +readDollarVariable :: Monad m => SCParser m Token readDollarVariable = do-    id <- getNextId+    start <- startSpan     pos <- getPosition      let singleCharred p = do-        n <- p-        value <- wrap [n]-        return (T_DollarBraced id value)+        value <- wrapString ((:[]) <$> p)+        id <- endSpan start+        return $ (T_DollarBraced id value)      let positional = do         value <- singleCharred digit@@ -1562,8 +1649,8 @@     let special = singleCharred specialVariable      let regular = do-        name <- readVariableName-        value <- wrap name+        value <- wrapString readVariableName+        id <- endSpan start         return (T_DollarBraced id value) `attempting` do             lookAhead $ char '['             parseNoteAt pos ErrorC 1087 "Use braces when expanding arrays, e.g. ${array[idx]} (or ${var}[.. to quiet)."@@ -1571,10 +1658,13 @@     try $ char '$' >> (positional <|> special <|> regular)    where-    wrap s = do-        x <- getNextId-        y <- getNextId-        return $ T_NormalWord x [T_Literal y s]+    wrapString p = do+        start <- getPosition+        s <- p+        end <- getPosition+        id1 <- getNextIdBetween start end+        id2 <- getNextIdBetween start end+        return $ T_NormalWord id1 [T_Literal id2 s]  readVariableName = do     f <- variableStart@@ -1582,9 +1672,9 @@     return (f:rest)  readDollarLonely = do-    id <- getNextId-    pos <- getPosition+    start <- startSpan     char '$'+    id <- endSpan start     n <- lookAhead (anyChar <|> (eof >> return '_'))     return $ T_Literal id "$" @@ -1605,12 +1695,9 @@ prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo" prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n" prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n  foo\n foo\n"-prop_readHereDoc18= isWarning readScript "cat << foo\nLoose \\t\nfoo"-prop_readHereDoc19= isOk readScript "# shellcheck disable=SC1117\ncat << foo\nLoose \\t\nfoo" prop_readHereDoc20= isWarning readScript "cat << foo\n  foo\n()\nfoo\n" prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n  foo\n()\nfoo\n" readHereDoc = called "here document" $ do-    fid <- getNextId     pos <- getPosition     try $ string "<<"     dashed <- (char '-' >> return Dashed) <|> return Undashed@@ -1619,8 +1706,9 @@         try . lookAhead $ char '('         let message = "Shells are space sensitive. Use '< <(cmd)', not '<<" ++ sp ++ "(cmd)'."         parseProblemAt pos ErrorC 1038 message-    hid <- getNextId+    start <- startSpan     (quoted, endToken) <- readToken+    hid <- endSpan start      -- add empty tokens for now, read the rest in readPendingHereDocs     let doc = T_HereDoc hid dashed quoted endToken []@@ -1643,15 +1731,15 @@     readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =       swapContext ctx $       do-        docPos <- getPosition-        tokenPos <- Map.findWithDefault (error "Missing ID") id <$> getMap+        docStartPos <- getPosition         (terminated, wasWarned, lines) <- readDocLines dashed endToken+        docEndPos <- getPosition         let hereData = unlines lines         unless terminated $ do             unless wasWarned $-                debugHereDoc tokenPos endToken hereData+                debugHereDoc id endToken hereData             fail "Here document was not correctly terminated"-        list <- parseHereData quoted docPos hereData+        list <- parseHereData quoted (docStartPos, docEndPos) hereData         addToHereDocMap id list      -- Read the lines making up the here doc. Returns (IsTerminated, Lines)@@ -1731,40 +1819,42 @@         void (char '\n') <|> eof         return c -    parseHereData Quoted startPos hereData = do-        id <- getNextIdAt startPos+    parseHereData Quoted (start,end) hereData = do+        id <- getNextIdBetween start end         return [T_Literal id hereData] -    parseHereData Unquoted startPos hereData =+    parseHereData Unquoted (startPos, _) hereData =         subParse startPos readHereData hereData      readHereData = many $ doubleQuotedPart <|> readHereLiteral      readHereLiteral = do-        id <- getNextId+        start <- startSpan         chars <- many1 $ noneOf "`$\\"+        id <- endSpan start         return $ T_Literal id chars -    debugHereDoc pos endToken doc+    debugHereDoc tokenId endToken doc         | endToken `isInfixOf` doc =             let lookAt line = when (endToken `isInfixOf` line) $-                      parseProblemAt pos ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")+                      parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")             in do-                  parseProblemAt pos ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.")+                  parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.")                   mapM_ lookAt (lines doc)         | map toLower endToken `isInfixOf` map toLower doc =-            parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")+            parseProblemAtId tokenId 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.")+            parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.")   readFilename = readNormalWord readIoFileOp = choice [g_DGREAT, g_LESSGREAT, g_GREATAND, g_LESSAND, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ]  readIoDuplicate = try $ do-    id <- getNextId+    start <- startSpan     op <- g_GREATAND <|> g_LESSAND     target <- readIoVariable <|> digitsAndOrDash+    id <- endSpan start     return $ T_IoDuplicate id op target   where     -- either digits with optional dash, or a required dash@@ -1776,10 +1866,11 @@  prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\"" readIoFile = called "redirection" $ do-    id <- getNextId+    start <- startSpan     op <- readIoFileOp     spacing     file <- readFilename+    id <- endSpan start     return $ T_IoFile id op file  readIoVariable = try $ do@@ -1801,9 +1892,10 @@ prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-" prop_readIoRedirect7 = isOk readIoRedirect "{foo}>&1-" readIoRedirect = do-    id <- getNextId+    start <- startSpan     n <- readIoSource     redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile+    id <- endSpan start     skipAnnotationAndWarn     spacing     return $ T_FdRedirect id n redir@@ -1812,31 +1904,46 @@  prop_readHereString = isOk readHereString "<<< \"Hello $world\"" readHereString = called "here string" $ do-    id <- getNextId+    start <- startSpan     try $ string "<<<"+    id <- endSpan start     spacing-    id2 <- getNextId     word <- readNormalWord-    return $ T_HereString id2 word+    return $ T_HereString id word -readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing)+readNewlineList =+    many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak+  where+    checkBadBreak = optional $ do+                pos <- getPosition+                try $ lookAhead (oneOf "|&") --  See if the next thing could be |, || or &&+                parseProblemAt pos ErrorC 1133+                    "Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one." readLineBreak = optional readNewlineList  prop_readSeparator1 = isWarning readScript "a &; b" prop_readSeparator2 = isOk readScript "a & b" prop_readSeparator3 = isWarning readScript "a &amp; b" prop_readSeparator4 = isWarning readScript "a &gt; file; b"+prop_readSeparator5 = isWarning readScript "curl https://example.com/?foo=moo&bar=cow" readSeparatorOp = do     notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator)     notFollowedBy2 (string "&>")+    start <- getPosition     f <- try (do                     pos <- getPosition                     char '&'-                    optional $ do-                        s <- lookAhead . choice . map (try . string) $-                            ["amp;", "gt;", "lt;"]-                        parseProblemAt pos ErrorC 1109 "This is an unquoted HTML entity. Replace with corresponding character."+                    optional $ choice [+                        do+                            s <- lookAhead . choice . map (try . string) $+                                ["amp;", "gt;", "lt;"]+                            parseProblemAt pos ErrorC 1109 "This is an unquoted HTML entity. Replace with corresponding character.", +                        do+                            try . lookAhead $ variableStart+                            parseProblemAt pos WarningC 1132 "This & terminates the command. Escape it or add space after & to silence."+                      ]+                     spacing                     pos <- getPosition                     char ';'@@ -1845,8 +1952,9 @@                     parseProblemAt pos ErrorC 1045 "It's not 'foo &; bar', just 'foo & bar'."                     return '&'             ) <|> char ';' <|> char '&'+    end <- getPosition     spacing-    return f+    return (f, (start, end))  readSequentialSep = void (g_Semi >> readLineBreak) <|> void readNewlineList readSeparator =@@ -1856,25 +1964,10 @@         return separator      <|>         do+            start <- getPosition             readNewlineList-            return '\n'--makeSimpleCommand id1 id2 prefix cmd suffix =-    let-        (preAssigned, preRest) = partition assignment prefix-        (preRedirected, preRest2) = partition redirection preRest-        (postRedirected, postRest) = partition redirection suffix--        redirs = preRedirected ++ postRedirected-        assigns = preAssigned-        args = cmd ++ preRest2 ++ postRest-    in-        T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args-  where-    assignment (T_Assignment {}) = True-    assignment _ = False-    redirection (T_FdRedirect {}) = True-    redirection _ = False+            end <- getPosition+            return ('\n', (start, end))  prop_readSimpleCommand = isOk readSimpleCommand "echo test > file" prop_readSimpleCommand2 = isOk readSimpleCommand "cmd &> file"@@ -1891,18 +1984,19 @@ prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo" prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]" readSimpleCommand = called "simple command" $ do-    id1 <- getNextId-    id2 <- getNextId     prefix <- option [] readCmdPrefix     skipAnnotationAndWarn-    pos <- getPosition     cmd <- option Nothing $ Just <$> readCmdName     when (null prefix && isNothing cmd) $ fail "Expected a command"      case cmd of-      Nothing -> return $ makeSimpleCommand id1 id2 prefix [] []+      Nothing -> do+        id1 <- getNextIdSpanningTokenList prefix+        id2 <- getNewIdFor id1+        return $ makeSimpleCommand id1 id2 prefix [] []+       Just cmd -> do-            validateCommand pos cmd+            validateCommand cmd             suffix <- option [] $ getParser readCmdSuffix cmd [                         (["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),                         (["time"], readTimeSuffix),@@ -1910,9 +2004,12 @@                         (["eval"], readEvalSuffix)                     ] +            id1 <- getNextIdSpanningTokenList (prefix ++ (cmd:suffix))+            id2 <- getNewIdFor id1+             let result = makeSimpleCommand id1 id2 prefix [cmd] suffix             if isCommand ["source", "."] cmd-                then readSource pos result+                then readSource result                 else return result   where     isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings@@ -1927,21 +2024,39 @@         case cmd of             _ -> False -    validateCommand pos cmd =+    validateCommand cmd =         case cmd of-            (T_NormalWord _ [T_Literal _ "//"]) -> commentWarning pos-            (T_NormalWord _ (T_Literal _ "/" : T_Glob _ "*" :_)) -> commentWarning pos+            (T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)+            (T_NormalWord _ (T_Literal _ "/" : T_Glob _ "*" :_)) -> commentWarning (getId cmd)             (T_NormalWord _ (T_Literal _ str:_)) -> do-                let cmd = map toLower $ takeWhile isAlpha str-                when (cmd `elem` ["elsif", "elseif"]) $-                    parseProblemAt pos ErrorC 1131 "Use 'elif' to start another branch."+                let cmdString = map toLower $ takeWhile isAlpha str+                when (cmdString `elem` ["elsif", "elseif"]) $+                    parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."             _ -> return () -    commentWarning pos =-        parseProblemAt pos ErrorC 1127 "Was this intended as a comment? Use # in sh."+    commentWarning id =+        parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh." -readSource :: Monad m => SourcePos -> Token -> SCParser m Token-readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do+    makeSimpleCommand id1 id2 prefix cmd suffix =+        let+            (preAssigned, preRest) = partition assignment prefix+            (preRedirected, preRest2) = partition redirection preRest+            (postRedirected, postRest) = partition redirection suffix++            redirs = preRedirected ++ postRedirected+            assigns = preAssigned+            args = cmd ++ preRest2 ++ postRest+        in+            T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args+      where+        assignment (T_Assignment {}) = True+        assignment _ = False+        redirection (T_FdRedirect {}) = True+        redirection _ = False+++readSource :: Monad m => Token -> SCParser m Token+readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file:_))) = do     override <- getSourceOverride     let literalFile = do         name <- override `mplus` getLiteralString file@@ -1950,14 +2065,14 @@         return name     case literalFile of         Nothing -> do-            parseNoteAt pos WarningC 1090+            parseNoteAtId (getId file) WarningC 1090                 "Can't follow non-constant source. Use a directive to specify location."             return t         Just filename -> do             proceed <- shouldFollow filename             if not proceed               then do-                parseNoteAt pos InfoC 1093+                parseNoteAtId (getId file) InfoC 1093                     "This file appears to be recursively sourced. Ignoring."                 return t               else do@@ -1968,19 +2083,19 @@                     else system $ siReadFile sys filename                 case input of                     Left err -> do-                        parseNoteAt pos InfoC 1091 $+                        parseNoteAtId (getId file) InfoC 1091 $                             "Not following: " ++ err                         return t                     Right script -> do-                        id1 <- getNextIdAt pos-                        id2 <- getNextIdAt pos+                        id1 <- getNewIdFor cmdId+                        id2 <- getNewIdFor cmdId                          let included = do                             src <- subRead filename script                             return $ T_SourceCommand id1 t (T_Include id2 src)                          let failed = do-                            parseNoteAt pos WarningC 1094+                            parseNoteAtId (getId file) WarningC 1094                                 "Parsing of sourced file failed. Ignoring it."                             return t @@ -1990,7 +2105,7 @@         withContext (ContextSource name) $             inSeparateContext $                 subParse (initialPos name) readScript script-readSource _ t = return t+readSource t = return t   prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"@@ -2009,9 +2124,10 @@ prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo" prop_readAndOr2 = isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo" readAndOr = do-    aid <- getNextId+    start <- startSpan     apos <- getPosition     annotations <- readAnnotations+    aid <- endSpan start      unless (null annotations) $ optional $ do         try . lookAhead $ readKeyword@@ -2039,27 +2155,28 @@     allspacing     m <- readAndOr     readTerm' m--readTerm' current =-    do-        id <- getNextId-        sep <- readSeparator-        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 [current]--transformWithSeparator i '&' = T_Backgrounded i-transformWithSeparator i _  = id+  where+    readTerm' current =+        do+            (sep, (start, end)) <- readSeparator+            id <- getNextIdBetween start end+            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 [current]+      where+        transformWithSeparator i '&' = T_Backgrounded i+        transformWithSeparator i _  = id   readPipeSequence = do-    id <- getNextId+    start <- startSpan     (cmds, pipes) <- sepBy1WithSeparators readCommand                         (readPipe `thenSkip` (spacing >> readLineBreak))+    id <- endSpan start     spacing     return $ T_Pipeline id pipes cmds   where@@ -2072,9 +2189,10 @@  readPipe = do     notFollowedBy2 g_OR_IF-    id <- getNextId+    start <- startSpan     char '|'     qualifier <- string "&" <|> return ""+    id <- endSpan start     spacing     return $ T_Pipe id ('|':qualifier) @@ -2109,7 +2227,7 @@ prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi; fi" prop_readIfClause5 = isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi" readIfClause = called "if expression" $ do-    id <- getNextId+    start <- startSpan     pos <- getPosition     (condition, action) <- readIfPart     elifs <- many readElifPart@@ -2119,6 +2237,7 @@         parseProblemAt pos ErrorC 1046 "Couldn't find 'fi' for this 'if'."         parseProblem ErrorC 1047 "Expected 'fi' matching previously mentioned 'if'."         return "Expected 'fi'"+    id <- endSpan start      return $ T_IfExpression id ((condition, action):elifs) elses @@ -2183,19 +2302,20 @@  prop_readSubshell = isOk readSubshell "( cd /foo; tar cf stuff.tar * )" readSubshell = called "explicit subshell" $ do-    id <- getNextId+    start <- startSpan     char '('     allspacing     list <- readCompoundList     allspacing-    char ')' <|> fail ") closing the subshell"+    char ')' <|> fail "Expected ) closing the subshell"+    id <- endSpan start     return $ T_Subshell id list  prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }" prop_readBraceGroup2 = isWarning readBraceGroup "{foo;}" prop_readBraceGroup3 = isOk readBraceGroup "{(foo)}" readBraceGroup = called "brace group" $ do-    id <- getNextId+    start <- startSpan     char '{'     void allspacingOrFail <|> optional (do         lookAhead $ noneOf "(" -- {( is legal@@ -2208,31 +2328,33 @@     char '}' <|> do         parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."         fail "Missing '}'"+    id <- endSpan start     return $ T_BraceGroup id list  prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done" readWhileClause = called "while loop" $ do-    pos <- getPosition-    (T_While id) <- g_While+    start <- startSpan+    kwId <- getId <$> g_While     condition <- readTerm-    statements <- readDoGroup pos+    statements <- readDoGroup kwId+    id <- endSpan start     return $ T_WhileExpression id condition statements  prop_readUntilClause = isOk readUntilClause "until kill -0 $PID; do sleep 1; done" readUntilClause = called "until loop" $ do-    pos <- getPosition-    (T_Until id) <- g_Until+    start <- startSpan+    kwId <- getId <$> g_Until     condition <- readTerm-    statements <- readDoGroup pos+    statements <- readDoGroup kwId+    id <- endSpan start     return $ T_UntilExpression id condition statements -readDoGroup loopPos = do-    pos <- getPosition+readDoGroup kwId = do     optional (do                 try . lookAhead $ g_Done-                parseProblemAt loopPos ErrorC 1057 "Did you forget the 'do' for this loop?")+                parseProblemAtId kwId ErrorC 1057 "Did you forget the 'do' for this loop?") -    g_Do `orFail` do+    doKw <- g_Do `orFail` do         parseProblem ErrorC 1058 "Expected 'do'."         return "Expected 'do'" @@ -2241,11 +2363,11 @@      optional (do                 try . lookAhead $ g_Done-                parseProblemAt loopPos ErrorC 1060 "Can't have empty do clauses (use 'true' as a no-op).")+                parseProblemAtId (getId doKw) ErrorC 1060 "Can't have empty do clauses (use 'true' as a no-op).")      commands <- readCompoundList     g_Done `orFail` do-            parseProblemAt pos ErrorC 1061 "Couldn't find 'done' for this 'do'."+            parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."             parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."             return "Expected 'done'"     return commands@@ -2266,9 +2388,9 @@     pos <- getPosition     (T_For id) <- g_For     spacing-    readArithmetic id pos <|> readRegular id pos+    readArithmetic id <|> readRegular id   where-    readArithmetic id pos = called "arithmetic for condition" $ do+    readArithmetic id = called "arithmetic for condition" $ do         try $ string "(("         x <- readArithmeticContents         char ';' >> spacing@@ -2279,29 +2401,28 @@         string "))"         spacing         optional $ readSequentialSep >> spacing-        group <- readBraced <|> readDoGroup pos+        group <- readBraced <|> readDoGroup id         return $ T_ForArithmetic id x y z group      readBraced = do         (T_BraceGroup _ list) <- readBraceGroup         return list -    readRegular id pos = do+    readRegular id = do         acceptButWarn (char '$') ErrorC 1086             "Don't use $ on the iterator name in for loops."         name <- readVariableName `thenSkip` allspacing         values <- readInClause <|> (optional readSequentialSep >> return [])-        group <- readDoGroup pos+        group <- readDoGroup id         return $ T_ForIn id name values group  prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done" prop_readSelectClause2 = isOk readSelectClause "select foo; do echo $foo; done" readSelectClause = called "select loop" $ do-    pos <- getPosition     (T_Select id) <- g_Select     spacing     typ <- readRegular-    group <- readDoGroup pos+    group <- readDoGroup id     typ id group   where     readRegular = do@@ -2331,7 +2452,7 @@ 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+    start <- startSpan     g_Case     word <- readNormalWord     allspacing@@ -2339,6 +2460,7 @@     readLineBreak     list <- readCaseList     g_Esac <|> fail "Expected 'esac' to close the case statement"+    id <- endSpan start     return $ T_CaseExpression id word list  readCaseList = many readCaseItem@@ -2383,18 +2505,20 @@ prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }" prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}" prop_readFunctionDefinition12= isOk readFunctionDefinition "function []!() { true; }"+prop_readFunctionDefinition13= isOk readFunctionDefinition "@require(){ true; }" readFunctionDefinition = called "function" $ do+    start <- startSpan     functionSignature <- try readFunctionSignature     allspacing     void (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition."     group <- readBraceGroup <|> readSubshell-    return $ functionSignature group+    id <- endSpan start+    return $ functionSignature id group   where     readFunctionSignature =         readWithFunction <|> readWithoutFunction       where         readWithFunction = do-            id <- getNextId             try $ do                 string "function"                 whitespace@@ -2405,15 +2529,14 @@             when (not hasParens && null spaces) $                 acceptButWarn (lookAhead (oneOf "{("))                     ErrorC 1095 "You need a space or linefeed between the function name and body."-            return $ T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name+            return $ \id -> T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name          readWithoutFunction = try $ do-            id <- getNextId             name <- many1 functionChars             guard $ name /= "time"  -- Interfers with time ( foo )             spacing             readParens-            return $ T_Function id (FunctionKeyword False) (FunctionParentheses True) name+            return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name          readParens = do             g_Lparen@@ -2428,23 +2551,26 @@ prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" prop_readCoProc3 = isOk readCoProc "coproc echo bar" readCoProc = called "coproc" $ do-    id <- getNextId+    start <- startSpan     try $ do         string "coproc"         whitespace-    choice [ try $ readCompoundCoProc id, readSimpleCoProc id ]+    choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]   where-    readCompoundCoProc id = do+    readCompoundCoProc start = do         var <- optionMaybe $             readVariableName `thenSkip` whitespace         body <- readBody readCompoundCommand+        id <- endSpan start         return $ T_CoProc id var body-    readSimpleCoProc id = do+    readSimpleCoProc start = do         body <- readBody readSimpleCommand+        id <- endSpan start         return $ T_CoProc id Nothing body     readBody parser = do-        id <- getNextId+        start <- startSpan         body <- parser+        id <- endSpan start         return $ T_CoProcBody id body  @@ -2452,7 +2578,6 @@  prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null" readCompoundCommand = do-    id <- getNextId     cmd <- choice [         readBraceGroup,         readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->@@ -2469,6 +2594,7 @@         ]     spacing     redirs <- many readIoRedirect+    id <- getNextIdSpanningTokenList (cmd:redirs)     unless (null redirs) $ optional $ do         lookAhead $ try (spacing >> needsSeparator)         parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands."@@ -2535,6 +2661,13 @@   where     readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos)) +-- Like readStringForParser, returning the span as a T_Literal+readLiteralForParser parser = do+    start <- startSpan+    str <- readStringForParser parser+    id <- endSpan start+    return $ T_Literal id str+ prop_readAssignmentWord = isOk readAssignmentWord "a=42" prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)" prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"@@ -2556,7 +2689,7 @@ readAssignmentWord = readAssignmentWordExt True readWellFormedAssignment = readAssignmentWordExt False readAssignmentWordExt lenient = try $ do-    id <- getNextId+    start <- startSpan     pos <- getPosition     when lenient $         optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")@@ -2565,11 +2698,12 @@         optional (readNormalDollar >> parseNoteAt pos ErrorC                                 1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")     indices <- many readArrayIndex-    hasLeftSpace <- liftM (not . null) spacing+    hasLeftSpace <- fmap (not . null) spacing     pos <- getPosition+    id <- endSpan start     op <- readAssignmentOp-    hasRightSpace <- liftM (not . null) spacing-    isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))+    hasRightSpace <- fmap (not . null) spacing+    isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))     if not hasLeftSpace && (hasRightSpace || isEndOfCommand)       then do         when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $@@ -2602,20 +2736,22 @@             string "=" >> return Assign             ]     readEmptyLiteral = do-        id <- getNextId+        start <- startSpan+        id <- endSpan start         return $ T_Literal id ""  readArrayIndex = do-    id <- getNextId+    start <- startSpan     char '['     pos <- getPosition     str <- readStringForParser readIndexSpan     char ']'+    id <- endSpan start     return $ T_UnparsedIndex id pos str  readArray :: Monad m => SCParser m Token readArray = called "array assignment" $ do-    id <- getNextId+    start <- startSpan     opening <- getPosition     char '('     optional $ do@@ -2624,39 +2760,45 @@     allspacing     words <- readElement `reluctantlyTill` char ')'     char ')' <|> fail "Expected ) to close array assignment"+    id <- endSpan start     return $ T_Array id words   where     readElement = (readIndexed <|> readRegular) `thenSkip` allspacing     readIndexed = do-        id <- getNextId+        start <- startSpan         index <- try $ do             x <- many1 readArrayIndex             char '='             return x         value <- readRegular <|> nothing+        id <- endSpan start         return $ T_IndexedElement id index value     readRegular = readArray <|> readNormalWord      nothing = do-        id <- getNextId+        start <- startSpan+        id <- endSpan start         return $ T_Literal id ""  tryToken s t = try $ do-    id <- getNextId+    start <- startSpan     string s+    id <- endSpan start     spacing     return $ t id  redirToken c t = try $ do-    id <- getNextId+    start <- startSpan     char c+    id <- endSpan start     notFollowedBy2 $ char '('     return $ t id  tryWordToken s t = tryParseWordToken s t `thenSkip` spacing tryParseWordToken keyword t = try $ do-    id <- getNextId+    start <- startSpan     str <- anycaseString keyword+    id <- endSpan start      optional $ do         c <- try . lookAhead $ anyChar@@ -2707,15 +2849,17 @@ g_In = tryWordToken "in" T_In <* skipAnnotationAndWarn g_Lbrace = tryWordToken "{" T_Lbrace g_Rbrace = do -- handled specially due to ksh echo "${ foo; }bar"-    id <- getNextId+    start <- startSpan     char '}'+    id <- endSpan start     return $ T_Rbrace id  g_Lparen = tryToken "(" T_Lparen g_Rparen = tryToken ")" T_Rparen g_Bang = do-    id <- getNextId+    start <- startSpan     char '!'+    id <- endSpan start     void spacing1 <|> do         pos <- getPosition         parseProblemAt pos ErrorC 1035@@ -2758,9 +2902,10 @@     readCorrect = void $ string "#!"      readSwapped = do-        pos <- getPosition+        start <- startSpan         string "!#"-        parseProblemAt pos ErrorC 1084+        id <- endSpan start+        parseProblemAtId id ErrorC 1084             "Use #!, not !#, for the shebang."      skipSpaces = fmap (not . null) $ many linewhitespace@@ -2828,25 +2973,35 @@ prop_readScript4 = isWarning readScriptFile "#!/usr/bin/perl\nfoo=(" prop_readScript5 = isOk readScriptFile "#!/bin/bash\n#This is an empty script\n\n" readScriptFile = do-    id <- getNextId+    start <- startSpan     pos <- getPosition     optional $ do         readUtf8Bom         parseProblem ErrorC 1082             "This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."     sb <- option "" readShebang-    verifyShell pos (getShell sb)-    if isValidShell (getShell sb) /= Just False+    allspacing+    annotationStart <- startSpan+    annotations <- readAnnotations+    annotationId <- endSpan annotationStart+    let shellAnnotationSpecified =+            any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations+    shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride+    let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified++    unless ignoreShebang $+        verifyShebang pos (getShell sb)+    if ignoreShebang || isValidShell (getShell sb) /= Just False       then do-            allspacing-            annotationId <- getNextId-            annotations <- readAnnotations             commands <- withAnnotations annotations readCompoundListOrEmpty+            id <- endSpan start             verifyEof-            let script = T_Annotation annotationId annotations $  T_Script id sb commands+            let script = T_Annotation annotationId annotations $+                            T_Script id sb commands             reparseIndices script         else do             many anyChar+            id <- endSpan start             return $ T_Script id sb []    where@@ -2860,7 +3015,7 @@                     then second                     else basename first -    verifyShell pos s =+    verifyShebang pos s = do         case isValidShell s of             Just True -> return ()             Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!"@@ -2910,24 +3065,28 @@     result {         -- Remove the noisiest parts         prTokenPositions = Map.fromList [-            (Id 0, Position {+            (Id 0, (newPosition {                 posFile = "removed for clarity",                 posLine = -1,                 posColumn = -1-            })]+            }, newPosition {+                posFile = "removed for clarity",+                posLine = -1,+                posColumn = -1+            }))]     }   where     result = runIdentity $-        parseScript (mockedSystemInterface []) $ ParseSpec {+        parseScript (mockedSystemInterface []) $ newParseSpec {             psFilename = "debug",-            psScript = string,-            psCheckSourced = False+            psScript = string         }  testEnvironment =     Environment {         systemInterface = (mockedSystemInterface []),-        checkSourced = False+        checkSourced = False,+        shellTypeOverride = Nothing     }  @@ -2999,14 +3158,14 @@     (result, state) <- runParser env (parseWithNotes readScript) name contents     case result of         Right (script, userstate) ->-            return ParseResult {+            return newParseResult {                 prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,-                prTokenPositions = Map.map posToPos (positionMap userstate),+                prTokenPositions = Map.map startEndPosToPos (positionMap userstate),                 prRoot = Just $                     reattachHereDocs script (hereDocMap userstate)             }         Left err ->-            return ParseResult {+            return newParseResult {                 prComments =                     map toPositionedComment $                         notesForContext (contextStack state)@@ -3073,15 +3232,26 @@  toPositionedComment :: ParseNote -> PositionedComment toPositionedComment (ParseNote start end severity code message) =-    PositionedComment (posToPos start) (posToPos end) $ Comment severity code message+    newPositionedComment {+        pcStartPos = (posToPos start)+      , pcEndPos = (posToPos end)+      , pcComment = newComment {+          cSeverity = severity+        , cCode = code+        , cMessage = message+      }+    }  posToPos :: SourcePos -> Position-posToPos sp = Position {+posToPos sp = newPosition {     posFile = sourceName sp,     posLine = fromIntegral $ sourceLine sp,     posColumn = fromIntegral $ sourceColumn sp } +startEndPosToPos :: (SourcePos, SourcePos) -> (Position, Position)+startEndPosToPos (s, e) = (posToPos s, posToPos e)+ -- TODO: Clean up crusty old code that this is layered on top of parseScript :: Monad m =>         SystemInterface m -> ParseSpec -> m ParseResult@@ -3090,7 +3260,8 @@   where     env = Environment {         systemInterface = sys,-        checkSourced = psCheckSourced spec+        checkSourced = psCheckSourced spec,+        shellTypeOverride = psShellTypeOverride spec     }  -- Same as 'try' but emit syntax errors if the parse fails.
+ striptests view
@@ -0,0 +1,78 @@+#!/usr/bin/env bash+# This file strips all unit tests from ShellCheck, removing+# the dependency on QuickCheck and Template Haskell and+# reduces the binary size considerably.+set -o pipefail++sponge() {+  local data+  data="$(cat)"+  printf '%s\n' "$data" > "$1"+}++modify() {+  if ! "${@:2}" < "$1" | sponge "$1"+  then+    {+      printf 'Failed to modify %s: ' "$1"+      printf '%q ' "${@:2}"+      printf '\n'+    } >&2+    exit 1+  fi+}++detestify() {+  printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'+  awk '+    BEGIN {+      state = 0;+    }++    /LANGUAGE TemplateHaskell/ { next; }+    /^import.*Test\./ { next; }++    /^module/ {+      sub(/,[^,)]*runTests/, "");+    }++    # Delete tests+    /^prop_/ { state = 1; next; }++    # ..and any blank lines following them.+    state == 1 && /^ / { next; }++    # Template Haskell marker+    /^return / {+      exit;+    }++    { state = 0; print; }+    '+}++++if [[ ! -e 'ShellCheck.cabal' ]]+then+  echo "Run me from the ShellCheck directory." >&2+  exit 1+fi++if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1+then+  echo "You have local changes! These may be overwritten." >&2+  exit 2+fi++modify 'ShellCheck.cabal' sed -e '+  /QuickCheck/d+  /^test-suite/{ s/.*//; q; }+  '++find . -name '.git' -prune -o -type f -name '*.hs' -print |+  while IFS= read -r file+  do+    modify "$file" detestify+  done+