packages feed

ShellCheck 0.7.0 → 0.7.1

raw patch · 20 files changed

+901/−1238 lines, 20 filesdep ~basesetup-changedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base

API changes (from Hackage documentation)

- ShellCheck.AST: TA_Assignment :: Id -> String -> Token -> Token -> Token
- ShellCheck.AST: TA_Binary :: Id -> String -> Token -> Token -> Token
- ShellCheck.AST: TA_Expansion :: Id -> [Token] -> Token
- ShellCheck.AST: TA_Sequence :: Id -> [Token] -> Token
- ShellCheck.AST: TA_Trinary :: Id -> Token -> Token -> Token -> Token
- ShellCheck.AST: TA_Unary :: Id -> String -> Token -> Token
- ShellCheck.AST: TA_Variable :: Id -> String -> [Token] -> Token
- ShellCheck.AST: TC_And :: Id -> ConditionType -> String -> Token -> Token -> Token
- ShellCheck.AST: TC_Binary :: Id -> ConditionType -> String -> Token -> Token -> Token
- ShellCheck.AST: TC_Empty :: Id -> ConditionType -> Token
- ShellCheck.AST: TC_Group :: Id -> ConditionType -> Token -> Token
- ShellCheck.AST: TC_Nullary :: Id -> ConditionType -> Token -> Token
- ShellCheck.AST: TC_Or :: Id -> ConditionType -> String -> Token -> Token -> Token
- ShellCheck.AST: TC_Unary :: Id -> ConditionType -> String -> Token -> Token
- ShellCheck.AST: T_AND_IF :: Id -> Token
- ShellCheck.AST: T_AndIf :: Id -> Token -> Token -> Token
- ShellCheck.AST: T_Annotation :: Id -> [Annotation] -> Token -> Token
- ShellCheck.AST: T_Arithmetic :: Id -> Token -> Token
- ShellCheck.AST: T_Array :: Id -> [Token] -> Token
- ShellCheck.AST: T_Assignment :: Id -> AssignmentMode -> String -> [Token] -> Token -> Token
- ShellCheck.AST: T_Backgrounded :: Id -> Token -> Token
- ShellCheck.AST: T_Backticked :: Id -> [Token] -> Token
- ShellCheck.AST: T_Bang :: Id -> Token
- ShellCheck.AST: T_Banged :: Id -> Token -> Token
- ShellCheck.AST: T_BatsTest :: Id -> Token -> Token -> Token
- ShellCheck.AST: T_BraceExpansion :: Id -> [Token] -> Token
- ShellCheck.AST: T_BraceGroup :: Id -> [Token] -> Token
- ShellCheck.AST: T_CLOBBER :: Id -> Token
- ShellCheck.AST: T_Case :: Id -> Token
- ShellCheck.AST: T_CaseExpression :: Id -> Token -> [(CaseType, [Token], [Token])] -> Token
- ShellCheck.AST: T_CoProc :: Id -> Maybe String -> Token -> Token
- ShellCheck.AST: T_CoProcBody :: Id -> Token -> Token
- ShellCheck.AST: T_Condition :: Id -> ConditionType -> Token -> Token
- ShellCheck.AST: T_DGREAT :: Id -> Token
- ShellCheck.AST: T_DLESS :: Id -> Token
- ShellCheck.AST: T_DLESSDASH :: Id -> Token
- ShellCheck.AST: T_DSEMI :: Id -> Token
- ShellCheck.AST: T_Do :: Id -> Token
- ShellCheck.AST: T_DollarArithmetic :: Id -> Token -> Token
- ShellCheck.AST: T_DollarBraceCommandExpansion :: Id -> [Token] -> Token
- ShellCheck.AST: T_DollarBraced :: Id -> Bool -> Token -> Token
- ShellCheck.AST: T_DollarBracket :: Id -> Token -> Token
- ShellCheck.AST: T_DollarDoubleQuoted :: Id -> [Token] -> Token
- ShellCheck.AST: T_DollarExpansion :: Id -> [Token] -> Token
- ShellCheck.AST: T_DollarSingleQuoted :: Id -> String -> Token
- ShellCheck.AST: T_Done :: Id -> Token
- ShellCheck.AST: T_DoubleQuoted :: Id -> [Token] -> Token
- ShellCheck.AST: T_EOF :: Id -> Token
- ShellCheck.AST: T_Elif :: Id -> Token
- ShellCheck.AST: T_Else :: Id -> Token
- ShellCheck.AST: T_Esac :: Id -> Token
- ShellCheck.AST: T_Extglob :: Id -> String -> [Token] -> Token
- ShellCheck.AST: T_FdRedirect :: Id -> String -> Token -> Token
- ShellCheck.AST: T_Fi :: Id -> Token
- ShellCheck.AST: T_For :: Id -> Token
- ShellCheck.AST: T_ForArithmetic :: Id -> Token -> Token -> Token -> [Token] -> Token
- ShellCheck.AST: T_ForIn :: Id -> String -> [Token] -> [Token] -> Token
- ShellCheck.AST: T_Function :: Id -> FunctionKeyword -> FunctionParentheses -> String -> Token -> Token
- ShellCheck.AST: T_GREATAND :: Id -> Token
- ShellCheck.AST: T_Glob :: Id -> String -> Token
- ShellCheck.AST: T_Greater :: Id -> Token
- ShellCheck.AST: T_HereDoc :: Id -> Dashed -> Quoted -> String -> [Token] -> Token
- ShellCheck.AST: T_HereString :: Id -> Token -> Token
- ShellCheck.AST: T_If :: Id -> Token
- ShellCheck.AST: T_IfExpression :: Id -> [([Token], [Token])] -> [Token] -> Token
- ShellCheck.AST: T_In :: Id -> Token
- ShellCheck.AST: T_Include :: Id -> Token -> Token
- ShellCheck.AST: T_IndexedElement :: Id -> [Token] -> Token -> Token
- ShellCheck.AST: T_IoDuplicate :: Id -> Token -> String -> Token
- ShellCheck.AST: T_IoFile :: Id -> Token -> Token -> Token
- ShellCheck.AST: T_LESSAND :: Id -> Token
- ShellCheck.AST: T_LESSGREAT :: Id -> Token
- ShellCheck.AST: T_Lbrace :: Id -> Token
- ShellCheck.AST: T_Less :: Id -> Token
- ShellCheck.AST: T_Literal :: Id -> String -> Token
- ShellCheck.AST: T_Lparen :: Id -> Token
- ShellCheck.AST: T_NEWLINE :: Id -> Token
- ShellCheck.AST: T_NormalWord :: Id -> [Token] -> Token
- ShellCheck.AST: T_OR_IF :: Id -> Token
- ShellCheck.AST: T_OrIf :: Id -> Token -> Token -> Token
- ShellCheck.AST: T_ParamSubSpecialChar :: Id -> String -> Token
- ShellCheck.AST: T_Pipe :: Id -> String -> Token
- ShellCheck.AST: T_Pipeline :: Id -> [Token] -> [Token] -> Token
- ShellCheck.AST: T_ProcSub :: Id -> String -> [Token] -> Token
- ShellCheck.AST: T_Rbrace :: Id -> Token
- ShellCheck.AST: T_Redirecting :: Id -> [Token] -> Token -> Token
- ShellCheck.AST: T_Rparen :: Id -> Token
- ShellCheck.AST: T_Script :: Id -> Token -> [Token] -> Token
- ShellCheck.AST: T_Select :: Id -> Token
- ShellCheck.AST: T_SelectIn :: Id -> String -> [Token] -> [Token] -> Token
- ShellCheck.AST: T_Semi :: Id -> Token
- ShellCheck.AST: T_SimpleCommand :: Id -> [Token] -> [Token] -> Token
- ShellCheck.AST: T_SingleQuoted :: Id -> String -> Token
- ShellCheck.AST: T_SourceCommand :: Id -> Token -> Token -> Token
- ShellCheck.AST: T_Subshell :: Id -> [Token] -> Token
- ShellCheck.AST: T_Then :: Id -> Token
- ShellCheck.AST: T_UnparsedIndex :: Id -> SourcePos -> String -> Token
- ShellCheck.AST: T_Until :: Id -> Token
- ShellCheck.AST: T_UntilExpression :: Id -> [Token] -> [Token] -> Token
- ShellCheck.AST: T_While :: Id -> Token
- ShellCheck.AST: T_WhileExpression :: Id -> [Token] -> [Token] -> Token
- ShellCheck.AST: tokenEquals :: Token -> Token -> Bool
- ShellCheck.AnalyzerLib: potentially :: Monad m => Maybe (m ()) -> m ()
- ShellCheck.Interface: instance GHC.Classes.Ord ShellCheck.Interface.Replacement
+ ShellCheck.AST: Inner_TA_Assignment :: String -> t -> t -> InnerToken t
+ ShellCheck.AST: Inner_TA_Binary :: String -> t -> t -> InnerToken t
+ ShellCheck.AST: Inner_TA_Expansion :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_TA_Sequence :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_TA_Trinary :: t -> t -> t -> InnerToken t
+ ShellCheck.AST: Inner_TA_Unary :: String -> t -> InnerToken t
+ ShellCheck.AST: Inner_TA_Variable :: String -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_TC_And :: ConditionType -> String -> t -> t -> InnerToken t
+ ShellCheck.AST: Inner_TC_Binary :: ConditionType -> String -> t -> t -> InnerToken t
+ ShellCheck.AST: Inner_TC_Empty :: ConditionType -> InnerToken t
+ ShellCheck.AST: Inner_TC_Group :: ConditionType -> t -> InnerToken t
+ ShellCheck.AST: Inner_TC_Nullary :: ConditionType -> t -> InnerToken t
+ ShellCheck.AST: Inner_TC_Or :: ConditionType -> String -> t -> t -> InnerToken t
+ ShellCheck.AST: Inner_TC_Unary :: ConditionType -> String -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_AND_IF :: InnerToken t
+ ShellCheck.AST: Inner_T_AndIf :: t -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_Annotation :: [Annotation] -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_Arithmetic :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_Array :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_Assignment :: AssignmentMode -> String -> [t] -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_Backgrounded :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_Backticked :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_Bang :: InnerToken t
+ ShellCheck.AST: Inner_T_Banged :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_BatsTest :: t -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_BraceExpansion :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_BraceGroup :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_CLOBBER :: InnerToken t
+ ShellCheck.AST: Inner_T_Case :: InnerToken t
+ ShellCheck.AST: Inner_T_CaseExpression :: t -> [(CaseType, [t], [t])] -> InnerToken t
+ ShellCheck.AST: Inner_T_CoProc :: Maybe String -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_CoProcBody :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_Condition :: ConditionType -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_DGREAT :: InnerToken t
+ ShellCheck.AST: Inner_T_DLESS :: InnerToken t
+ ShellCheck.AST: Inner_T_DLESSDASH :: InnerToken t
+ ShellCheck.AST: Inner_T_DSEMI :: InnerToken t
+ ShellCheck.AST: Inner_T_Do :: InnerToken t
+ ShellCheck.AST: Inner_T_DollarArithmetic :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_DollarBraceCommandExpansion :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_DollarBraced :: Bool -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_DollarBracket :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_DollarDoubleQuoted :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_DollarExpansion :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_DollarSingleQuoted :: String -> InnerToken t
+ ShellCheck.AST: Inner_T_Done :: InnerToken t
+ ShellCheck.AST: Inner_T_DoubleQuoted :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_EOF :: InnerToken t
+ ShellCheck.AST: Inner_T_Elif :: InnerToken t
+ ShellCheck.AST: Inner_T_Else :: InnerToken t
+ ShellCheck.AST: Inner_T_Esac :: InnerToken t
+ ShellCheck.AST: Inner_T_Extglob :: String -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_FdRedirect :: String -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_Fi :: InnerToken t
+ ShellCheck.AST: Inner_T_For :: InnerToken t
+ ShellCheck.AST: Inner_T_ForArithmetic :: t -> t -> t -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_ForIn :: String -> [t] -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_Function :: FunctionKeyword -> FunctionParentheses -> String -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_GREATAND :: InnerToken t
+ ShellCheck.AST: Inner_T_Glob :: String -> InnerToken t
+ ShellCheck.AST: Inner_T_Greater :: InnerToken t
+ ShellCheck.AST: Inner_T_HereDoc :: Dashed -> Quoted -> String -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_HereString :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_If :: InnerToken t
+ ShellCheck.AST: Inner_T_IfExpression :: [([t], [t])] -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_In :: InnerToken t
+ ShellCheck.AST: Inner_T_Include :: t -> InnerToken t
+ ShellCheck.AST: Inner_T_IndexedElement :: [t] -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_IoDuplicate :: t -> String -> InnerToken t
+ ShellCheck.AST: Inner_T_IoFile :: t -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_LESSAND :: InnerToken t
+ ShellCheck.AST: Inner_T_LESSGREAT :: InnerToken t
+ ShellCheck.AST: Inner_T_Lbrace :: InnerToken t
+ ShellCheck.AST: Inner_T_Less :: InnerToken t
+ ShellCheck.AST: Inner_T_Literal :: String -> InnerToken t
+ ShellCheck.AST: Inner_T_Lparen :: InnerToken t
+ ShellCheck.AST: Inner_T_NEWLINE :: InnerToken t
+ ShellCheck.AST: Inner_T_NormalWord :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_OR_IF :: InnerToken t
+ ShellCheck.AST: Inner_T_OrIf :: t -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_ParamSubSpecialChar :: String -> InnerToken t
+ ShellCheck.AST: Inner_T_Pipe :: String -> InnerToken t
+ ShellCheck.AST: Inner_T_Pipeline :: [t] -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_ProcSub :: String -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_Rbrace :: InnerToken t
+ ShellCheck.AST: Inner_T_Redirecting :: [t] -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_Rparen :: InnerToken t
+ ShellCheck.AST: Inner_T_Script :: t -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_Select :: InnerToken t
+ ShellCheck.AST: Inner_T_SelectIn :: String -> [t] -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_Semi :: InnerToken t
+ ShellCheck.AST: Inner_T_SimpleCommand :: [t] -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_SingleQuoted :: String -> InnerToken t
+ ShellCheck.AST: Inner_T_SourceCommand :: t -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_Subshell :: [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_Then :: InnerToken t
+ ShellCheck.AST: Inner_T_UnparsedIndex :: SourcePos -> String -> InnerToken t
+ ShellCheck.AST: Inner_T_Until :: InnerToken t
+ ShellCheck.AST: Inner_T_UntilExpression :: [t] -> [t] -> InnerToken t
+ ShellCheck.AST: Inner_T_While :: InnerToken t
+ ShellCheck.AST: Inner_T_WhileExpression :: [t] -> [t] -> InnerToken t
+ ShellCheck.AST: OuterToken :: Id -> InnerToken Token -> Token
+ ShellCheck.AST: data InnerToken t
+ ShellCheck.AST: instance Data.Foldable.Foldable ShellCheck.AST.InnerToken
+ ShellCheck.AST: instance Data.Traversable.Traversable ShellCheck.AST.InnerToken
+ ShellCheck.AST: instance GHC.Base.Functor ShellCheck.AST.InnerToken
+ ShellCheck.AST: instance GHC.Classes.Eq t => GHC.Classes.Eq (ShellCheck.AST.InnerToken t)
+ ShellCheck.AST: instance GHC.Show.Show t => GHC.Show.Show (ShellCheck.AST.InnerToken t)
+ ShellCheck.AST: pattern T_WhileExpression :: () => () => Id -> [Token] -> [Token] -> Token
+ ShellCheck.ASTLib: getCommandArgv :: Token -> Maybe [Token]
+ ShellCheck.ASTLib: getLiteralStringDef :: String -> Token -> String
+ ShellCheck.Analytics: instance GHC.Base.Monoid ShellCheck.Analytics.SpaceStatus
+ ShellCheck.Analytics: instance GHC.Base.Semigroup ShellCheck.Analytics.SpaceStatus
+ ShellCheck.Analytics: instance GHC.Classes.Eq ShellCheck.Analytics.SpaceStatus
+ ShellCheck.Checks.Commands: optionalChecks :: [CheckDescription]
+ ShellCheck.Data: arithmeticBinaryTestOps :: [[Char]]
+ ShellCheck.Data: flagsForRead :: [Char]
- ShellCheck.ASTLib: getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
+ ShellCheck.ASTLib: getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
- ShellCheck.AnalyzerLib: getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
+ ShellCheck.AnalyzerLib: getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
- ShellCheck.Checks.Commands: checker :: Parameters -> Checker
+ ShellCheck.Checks.Commands: checker :: AnalysisSpec -> Parameters -> Checker

Files

README.md view
@@ -109,6 +109,7 @@ * [Codacy](https://www.codacy.com/) * [Code Climate](https://codeclimate.com/) * [Code Factor](https://www.codefactor.io/)+* [Github](https://github.com/features/actions)(only Linux)  Services and platforms with third party plugins: @@ -116,7 +117,7 @@  Most other services, including [GitLab](https://about.gitlab.com/), let you install ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),-or by downloading and unpacking a [binary release](#installing-the-shellcheck-binary).+or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).  It's a good idea to manually install a specific ShellCheck version regardless. This avoids any surprise build breaks when a new version with new warnings is published.@@ -209,15 +210,21 @@  or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled. +Using the [nix package manager](https://nixos.org/nix):+```sh+nix-env -iA nixpkgs.shellcheck+```+ 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)-* [Linux, aarch64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz) aka ARM64 (statically linked)-* [MacOS, x86_64](https://shellcheck.storage.googleapis.com/shellcheck-stable.darwin.x86_64.tar.xz)-* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)+* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)+* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)+* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)+* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)+* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/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.+or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases+(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).  Distro packages already come with a `man` page. If you are building from source, it can be installed with: @@ -244,7 +251,7 @@  ```bash scversion="stable" # or "v0.4.7", or "latest"-wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv+wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv cp "shellcheck-${scversion}/shellcheck" /usr/bin/ shellcheck --version ```
Setup.hs view
@@ -1,36 +1,2 @@-import Distribution.PackageDescription (-  HookedBuildInfo,-  emptyHookedBuildInfo )-import Distribution.Simple (-  Args,-  UserHooks ( preSDist ),-  defaultMainWithHooks,-  simpleUserHooks )-import Distribution.Simple.Setup ( SDistFlags )--import System.Process ( system )---main = defaultMainWithHooks myHooks-  where-    myHooks = simpleUserHooks { preSDist = myPreSDist }---- | This hook will be executed before e.g. @cabal sdist@. It runs---   pandoc to create the man page from shellcheck.1.md. If the pandoc---   command is not found, this will fail with an error message:------     /bin/sh: pandoc: command not found------   Since the man page is listed in the Extra-Source-Files section of---   our cabal file, a failure here should result in a failure to---   create the distribution tarball (that's a good thing).----myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo-myPreSDist _ _ = do-  putStrLn "Building the man page (shellcheck.1) with pandoc..."-  putStrLn pandoc_cmd-  result <- system pandoc_cmd-  putStrLn $ "pandoc exited with " ++ show result-  return emptyHookedBuildInfo-  where-    pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"+import Distribution.Simple+main = defaultMain
ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name:             ShellCheck-Version:          0.7.0+Version:          0.7.1 Synopsis:         Shell script analysis tool License:          GPL-3 License-file:     LICENSE@@ -7,7 +7,7 @@ Author:           Vidar Holen Maintainer:       vidar@vidarholen.net Homepage:         https://www.shellcheck.net/-Build-Type:       Custom+Build-Type:       Simple Cabal-Version:    >= 1.8 Bug-reports:      https://github.com/koalaman/shellcheck/issues Description:@@ -26,19 +26,13 @@     -- documentation     README.md     shellcheck.1.md-    -- built with a cabal sdist hook-    shellcheck.1+    -- A script to build the man page using pandoc+    manpage     -- convenience script for stripping tests     striptests     -- tests     test/shellcheck.hs -custom-setup-  setup-depends:-    base    >= 4    && <5,-    process >= 1.0  && <1.7,-    Cabal   >= 1.10 && <2.5- source-repository head     type: git     location: git://github.com/koalaman/shellcheck.git@@ -51,9 +45,7 @@     build-depends:       aeson,       array,-      -- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.-      -- Just disable that version entirely to fail fast.-      base > 4.6.0.1 && < 5,+      base >= 4.8.0.0 && < 5,       bytestring,       containers >= 0.5,       deepseq >= 1.4.0.0,
+ manpage view
@@ -0,0 +1,4 @@+#!/bin/sh+echo >&2 "Generating man page using pandoc"+pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit+echo >&2 "Done. You can read it with:   man ./shellcheck.1"
− shellcheck.1
@@ -1,449 +0,0 @@-.\" Automatically generated by Pandoc 2.2.1-.\"-.TH "SHELLCHECK" "1" "" "Shell script analysis tool" ""-.hy-.SH NAME-.PP-shellcheck \- Shell script analysis tool-.SH SYNOPSIS-.PP-\f[B]shellcheck\f[] [\f[I]OPTIONS\f[]...] \f[I]FILES\f[]...-.SH DESCRIPTION-.PP-ShellCheck is a static analysis and linting tool for sh/bash scripts.-It\[aq]s mainly focused on handling typical beginner and intermediate-level syntax errors and pitfalls where the shell just gives a cryptic-error message or strange behavior, but it also reports on a few more-advanced issues where corner cases can cause delayed failures.-.PP-ShellCheck gives shell specific advice.-Consider this line:-.IP-.nf-\f[C]-((\ area\ =\ 3.14*r*r\ ))-\f[]-.fi-.IP \[bu] 2-For scripts starting with \f[C]#!/bin/sh\f[] (or when using-\f[C]\-s\ sh\f[]), ShellCheck will warn that \f[C]((\ ..\ ))\f[] is not-POSIX compliant (similar to checkbashisms).-.IP \[bu] 2-For scripts starting with \f[C]#!/bin/bash\f[] (or using-\f[C]\-s\ bash\f[]), ShellCheck will warn that decimals are not-supported.-.IP \[bu] 2-For scripts starting with \f[C]#!/bin/ksh\f[] (or using-\f[C]\-s\ ksh\f[]), ShellCheck will not warn at all, as \f[C]ksh\f[]-supports decimals in arithmetic contexts.-.SH OPTIONS-.TP-.B \f[B]\-a\f[],\ \f[B]\-\-check\-sourced\f[]-Emit warnings in sourced files.-Normally, \f[C]shellcheck\f[] will only warn about issues in the-specified files.-With this option, any issues in sourced files will also be reported.-.RS-.RE-.TP-.B \f[B]\-C\f[][\f[I]WHEN\f[]],\ \f[B]\-\-color\f[][=\f[I]WHEN\f[]]-For TTY output, enable colors \f[I]always\f[], \f[I]never\f[] or-\f[I]auto\f[].-The default is \f[I]auto\f[].-\f[B]\-\-color\f[] without an argument is equivalent to-\f[B]\-\-color=always\f[].-.RS-.RE-.TP-.B \f[B]\-i\f[]\ \f[I]CODE1\f[][,\f[I]CODE2\f[]...],\ \f[B]\-\-include=\f[]\f[I]CODE1\f[][,\f[I]CODE2\f[]...]-Explicitly include only the specified codes in the report.-Subsequent \f[B]\-i\f[] options are cumulative, but all the codes can be-specified at once, comma\-separated as a single argument.-Include options override any provided exclude options.-.RS-.RE-.TP-.B \f[B]\-e\f[]\ \f[I]CODE1\f[][,\f[I]CODE2\f[]...],\ \f[B]\-\-exclude=\f[]\f[I]CODE1\f[][,\f[I]CODE2\f[]...]-Explicitly exclude the specified codes from the report.-Subsequent \f[B]\-e\f[] options are cumulative, but all the codes can be-specified at once, comma\-separated as a single argument.-.RS-.RE-.TP-.B \f[B]\-f\f[] \f[I]FORMAT\f[], \f[B]\-\-format=\f[]\f[I]FORMAT\f[]-Specify the output format of shellcheck, which prints its results in the-standard output.-Subsequent \f[B]\-f\f[] options are ignored, see \f[B]FORMATS\f[] below-for more information.-.RS-.RE-.TP-.B \f[B]\-\-list\-optional\f[]-Output a list of known optional checks.-These can be enabled with \f[B]\-o\f[] flags or \f[B]enable\f[]-directives.-.RS-.RE-.TP-.B \f[B]\-\-norc\f[]-Don\[aq]t try to look for .shellcheckrc configuration files.-.RS-.RE-.TP-.B \f[B]\-o\f[]\ \f[I]NAME1\f[][,\f[I]NAME2\f[]...],\ \f[B]\-\-enable=\f[]\f[I]NAME1\f[][,\f[I]NAME2\f[]...]-Enable optional checks.-The special name \f[I]all\f[] enables all of them.-Subsequent \f[B]\-o\f[] options accumulate.-This is equivalent to specifying \f[B]enable\f[] directives.-.RS-.RE-.TP-.B \f[B]\-P\f[]\ \f[I]SOURCEPATH\f[],\ \f[B]\-\-source\-path=\f[]\f[I]SOURCEPATH\f[]-Specify paths to search for sourced files, separated by \f[C]:\f[] on-Unix and \f[C];\f[] on Windows.-This is equivalent to specifying \f[C]search\-path\f[] directives.-.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-\f[I]ksh\f[].-The default is to deduce the shell from the file\[aq]s \f[C]shell\f[]-directive, shebang, or \f[C]\&.bash/.bats/.dash/.ksh\f[] extension, in-that order.-\f[I]sh\f[] refers to POSIX \f[C]sh\f[] (not the system\[aq]s), and will-warn of portability issues.-.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 in order of severity 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]\-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-.B \f[B]\-x\f[],\ \f[B]\-\-external\-sources\f[]-Follow \[aq]source\[aq] statements even when the file is not specified-as input.-By default, \f[C]shellcheck\f[] will only follow files specified on the-command line (plus \f[C]/dev/null\f[]).-This option allows following any file the script may \f[C]source\f[].-.RS-.RE-.TP-.B \f[B]FILES...\f[]-One or more script files to check, or "\-" for standard input.-.RS-.RE-.SH FORMATS-.TP-.B \f[B]tty\f[]-Plain text, human readable output.-This is the default.-.RS-.RE-.TP-.B \f[B]gcc\f[]-GCC compatible output.-Useful for editors that support compiling and showing syntax errors.-.RS-.PP-For example, in Vim,-\f[C]:set\ makeprg=shellcheck\\\ \-f\\\ gcc\\\ %\f[] will allow using-\f[C]:make\f[] to check the script, and \f[C]:cnext\f[] to jump to the-next error.-.IP-.nf-\f[C]-<file>:<line>:<column>:\ <type>:\ <message>-\f[]-.fi-.RE-.TP-.B \f[B]checkstyle\f[]-Checkstyle compatible XML output.-Supported directly or through plugins by many IDEs and build monitoring-systems.-.RS-.IP-.nf-\f[C]-<?xml\ version=\[aq]1.0\[aq]\ encoding=\[aq]UTF\-8\[aq]?>-<checkstyle\ version=\[aq]4.3\[aq]>-\ \ <file\ name=\[aq]file\[aq]>-\ \ \ \ <error-\ \ \ \ \ \ line=\[aq]line\[aq]-\ \ \ \ \ \ column=\[aq]column\[aq]-\ \ \ \ \ \ severity=\[aq]severity\[aq]-\ \ \ \ \ \ message=\[aq]message\[aq]-\ \ \ \ \ \ source=\[aq]ShellCheck.SC####\[aq]\ />-\ \ \ \ ...-\ \ </file>-\ \ ...-</checkstyle>-\f[]-.fi-.RE-.TP-.B \f[B]diff\f[]-Auto\-fixes in unified diff format.-Can be piped to \f[C]git\ apply\f[] or \f[C]patch\ \-p1\f[] to-automatically apply fixes.-.RS-.IP-.nf-\f[C]-\-\-\-\ a/test.sh-+++\ b/test.sh-\@\@\ \-2,6\ +2,6\ \@\@-\ ##\ Example\ of\ a\ broken\ script.-\ for\ f\ in\ $(ls\ *.m3u)-\ do-\-\ \ grep\ \-qi\ hq.*mp3\ $f\ \\-+\ \ grep\ \-qi\ hq.*mp3\ "$f"\ \\-\ \ \ \ \ &&\ echo\ \-e\ \[aq]Playlist\ $f\ contains\ a\ HQ\ file\ in\ mp3\ format\[aq]-\ done-\f[]-.fi-.RE-.TP-.B \f[B]json1\f[]-Json is a popular serialization format that is more suitable for web-applications.-ShellCheck\[aq]s json is compact and contains only the bare minimum.-Tabs are counted as 1 character.-.RS-.IP-.nf-\f[C]-{-\ \ comments:\ [-\ \ \ \ {-\ \ \ \ \ \ "file":\ "filename",-\ \ \ \ \ \ "line":\ lineNumber,-\ \ \ \ \ \ "column":\ columnNumber,-\ \ \ \ \ \ "level":\ "severitylevel",-\ \ \ \ \ \ "code":\ errorCode,-\ \ \ \ \ \ "message":\ "warning\ message"-\ \ \ \ },-\ \ \ \ ...-\ \ ]-}-\f[]-.fi-.RE-.TP-.B \f[B]json\f[]-This is a legacy version of the \f[B]json1\f[] format.-It\[aq]s a raw array of comments, and all offsets have a tab stop of 8.-.RS-.RE-.TP-.B \f[B]quiet\f[]-Suppress all normal output.-Exit with zero if no issues are found, otherwise exit with one.-Stops processing after the first issue.-.RS-.RE-.SH DIRECTIVES-.PP-ShellCheck directives can be specified as comments in the shell script.-If they appear before the first command, they are considered file\-wide.-Otherwise, they apply to the immediately following command or block:-.IP-.nf-\f[C]-#\ shellcheck\ key=value\ key=value-command\-or\-structure-\f[]-.fi-.PP-For example, to suppress SC2035 about using \f[C]\&./*.jpg\f[]:-.IP-.nf-\f[C]-#\ shellcheck\ disable=SC2035-echo\ "Files:\ "\ *.jpg-\f[]-.fi-.PP-To tell ShellCheck where to look for an otherwise dynamically determined-file:-.IP-.nf-\f[C]-#\ shellcheck\ source=./lib.sh-source\ "$(find_install_dir)/lib.sh"-\f[]-.fi-.PP-Here a shell brace group is used to suppress a warning on multiple-lines:-.IP-.nf-\f[C]-#\ shellcheck\ disable=SC2016-{-\ \ echo\ \[aq]Modifying\ $PATH\[aq]-\ \ echo\ \[aq]PATH=foo:$PATH\[aq]\ >>\ ~/.bashrc-}-\f[]-.fi-.PP-Valid keys are:-.TP-.B \f[B]disable\f[]-Disables a comma separated list of error codes for the following-command.-The command can be a simple command like \f[C]echo\ foo\f[], or a-compound command like a function definition, subshell block or loop.-.RS-.RE-.TP-.B \f[B]enable\f[]-Enable an optional check by name, as listed with-\f[B]\-\-list\-optional\f[].-Only file\-wide \f[C]enable\f[] directives are considered.-.RS-.RE-.TP-.B \f[B]source\f[]-Overrides the filename included by a \f[C]source\f[]/\f[C]\&.\f[]-statement.-This can be used to tell shellcheck where to look for a file whose name-is determined at runtime, or to skip a source by telling it to use-\f[C]/dev/null\f[].-.RS-.RE-.TP-.B \f[B]source\-path\f[]-Add a directory to the search path for \f[C]source\f[]/\f[C]\&.\f[]-statements (by default, only ShellCheck\[aq]s working directory is-included).-Absolute paths will also be rooted in these paths.-The special path \f[C]SCRIPTDIR\f[] can be used to specify the currently-checked script\[aq]s directory, as in \f[C]source\-path=SCRIPTDIR\f[] or-\f[C]source\-path=SCRIPTDIR/../libs\f[].-Multiple paths accumulate, and \f[C]\-P\f[] takes precedence over them.-.RS-.RE-.TP-.B \f[B]shell\f[]-Overrides the shell detected from the shebang.-This is useful for files meant to be included (and thus lacking a-shebang), or possibly as a more targeted alternative to-\[aq]disable=2039\[aq].-.RS-.RE-.SH RC FILES-.PP-Unless \f[C]\-\-norc\f[] is used, ShellCheck will look for a file-\f[C]\&.shellcheckrc\f[] or \f[C]shellcheckrc\f[] in the script\[aq]s-directory and each parent directory.-If found, it will read \f[C]key=value\f[] pairs from it and treat them-as file\-wide directives.-.PP-Here is an example \f[C]\&.shellcheckrc\f[]:-.IP-.nf-\f[C]-#\ Look\ for\ \[aq]source\[aq]d\ files\ relative\ to\ the\ checked\ script,-#\ and\ also\ look\ for\ absolute\ paths\ in\ /mnt/chroot-source\-path=SCRIPTDIR-source\-path=/mnt/chroot--#\ Turn\ on\ warnings\ for\ unquoted\ variables\ with\ safe\ values-enable=quote\-safe\-variables--#\ Turn\ on\ warnings\ for\ unassigned\ uppercase\ variables-enable=check\-unassigned\-uppercase--#\ Allow\ using\ `which`\ since\ it\ gives\ full\ paths\ and\ is\ common\ enough-disable=SC2230-\f[]-.fi-.PP-If no \f[C]\&.shellcheckrc\f[] is found in any of the parent-directories, ShellCheck will look in \f[C]~/.shellcheckrc\f[] followed-by the XDG config directory (usually \f[C]~/.config/shellcheckrc\f[]) on-Unix, or \f[C]%APPDATA%/shellcheckrc\f[] on Windows.-Only the first file found will be used.-.PP-Note for Snap users: the Snap sandbox disallows access to hidden files.-Use \f[C]shellcheckrc\f[] without the dot instead.-.PP-Note for Docker users: ShellCheck will only be able to look for files-that are mounted in the container, so \f[C]~/.shellcheckrc\f[] will not-be read.-.SH ENVIRONMENT VARIABLES-.PP-The environment variable \f[C]SHELLCHECK_OPTS\f[] can be set with-default flags:-.IP-.nf-\f[C]-export\ SHELLCHECK_OPTS=\[aq]\-\-shell=bash\ \-\-exclude=SC2016\[aq]-\f[]-.fi-.PP-Its value will be split on spaces and prepended to the command line on-each invocation.-.SH RETURN VALUES-.PP-ShellCheck uses the follow exit codes:-.IP \[bu] 2-0: All files successfully scanned with no issues.-.IP \[bu] 2-1: All files successfully scanned with some issues.-.IP \[bu] 2-2: Some files could not be processed (e.g.-file not found).-.IP \[bu] 2-3: ShellCheck was invoked with bad syntax (e.g.-unknown flag).-.IP \[bu] 2-4: ShellCheck was invoked with bad options (e.g.-unknown formatter).-.SH LOCALE-.PP-This version of ShellCheck is only available in English.-All files are leniently decoded as UTF\-8, with a fallback of-ISO\-8859\-1 for invalid sequences.-\f[C]LC_CTYPE\f[] is respected for output, and defaults to UTF\-8 for-locales where encoding is unspecified (such as the \f[C]C\f[] locale).-.PP-Windows users seeing-\f[C]commitBuffer:\ invalid\ argument\ (invalid\ character)\f[] should-set their terminal to use UTF\-8 with \f[C]chcp\ 65001\f[].-.SH AUTHORS-.PP-ShellCheck is developed and maintained by Vidar Holen, with assistance-from a long list of wonderful contributors.-.SH REPORTING BUGS-.PP-Bugs and issues can be reported on GitHub:-.PP-https://github.com/koalaman/shellcheck/issues-.SH COPYRIGHT-.PP-Copyright 2012\-2019, Vidar Holen and contributors.-Licensed under the GNU General Public License version 3 or later, see-https://gnu.org/licenses/gpl.html-.SH SEE ALSO-.PP-sh(1) bash(1)
shellcheck.1.md view
@@ -107,7 +107,7 @@  **-x**,\ **--external-sources** -:   Follow 'source' statements even when the file is not specified as input.+:   Follow `source` statements even when the file is not specified as input.     By default, `shellcheck` will only follow files specified on the command     line (plus `/dev/null`). This option allows following any file the script     may `source`.@@ -275,8 +275,8 @@     # Turn on warnings for unassigned uppercase variables     enable=check-unassigned-uppercase -    # Allow using `which` since it gives full paths and is common enough-    disable=SC2230+    # Allow [ ! -z foo ] instead of suggesting -n+    disable=SC2236  If no `.shellcheckrc` is found in any of the parent directories, ShellCheck will look in `~/.shellcheckrc` followed by the XDG config directory@@ -301,7 +301,7 @@  # RETURN VALUES -ShellCheck uses the follow exit codes:+ShellCheck uses the following exit codes:  + 0: All files successfully scanned with no issues. + 1: All files successfully scanned with some issues.
shellcheck.hs view
@@ -491,6 +491,12 @@         first <- a arg         if not first then return False else b arg +    findM p = foldr go (pure Nothing)+      where+        go x acc = do+            b <- p x+            if b then pure (Just x) else acc+     findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =         if isAbsolute original         then@@ -500,11 +506,11 @@             find original original       where         find filename deflt = do-            sources <- filterM ((allowable inputs) `andM` doesFileExist) $+            sources <- findM ((allowable inputs) `andM` doesFileExist) $                         (adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)             case sources of-                [] -> return deflt-                (first:_) -> return first+                Nothing -> return deflt+                Just first -> return first         scriptdir = dropFileName currentScript         adjustPath str =             case (splitDirectories str) of
src/ShellCheck/AST.hs view
@@ -17,7 +17,7 @@     You should have received a copy of the GNU General Public License     along with this program.  If not, see <https://www.gnu.org/licenses/>. -}-{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-} module ShellCheck.AST where  import GHC.Generics (Generic)@@ -37,110 +37,112 @@ data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)  newtype Root = Root Token-data Token =-    TA_Binary Id String Token Token-    | TA_Assignment Id String Token Token-    | TA_Variable Id String [Token]-    | TA_Expansion Id [Token]-    | TA_Sequence Id [Token]-    | TA_Trinary Id Token Token Token-    | TA_Unary Id String Token-    | TC_And Id ConditionType String Token Token-    | TC_Binary Id ConditionType String Token Token-    | TC_Group Id ConditionType Token-    | TC_Nullary Id ConditionType Token-    | TC_Or Id ConditionType String Token Token-    | TC_Unary Id ConditionType String Token-    | TC_Empty Id ConditionType-    | T_AND_IF Id-    | T_AndIf Id Token Token-    | T_Arithmetic Id Token-    | T_Array Id [Token]-    | T_IndexedElement Id [Token] Token+data Token = OuterToken Id (InnerToken Token) deriving (Show)++data InnerToken t =+    Inner_TA_Binary String t t+    | Inner_TA_Assignment String t t+    | Inner_TA_Variable String [t]+    | Inner_TA_Expansion [t]+    | Inner_TA_Sequence [t]+    | Inner_TA_Trinary t t t+    | Inner_TA_Unary String t+    | Inner_TC_And ConditionType String t t+    | Inner_TC_Binary ConditionType String t t+    | Inner_TC_Group ConditionType t+    | Inner_TC_Nullary ConditionType t+    | Inner_TC_Or ConditionType String t t+    | Inner_TC_Unary ConditionType String t+    | Inner_TC_Empty ConditionType+    | Inner_T_AND_IF+    | Inner_T_AndIf t t+    | Inner_T_Arithmetic t+    | Inner_T_Array [t]+    | Inner_T_IndexedElement [t] t     -- Store the index as string, and parse as arithmetic or string later-    | T_UnparsedIndex Id SourcePos String-    | T_Assignment Id AssignmentMode String [Token] Token-    | T_Backgrounded Id Token-    | T_Backticked Id [Token]-    | T_Bang Id-    | T_Banged Id Token-    | T_BraceExpansion Id [Token]-    | T_BraceGroup Id [Token]-    | T_CLOBBER Id-    | T_Case Id-    | T_CaseExpression Id Token [(CaseType, [Token], [Token])]-    | T_Condition Id ConditionType Token-    | T_DGREAT Id-    | T_DLESS Id-    | T_DLESSDASH Id-    | T_DSEMI Id-    | T_Do Id-    | T_DollarArithmetic Id Token-    | T_DollarBraced Id Bool Token-    | T_DollarBracket Id Token-    | T_DollarDoubleQuoted Id [Token]-    | T_DollarExpansion Id [Token]-    | T_DollarSingleQuoted Id String-    | T_DollarBraceCommandExpansion Id [Token]-    | T_Done Id-    | T_DoubleQuoted Id [Token]-    | T_EOF Id-    | T_Elif Id-    | T_Else Id-    | T_Esac Id-    | T_Extglob Id String [Token]-    | T_FdRedirect Id String Token-    | T_Fi Id-    | T_For Id-    | T_ForArithmetic Id Token Token Token [Token]-    | T_ForIn Id String [Token] [Token]-    | T_Function Id FunctionKeyword FunctionParentheses String Token-    | T_GREATAND Id-    | T_Glob Id String-    | T_Greater Id-    | T_HereDoc Id Dashed Quoted String [Token]-    | T_HereString Id Token-    | T_If Id-    | T_IfExpression Id [([Token],[Token])] [Token]-    | T_In  Id-    | T_IoFile Id Token Token-    | T_IoDuplicate Id Token String-    | T_LESSAND Id-    | T_LESSGREAT Id-    | T_Lbrace Id-    | T_Less Id-    | T_Literal Id String-    | T_Lparen Id-    | T_NEWLINE Id-    | T_NormalWord Id [Token]-    | T_OR_IF Id-    | T_OrIf Id Token Token-    | T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar}  or '/' in ${foo/bar/baz}-    | T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]-    | T_ProcSub Id String [Token]-    | T_Rbrace Id-    | T_Redirecting Id [Token] Token-    | T_Rparen Id-    | T_Script Id Token [Token] -- Shebang T_Literal, followed by script.-    | T_Select Id-    | T_SelectIn Id String [Token] [Token]-    | T_Semi Id-    | T_SimpleCommand Id [Token] [Token]-    | T_SingleQuoted Id String-    | T_Subshell Id [Token]-    | T_Then Id-    | T_Until Id-    | T_UntilExpression Id [Token] [Token]-    | T_While Id-    | T_WhileExpression Id [Token] [Token]-    | T_Annotation Id [Annotation] Token-    | T_Pipe Id String-    | T_CoProc Id (Maybe String) Token-    | T_CoProcBody Id Token-    | T_Include Id Token-    | T_SourceCommand Id Token Token-    | T_BatsTest Id Token Token-    deriving (Show)+    | Inner_T_UnparsedIndex SourcePos String+    | Inner_T_Assignment AssignmentMode String [t] t+    | Inner_T_Backgrounded t+    | Inner_T_Backticked [t]+    | Inner_T_Bang+    | Inner_T_Banged t+    | Inner_T_BraceExpansion [t]+    | Inner_T_BraceGroup [t]+    | Inner_T_CLOBBER+    | Inner_T_Case+    | Inner_T_CaseExpression t [(CaseType, [t], [t])]+    | Inner_T_Condition ConditionType t+    | Inner_T_DGREAT+    | Inner_T_DLESS+    | Inner_T_DLESSDASH+    | Inner_T_DSEMI+    | Inner_T_Do+    | Inner_T_DollarArithmetic t+    | Inner_T_DollarBraced Bool t+    | Inner_T_DollarBracket t+    | Inner_T_DollarDoubleQuoted [t]+    | Inner_T_DollarExpansion [t]+    | Inner_T_DollarSingleQuoted String+    | Inner_T_DollarBraceCommandExpansion [t]+    | Inner_T_Done+    | Inner_T_DoubleQuoted [t]+    | Inner_T_EOF+    | Inner_T_Elif+    | Inner_T_Else+    | Inner_T_Esac+    | Inner_T_Extglob String [t]+    | Inner_T_FdRedirect String t+    | Inner_T_Fi+    | Inner_T_For+    | Inner_T_ForArithmetic t t t [t]+    | Inner_T_ForIn String [t] [t]+    | Inner_T_Function FunctionKeyword FunctionParentheses String t+    | Inner_T_GREATAND+    | Inner_T_Glob String+    | Inner_T_Greater+    | Inner_T_HereDoc Dashed Quoted String [t]+    | Inner_T_HereString t+    | Inner_T_If+    | Inner_T_IfExpression [([t],[t])] [t]+    | Inner_T_In+    | Inner_T_IoFile t t+    | Inner_T_IoDuplicate t String+    | Inner_T_LESSAND+    | Inner_T_LESSGREAT+    | Inner_T_Lbrace+    | Inner_T_Less+    | Inner_T_Literal String+    | Inner_T_Lparen+    | Inner_T_NEWLINE+    | Inner_T_NormalWord [t]+    | Inner_T_OR_IF+    | Inner_T_OrIf t t+    | Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar}  or '/' in ${foo/bar/baz}+    | Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands]+    | Inner_T_ProcSub String [t]+    | Inner_T_Rbrace+    | Inner_T_Redirecting [t] t+    | Inner_T_Rparen+    | Inner_T_Script t [t] -- Shebang T_Literal, followed by script.+    | Inner_T_Select+    | Inner_T_SelectIn String [t] [t]+    | Inner_T_Semi+    | Inner_T_SimpleCommand [t] [t]+    | Inner_T_SingleQuoted String+    | Inner_T_Subshell [t]+    | Inner_T_Then+    | Inner_T_Until+    | Inner_T_UntilExpression [t] [t]+    | Inner_T_While+    | Inner_T_WhileExpression [t] [t]+    | Inner_T_Annotation [Annotation] t+    | Inner_T_Pipe String+    | Inner_T_CoProc (Maybe String) t+    | Inner_T_CoProcBody t+    | Inner_T_Include t+    | Inner_T_SourceCommand t t+    | Inner_T_BatsTest t t+    deriving (Show, Eq, Functor, Foldable, Traversable)  data Annotation =     DisableComment Integer@@ -151,240 +153,125 @@     deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) --- This is an abomination.-tokenEquals :: Token -> Token -> Bool-tokenEquals a b = kludge a == kludge b-    where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"+pattern T_AND_IF id = OuterToken id Inner_T_AND_IF+pattern T_Bang id = OuterToken id Inner_T_Bang+pattern T_Case id = OuterToken id Inner_T_Case+pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ)+pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER+pattern T_DGREAT id = OuterToken id Inner_T_DGREAT+pattern T_DLESS id = OuterToken id Inner_T_DLESS+pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH+pattern T_Do id = OuterToken id Inner_T_Do+pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str)+pattern T_Done id = OuterToken id Inner_T_Done+pattern T_DSEMI id = OuterToken id Inner_T_DSEMI+pattern T_Elif id = OuterToken id Inner_T_Elif+pattern T_Else id = OuterToken id Inner_T_Else+pattern T_EOF id = OuterToken id Inner_T_EOF+pattern T_Esac id = OuterToken id Inner_T_Esac+pattern T_Fi id = OuterToken id Inner_T_Fi+pattern T_For id = OuterToken id Inner_T_For+pattern T_Glob id str = OuterToken id (Inner_T_Glob str)+pattern T_GREATAND id = OuterToken id Inner_T_GREATAND+pattern T_Greater id = OuterToken id Inner_T_Greater+pattern T_If id = OuterToken id Inner_T_If+pattern T_In id = OuterToken id Inner_T_In+pattern T_Lbrace id = OuterToken id Inner_T_Lbrace+pattern T_Less id = OuterToken id Inner_T_Less+pattern T_LESSAND id = OuterToken id Inner_T_LESSAND+pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT+pattern T_Literal id str = OuterToken id (Inner_T_Literal str)+pattern T_Lparen id = OuterToken id Inner_T_Lparen+pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE+pattern T_OR_IF id = OuterToken id Inner_T_OR_IF+pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str)+pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str)+pattern T_Rbrace id = OuterToken id Inner_T_Rbrace+pattern T_Rparen id = OuterToken id Inner_T_Rparen+pattern T_Select id = OuterToken id Inner_T_Select+pattern T_Semi id = OuterToken id Inner_T_Semi+pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str)+pattern T_Then id = OuterToken id Inner_T_Then+pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str)+pattern T_Until id = OuterToken id Inner_T_Until+pattern T_While id = OuterToken id Inner_T_While+pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2)+pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2)+pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t)+pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u)+pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)+pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)+pattern T_Array id t = OuterToken id (Inner_T_Array t)+pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)+pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)+pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)+pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)+pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t)+pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l)+pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list)+pattern T_Banged id l = OuterToken id (Inner_T_Banged l)+pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t)+pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list)+pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l)+pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2)+pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases)+pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs)+pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token)+pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token)+pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token)+pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t)+pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)+pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)+pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)+pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)+pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)+pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)+pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)+pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)+pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list)+pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list)+pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l)+pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t)+pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group)+pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l)+pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body)+pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l)+pattern T_HereString id word = OuterToken id (Inner_T_HereString word)+pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses)+pattern T_Include id script = OuterToken id (Inner_T_Include script)+pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t)+pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num)+pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file)+pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list)+pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u)+pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2)+pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l)+pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd)+pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list)+pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l)+pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds)+pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include)+pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)+pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)+pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) +{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}+ instance Eq Token where-    (==) = tokenEquals+    OuterToken _ a == OuterToken _ b = a == b  analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token analyze f g i =     round   where-    round t = do+    round t@(OuterToken id it) = do         f t-        newT <- delve t+        newIt <- traverse round it         g t-        i newT-    roundAll = mapM round--    dl l v = do-        x <- roundAll l-        return $ v x-    dll l m v = do-        x <- roundAll l-        y <- roundAll m-        return $ v x y-    d1 t v = do-        x <- round t-        return $ v x-    d2 t1 t2 v = do-        x <- round t1-        y <- round t2-        return $ v x y--    delve (T_NormalWord id list) = dl list $ T_NormalWord id-    delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id-    delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id-    delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id-    delve (T_DollarBraceCommandExpansion id list) = dl list $ T_DollarBraceCommandExpansion id-    delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id-    delve (T_Backticked id list) = dl list $ T_Backticked id-    delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id-    delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id-    delve (T_IoFile id op file) = d2 op file $ T_IoFile id-    delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num-    delve (T_HereString id word) = d1 word $ T_HereString id-    delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v-    delve (T_Assignment id mode var indices value) = do-        a <- roundAll indices-        b <- round value-        return $ T_Assignment id mode var a b-    delve (T_Array id t) = dl t $ T_Array id-    delve (T_IndexedElement id indices t) = do-        a <- roundAll indices-        b <- round t-        return $ T_IndexedElement id a b-    delve (T_Redirecting id redirs cmd) = do-        a <- roundAll redirs-        b <- round cmd-        return $ T_Redirecting id a b-    delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id-    delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id-    delve (T_Banged id l) = d1 l $ T_Banged id-    delve (T_AndIf id t u) = d2 t u $ T_AndIf id-    delve (T_OrIf id t u) = d2 t u $ T_OrIf id-    delve (T_Backgrounded id l) = d1 l $ T_Backgrounded id-    delve (T_Subshell id l) = dl l $ T_Subshell id-    delve (T_ProcSub id typ l) = dl l $ T_ProcSub id typ-    delve (T_Arithmetic id c) = d1 c $ T_Arithmetic id-    delve (T_IfExpression id conditions elses) = do-        newConds <- mapM (\(c, t) -> do-                            x <- mapM round c-                            y <- mapM round t-                            return (x,y)-                    ) conditions-        newElses <- roundAll elses-        return $ T_IfExpression id newConds newElses-    delve (T_BraceGroup id l) = dl l $ T_BraceGroup id-    delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id-    delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id-    delve (T_ForIn id v w l) = dll w l $ T_ForIn id v-    delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v-    delve (T_CaseExpression id word cases) = do-        newWord <- round word-        newCases <- mapM (\(o, c, t) -> do-                            x <- mapM round c-                            y <- mapM round t-                            return (o, x,y)-                        ) cases-        return $ T_CaseExpression id newWord newCases--    delve (T_ForArithmetic id a b c group) = do-        x <- round a-        y <- round b-        z <- round c-        list <- mapM round group-        return $ T_ForArithmetic id x y z list--    delve (T_Script id s l) = dl l $ T_Script id s-    delve (T_Function id a b name body) = d1 body $ T_Function id a b name-    delve (T_Condition id typ token) = d1 token $ T_Condition id typ-    delve (T_Extglob id str l) = dl l $ T_Extglob id str-    delve (T_DollarBraced id braced op) = d1 op $ T_DollarBraced id braced-    delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str--    delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str-    delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str-    delve (TC_Group id typ token) = d1 token $ TC_Group id typ-    delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op-    delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op-    delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ--    delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op-    delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op-    delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op-    delve (TA_Sequence id l) = dl l $ TA_Sequence id-    delve (TA_Trinary id t1 t2 t3) = do-        a <- round t1-        b <- round t2-        c <- round t3-        return $ TA_Trinary id a b c-    delve (TA_Expansion id t) = dl t $ TA_Expansion id-    delve (TA_Variable id str t) = dl t $ TA_Variable id str-    delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns-    delve (T_CoProc id var body) = d1 body $ T_CoProc id var-    delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id-    delve (T_Include id script) = d1 script $ T_Include id-    delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id-    delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id-    delve t = return t+        i (OuterToken id newIt)  getId :: Token -> Id-getId t = case t of-        T_AND_IF id  -> id-        T_OR_IF id  -> id-        T_DSEMI id  -> id-        T_Semi id  -> id-        T_DLESS id  -> id-        T_DGREAT id  -> id-        T_LESSAND id  -> id-        T_GREATAND id  -> id-        T_LESSGREAT id  -> id-        T_DLESSDASH id  -> id-        T_CLOBBER id  -> id-        T_If id  -> id-        T_Then id  -> id-        T_Else id  -> id-        T_Elif id  -> id-        T_Fi id  -> id-        T_Do id  -> id-        T_Done id  -> id-        T_Case id  -> id-        T_Esac id  -> id-        T_While id  -> id-        T_Until id  -> id-        T_For id  -> id-        T_Select id  -> id-        T_Lbrace id  -> id-        T_Rbrace id  -> id-        T_Lparen id  -> id-        T_Rparen id  -> id-        T_Bang id  -> id-        T_In  id  -> id-        T_NEWLINE id  -> id-        T_EOF id  -> id-        T_Less id  -> id-        T_Greater id  -> id-        T_SingleQuoted id _  -> id-        T_Literal id _  -> id-        T_NormalWord id _  -> id-        T_DoubleQuoted id _  -> id-        T_DollarExpansion id _  -> id-        T_DollarBraced id _ _ -> id-        T_DollarArithmetic id _  -> id-        T_BraceExpansion id _  -> id-        T_ParamSubSpecialChar id _ -> id-        T_DollarBraceCommandExpansion id _  -> id-        T_IoFile id _ _  -> id-        T_IoDuplicate id _ _  -> id-        T_HereDoc id _ _ _ _ -> id-        T_HereString id _  -> id-        T_FdRedirect id _ _  -> id-        T_Assignment id _ _ _ _  -> id-        T_Array id _  -> id-        T_IndexedElement id _ _  -> id-        T_Redirecting id _ _  -> id-        T_SimpleCommand id _ _  -> id-        T_Pipeline id _ _  -> id-        T_Banged id _  -> id-        T_AndIf id _ _ -> id-        T_OrIf id _ _ -> id-        T_Backgrounded id _  -> id-        T_IfExpression id _ _  -> id-        T_Subshell id _  -> id-        T_BraceGroup id _  -> id-        T_WhileExpression id _ _  -> id-        T_UntilExpression id _ _  -> id-        T_ForIn id _ _ _  -> id-        T_SelectIn id _ _ _  -> id-        T_CaseExpression id _ _ -> id-        T_Function id _ _ _ _  -> id-        T_Arithmetic id _  -> id-        T_Script id _ _  -> id-        T_Condition id _ _  -> id-        T_Extglob id _ _ -> id-        T_Backticked id _ -> id-        TC_And id _ _ _ _  -> id-        TC_Or id _ _ _ _  -> id-        TC_Group id _ _  -> id-        TC_Binary id _ _ _ _  -> id-        TC_Unary id _ _ _  -> id-        TC_Nullary id _ _  -> id-        TA_Binary id _ _ _  -> id-        TA_Assignment id _ _ _  -> id-        TA_Unary id _ _  -> id-        TA_Sequence id _  -> id-        TA_Trinary id _ _ _  -> id-        TA_Expansion id _  -> id-        T_ProcSub id _ _ -> id-        T_Glob id _ -> id-        T_ForArithmetic id _ _ _ _ -> id-        T_DollarSingleQuoted id _ -> id-        T_DollarDoubleQuoted id _ -> id-        T_DollarBracket id _ -> id-        T_Annotation id _ _ -> id-        T_Pipe id _ -> id-        T_CoProc id _ _ -> id-        T_CoProcBody id _ -> id-        T_Include id _ -> id-        T_SourceCommand id _ _ -> id-        T_UnparsedIndex id _ _ -> id-        TC_Empty id _ -> id-        TA_Variable id _ _ -> id-        T_BatsTest id _ _ -> id+getId (OuterToken id _) = id  blank :: Monad m => Token -> m () blank = const $ return ()
src/ShellCheck/ASTLib.hs view
@@ -25,6 +25,7 @@ import Control.Monad import Data.Char import Data.Functor+import Data.Functor.Identity import Data.List import Data.Maybe @@ -46,6 +47,7 @@     T_BraceExpansion {} -> True     T_Glob {} -> True     T_Extglob {} -> True+    T_DoubleQuoted _ l -> any willBecomeMultipleArgs l     T_NormalWord _ l -> any willSplit l     _ -> False @@ -175,9 +177,13 @@ getLiteralString :: Token -> Maybe String getLiteralString = getLiteralStringExt (const Nothing) +-- Definitely get a literal string, with a given default for all non-literals+getLiteralStringDef :: String -> Token -> String+getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)+ -- Definitely get a literal string, skipping over all non-literals onlyLiteralString :: Token -> String-onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")+onlyLiteralString = getLiteralStringDef ""  -- Maybe get a literal string, but only if it's an unquoted argument. getUnquotedLiteral (T_NormalWord _ list) =@@ -216,7 +222,7 @@  -- Maybe get the literal value of a token, using a custom function -- to map unrecognized Tokens into strings.-getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String+getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String getLiteralStringExt more = g   where     allInList = fmap concat . mapM g@@ -297,6 +303,11 @@ getCommandName :: Token -> Maybe String getCommandName = fst . getCommandNameAndToken +-- Maybe get the name+arguments of a command.+getCommandArgv t = do+    (T_SimpleCommand _ _ args@(_:_)) <- getCommand t+    return args+ -- 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.@@ -362,19 +373,23 @@ isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False  -- Get the lists of commands from tokens that contain them, such as--- the body of while loops or branches of if statements.+-- the conditions and bodies of while loops or branches of if statements. getCommandSequences :: Token -> [[Token]] getCommandSequences t =     case t of         T_Script _ _ cmds -> [cmds]         T_BraceGroup _ cmds -> [cmds]         T_Subshell _ cmds -> [cmds]-        T_WhileExpression _ _ cmds -> [cmds]-        T_UntilExpression _ _ cmds -> [cmds]+        T_WhileExpression _ cond cmds -> [cond, cmds]+        T_UntilExpression _ cond cmds -> [cond, cmds]         T_ForIn _ _ _ cmds -> [cmds]         T_ForArithmetic _ _ _ _ cmds -> [cmds]-        T_IfExpression _ thens elses -> map snd thens ++ [elses]+        T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]         T_Annotation _ _ t -> getCommandSequences t++        T_DollarExpansion _ cmds -> [cmds]+        T_DollarBraceCommandExpansion _ cmds -> [cmds]+        T_Backticked _ cmds -> [cmds]         _ -> []  -- Get a list of names of associative arrays@@ -382,7 +397,7 @@     nub . execWriter $ doAnalysis f t   where     f :: Token -> Writer [String] ()-    f t@T_SimpleCommand {} = fromMaybe (return ()) $ do+    f t@T_SimpleCommand {} = sequence_ $ do         name <- getCommandName t         let assocNames = ["declare","local","typeset"]         guard $ elem name assocNames
src/ShellCheck/Analytics.hs view
@@ -33,7 +33,7 @@ import Control.Monad import Control.Monad.Identity import Control.Monad.State-import Control.Monad.Writer+import Control.Monad.Writer hiding ((<>)) import Control.Monad.Reader import Data.Char import Data.Functor@@ -41,6 +41,7 @@ import Data.List import Data.Maybe import Data.Ord+import Data.Semigroup import Debug.Trace import qualified Data.Map.Strict as Map import Test.QuickCheck.All (forAllProperties)@@ -86,13 +87,8 @@  getEnableDirectives root =     case root of-        T_Annotation _ list _ -> mapMaybe getEnable list+        T_Annotation _ list _ -> [s | EnableComment s <- list]         _ -> []-  where-    getEnable t =-        case t of-            EnableComment s -> return s-            _ -> Nothing  checkList l t = concatMap (\f -> f t) l @@ -125,6 +121,7 @@     ,checkArithmeticDeref     ,checkArithmeticBadOctal     ,checkComparisonAgainstGlob+    ,checkCaseAgainstGlob     ,checkCommarrays     ,checkOrNeq     ,checkEchoWc@@ -191,6 +188,8 @@     ,checkRedirectionToCommand     ,checkDollarQuoteParen     ,checkUselessBang+    ,checkTranslatedStringVariable+    ,checkModifiedArithmeticInRedirection     ]  optionalChecks = map fst optionalTreeChecks@@ -259,12 +258,12 @@ verifyNotTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool verifyNotTree f s = producesComments f s == Just False -checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =-    when (t `isCommand` str) $ f cmd rest+checkCommand str f t@(T_SimpleCommand id _ (cmd:rest))+    | t `isCommand` str = f cmd rest checkCommand _ _ _ = return () -checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =-    when (t `isUnqualifiedCommand` str) $ f cmd rest+checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest))+    | t `isUnqualifiedCommand` str = f cmd rest checkUnqualifiedCommand _ _ _ = return ()  @@ -406,7 +405,7 @@ prop_checkArithmeticOpCommand2 = verify checkArithmeticOpCommand "foo=bar * 2" prop_checkArithmeticOpCommand3 = verifyNot checkArithmeticOpCommand "foo + opts" checkArithmeticOpCommand _ (T_SimpleCommand id [T_Assignment {}] (firstWord:_)) =-    fromMaybe (return ()) $ check <$> getGlobOrLiteralString firstWord+    mapM_ check $ getGlobOrLiteralString firstWord   where     check op =         when (op `elem` ["+", "-", "*", "/"]) $@@ -417,7 +416,7 @@ prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1" prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2" checkWrongArithmeticAssignment params (T_SimpleCommand id (T_Assignment _ _ _ _ val:[]) []) =-  fromMaybe (return ()) $ do+  sequence_ $ do     str <- getNormalString val     match <- matchRegex regex str     var <- match !!! 0@@ -433,7 +432,7 @@     insertRef _ = Prelude.id      getNormalString (T_NormalWord _ words) = do-        parts <- foldl (liftM2 (\x y -> x ++ [y])) (Just []) $ map getLiterals words+        parts <- mapM getLiterals words         return $ concat parts     getNormalString _ = Nothing @@ -452,7 +451,7 @@ checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) =     checkCommand "cat" (const f) cmd   where-    f [word] = unless (mayBecomeMultipleArgs word || isOption word) $+    f [word] | not (mayBecomeMultipleArgs word || isOption word) =         style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead."     f _ = return ()     isOption word = "-" `isPrefixOf` onlyLiteralString word@@ -490,8 +489,8 @@      for ["grep", "wc"] $         \(grep:wc:_) ->-            let flagsGrep = fromMaybe [] $ map snd . getAllFlags <$> getCommand grep-                flagsWc = fromMaybe [] $ map snd . getAllFlags <$> getCommand wc+            let flagsGrep = maybe [] (map snd . getAllFlags) $ getCommand grep+                flagsWc = maybe [] (map snd . getAllFlags) $ getCommand wc             in                 unless (any (`elem` ["o", "only-matching", "r", "R", "recursive"]) flagsGrep || any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc || null flagsWc) $                     style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l."@@ -502,11 +501,10 @@         for' ["ls", "xargs"] $             \x -> warn x 2011 "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames."         ]-    unless didLs $ do+    unless didLs $ void $         for ["ls", "?"] $             \(ls:_) -> unless (hasShortParameter 'N' (oversimplify ls)) $                 info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames."-        return ()   where     for l f =         let indices = indexOfSublists l (map (headOrDefault "" . oversimplify) commands)@@ -560,17 +558,15 @@     isOverride _ = False checkShebang params (T_Script _ (T_Literal 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 (null sb) $+            err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive."         when (executableFromShebang sb == "ash") $             warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."     unless (null sb) $ do         unless ("/" `isPrefixOf` sb) $             err id 2239 "Ensure the shebang uses an absolute path to the interpreter."-        case words sb of-            first:_ ->-                when ("/" `isSuffixOf` first) $-                    err id 2246 "This shebang specifies a directory. Ensure the interpreter is a file."+        when ("/" `isSuffixOf` head (words sb)) $+            err id 2246 "This shebang specifies a directory. Ensure the interpreter is a file."   prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"@@ -582,17 +578,29 @@ prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done" prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done" prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done"-checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =-    when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list-            || (fmap wouldHaveBeenGlob (getLiteralString word) == Just True)) $+prop_checkForInQuoted7 = verify checkForInQuoted "for f in ls, grep, mv; do true; done"+prop_checkForInQuoted8 = verify checkForInQuoted "for f in 'ls', 'grep', 'mv'; do true; done"+prop_checkForInQuoted9 = verifyNot checkForInQuoted "for f in 'ls,' 'grep,' 'mv'; do true; done"+checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _)+    | any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list+            || maybe False wouldHaveBeenGlob (getLiteralString word) =         err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once." checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) =     warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . "-checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) =-    if ',' `elem` s-      then unless ('{' `elem` s) $-            warn id 2042 "Use spaces, not commas, to separate loop elements."-      else warn id 2043 "This loop will only ever run once for a constant value. Did you perhaps mean to loop over dir/*, $var or $(cmd)?"+checkForInQuoted _ (T_ForIn _ _ [single] _)+    | maybe False (',' `elem`) $ getUnquotedLiteral single =+        warn (getId single) 2042 "Use spaces, not commas, to separate loop elements."+    | not (willSplit single || mayBecomeMultipleArgs single) =+        warn (getId single) 2043 "This loop will only ever run once. Bad quoting or missing glob/expansion?"+checkForInQuoted params (T_ForIn _ _ multiple _) =+    forM_ multiple $ \arg -> sequence_ $ do+        suffix <- getTrailingUnquotedLiteral arg+        string <- getLiteralString suffix+        guard $ "," `isSuffixOf` string+        return $+            warnWithFix (getId arg) 2258+                "The trailing comma is part of the value, not a separator. Delete or quote it."+                (fixWith [replaceEnd (getId suffix) params 1 ""]) checkForInQuoted _ _ = return ()  prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"@@ -710,13 +718,13 @@   where     note x = makeComment InfoC x 2094                 "Make sure not to read and write the same file in the same pipeline."-    checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) =-        when (exceptId /= newId+    checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) |+        exceptId /= newId                 && x == y                 && not (isOutput t && isOutput u)                 && not (special t)                 && not (any isHarmlessCommand [t,u])-                && not (any containsAssignment [u])) $ do+                && not (any containsAssignment [u]) = do             addComment $ note newId             addComment $ note exceptId     checkOccurrences _ _ = return ()@@ -774,9 +782,9 @@ prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*" prop_checkDollarStar3 = verifyNot checkDollarStar "[[ $* = 'a b' ]]" checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _ _)])-      | bracedString b == "*"  =-    unless (isStrictlyQuoteFree (parentMap p) t) $-        warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."+      | bracedString b == "*" &&+        not (isStrictlyQuoteFree (parentMap p) t) =+            warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems." checkDollarStar _ _ = return ()  @@ -792,7 +800,7 @@ prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}" prop_checkUnquotedDollarAt10 = verifyNot checkUnquotedDollarAt "echo ${@+\"$@\"}" checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word =-    forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->+    forM_ (find isArrayExpansion parts) $ \x ->         unless (isQuotedAlternativeReference x) $             err (getId x) 2068                 "Double quote array expansions to avoid re-splitting elements."@@ -804,12 +812,12 @@ prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@" prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\"" checkConcatenatedDollarAt p word@T_NormalWord {}-    | not $ isQuoteFree (parentMap p) word =-        unless (null $ drop 1 parts) $-            mapM_ for array+    | not $ isQuoteFree (parentMap p) word+    || null (drop 1 parts) =+        mapM_ for array   where     parts = getWordParts word-    array = take 1 $ filter isArrayExpansion parts+    array = find isArrayExpansion parts     for t = err (getId t) 2145 "Argument mixes string and array. Use * or separate argument." checkConcatenatedDollarAt _ _ = return () @@ -855,7 +863,7 @@     readF _ _ _ = return []      writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do-        isArray <- gets (isJust . Map.lookup name)+        isArray <- gets (Map.member name)         return $ if not isArray then [] else             case mode of                 Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."]@@ -931,7 +939,12 @@ prop_checkSingleQuotedVariables17= verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *" prop_checkSingleQuotedVariables18= verifyNot checkSingleQuotedVariables "echo '``'" prop_checkSingleQuotedVariables19= verifyNot checkSingleQuotedVariables "echo '```'"+prop_checkSingleQuotedVariables20= verifyNot checkSingleQuotedVariables "mumps -run %XCMD 'W $O(^GLOBAL(5))'"+prop_checkSingleQuotedVariables21= verifyNot checkSingleQuotedVariables "mumps -run LOOP%XCMD --xec 'W $O(^GLOBAL(6))'" +++ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =     when (s `matches` re) $         if "sed" == commandName@@ -944,7 +957,7 @@     commandName = fromMaybe "" $ do         cmd <- getClosestCommand parents t         name <- getCommandBasename cmd-        return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else name+        return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else if name == "mumps" then getMumpsCommand cmd else name      isProbablyOk =             any isOkAssignment (take 3 $ getPath parents t)@@ -965,6 +978,8 @@                 ,"rename"                 ,"unset"                 ,"git filter-branch"+                ,"mumps -run %XCMD"+                ,"mumps -run LOOP%XCMD"                 ]             || "awk" `isSuffixOf` commandName             || "perl" `isPrefixOf` commandName@@ -994,6 +1009,13 @@             _ -> "git"     getGitCommand (T_Redirecting _ _ cmd) = getGitCommand cmd     getGitCommand _ = "git"+    getMumpsCommand (T_SimpleCommand _ _ words) =+        case map getLiteralString words of+            Just "mumps":Just "-run":Just "%XCMD":_ -> "mumps -run %XCMD"+            Just "mumps":Just "-run":Just "LOOP%XCMD":_ -> "mumps -run LOOP%XCMD"+            _ -> "mumps"+    getMumpsCommand (T_Redirecting _ _ cmd) = getMumpsCommand cmd+    getMumpsCommand _ = "mumps" checkSingleQuotedVariables _ _ = return ()  @@ -1002,8 +1024,9 @@ prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow" prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]" prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]"-checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =-       err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."+checkUnquotedN _ (TC_Unary _ SingleBracket "-n" t) | willSplit t =+    unless (any isArrayExpansion $ getWordParts t) $ -- There's SC2198 for these+       err (getId t) 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]." checkUnquotedN _ _ = return ()  prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"@@ -1020,6 +1043,7 @@ prop_checkNumberComparisons13 = verify checkNumberComparisons "[ $foo > $bar ]" prop_checkNumberComparisons14 = verifyNot checkNumberComparisons "[[ foo < bar ]]" prop_checkNumberComparisons15 = verifyNot checkNumberComparisons "[ $foo '>' $bar ]"+prop_checkNumberComparisons16 = verify checkNumberComparisons "[ foo -eq 'y' ]" checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do     if isNum lhs || isNum rhs       then do@@ -1043,7 +1067,7 @@                 Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting."                 _ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])." -    when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do+    when (op `elem` arithmeticBinaryTestOps) $ do         mapM_ checkDecimals [lhs, rhs]         when (typ == SingleBracket) $             checkStrings [lhs, rhs]@@ -1060,11 +1084,9 @@         "Either use integers only, or use bc or awk to compare."        checkStrings =-        mapM_ stringError . take 1 . filter isNonNum+        mapM_ stringError . find isNonNum -      isNonNum t = fromMaybe False $ do-        s <- getLiteralStringExt (const $ return "") t-        return . not . all numChar $ s+      isNonNum t = not . all numChar $ onlyLiteralString t       numChar x = isDigit x || x `elem` "+-. "        stringError t = err (getId t) 2170 $@@ -1103,8 +1125,8 @@ checkNumberComparisons _ _ = return ()  prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]"-checkSingleBracketOperators params (TC_Binary id SingleBracket "=~" lhs rhs) =-    when (shellType params `elem` [Bash, Ksh]) $+checkSingleBracketOperators params (TC_Binary id SingleBracket "=~" lhs rhs)+    | shellType params `elem` [Bash, Ksh] =         err id 2074 $ "Can't use =~ in [ ]. Use [[..]] instead." checkSingleBracketOperators _ _ = return () @@ -1169,10 +1191,10 @@ 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, but this looks like a glob. Use = instead."+checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs)+    | isConfusedGlobRegex s =+        warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."+    where s = concat $ oversimplify rhs checkGlobbedRegex _ _ = return ()  @@ -1192,7 +1214,7 @@         else checkUnmatchable id op lhs rhs   where     isDynamic =-        op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]+        op `elem` arithmeticBinaryTestOps             && typ == DoubleBracket         || op `elem` [ "-nt", "-ot", "-ef"] @@ -1210,7 +1232,7 @@ prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]" prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]" prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"-checkLiteralBreakingTest _ t = potentially $+checkLiteralBreakingTest _ t = sequence_ $         case t of             (TC_Nullary _ _ w@(T_NormalWord _ l)) -> do                 guard . not $ isConstant w -- Covered by SC2078@@ -1224,16 +1246,13 @@   where     hasEquals = matchToken ('=' `elem`)     isNonEmpty = matchToken (not . null)-    matchToken m t = isJust $ do-        str <- getLiteralString t-        guard $ m str-        return ()+    matchToken m t = maybe False m (getLiteralString t)      comparisonWarning list = do-        token <- listToMaybe $ filter hasEquals list+        token <- find hasEquals list         return $ err (getId token) 2077 "You need spaces around the comparison operator."     tautologyWarning t s = do-        token <- listToMaybe $ filter isNonEmpty $ getWordParts t+        token <- find isNonEmpty $ getWordParts t         return $ err (getId token) 2157 s  prop_checkConstantNullary = verify checkConstantNullary "[[ '$(foo)' ]]"@@ -1258,7 +1277,7 @@ prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"-checkForDecimals params t@(TA_Expansion id _) = potentially $ do+checkForDecimals params t@(TA_Expansion id _) = sequence_ $ do     guard $ not (hasFloatingPoint params)     str <- getLiteralString t     first <- str !!! 0@@ -1294,7 +1313,7 @@     unless (isException $ bracedString b) getWarning   where     isException [] = True-    isException s = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit (head s)+    isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h     getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t     warningFor t =         case t of@@ -1311,7 +1330,7 @@ prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))" prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))" prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))"-checkArithmeticBadOctal _ t@(TA_Expansion id _) = potentially $ do+checkArithmeticBadOctal _ t@(TA_Expansion id _) = sequence_ $ do     str <- getLiteralString t     guard $ str `matches` octalRE     return $ err id 2080 "Numbers with leading 0 are considered octal."@@ -1338,6 +1357,21 @@  checkComparisonAgainstGlob _ _ = return () +prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac"+prop_checkCaseAgainstGlob2 = verify checkCaseAgainstGlob "case foo in $(foo)) foo;; esac"+prop_checkCaseAgainstGlob3 = verifyNot checkCaseAgainstGlob "case foo in *$bar*) foo;; esac"+checkCaseAgainstGlob _ t =+    case t of+        (T_CaseExpression _ _ cases) -> mapM_ check cases+        _ -> return ()+  where+    check (_, list, _) = mapM_ check' list+    check' expr@(T_NormalWord _ list)+        -- If it's already a glob, assume that's what the user wanted+        | not (isGlob expr) && any isQuoteableExpansion list =+            warn (getId expr) 2254 "Quote expansions in case patterns to match literally rather than as a glob."+    check' _ = return ()+ prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)" prop_checkCommarrays2 = verify checkCommarrays "a+=(1,2,3)" prop_checkCommarrays3 = verifyNot checkCommarrays "cow=(1 \"foo,bar\" 3)"@@ -1365,6 +1399,7 @@ prop_checkOrNeq6 = verify checkOrNeq "[ $a != a ] || [ $a != b ]" prop_checkOrNeq7 = verify checkOrNeq "[ $a != a ] || [ $a != b ] || true" prop_checkOrNeq8 = verifyNot checkOrNeq "[[ $a != x || $a != x ]]"+prop_checkOrNeq9 = verifyNot checkOrNeq "[ 0 -ne $FOO ] || [ 0 -ne $BAR ]" -- This only catches the most idiomatic cases. Fixme?  -- For test-level "or": [ x != y -o x != z ]@@ -1378,7 +1413,7 @@         warn id 2056 "You probably wanted && here, otherwise it's always true."  -- For command level "or": [ x != y ] || [ x != z ]-checkOrNeq _ (T_OrIf id lhs rhs) = potentially $ do+checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do     (lhs1, op1, rhs1) <- getExpr lhs     (lhs2, op2, rhs2) <- getExpr rhs     guard $ op1 == op2 && op1 `elem` ["-ne", "!="]@@ -1392,9 +1427,17 @@             T_Pipeline _ _ [x] -> getExpr x             T_Redirecting _ _ c -> getExpr c             T_Condition _ _ c -> getExpr c-            TC_Binary _ _ op lhs rhs -> return (lhs, op, rhs)-            _ -> fail ""+            TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs)+            _ -> Nothing +    -- Swap items so that the constant side is rhs (or Nothing if both/neither is constant)+    orient (lhs, op, rhs) =+        case (isConstant lhs, isConstant rhs) of+            (True, False) -> return (rhs, op, lhs)+            (False, True) -> return (lhs, op, rhs)+            _ -> Nothing++ checkOrNeq _ _ = return ()  @@ -1504,8 +1547,8 @@ prop_checkIndirectExpansion3 = verify checkIndirectExpansion "${$#}" prop_checkIndirectExpansion4 = verify checkIndirectExpansion "${var${n}_$((i%2))}" prop_checkIndirectExpansion5 = verifyNot checkIndirectExpansion "${bar}"-checkIndirectExpansion _ (T_DollarBraced i _ (T_NormalWord _ contents)) =-    when (isIndirection contents) $+checkIndirectExpansion _ (T_DollarBraced i _ (T_NormalWord _ contents))+    | isIndirection contents =         err i 2082 "To expand via indirection, use arrays, ${!name} or (for sh only) eval."   where     isIndirection vars =@@ -1542,8 +1585,8 @@         case trapped of             T_DollarExpansion id _ -> warnAboutExpansion id             T_DollarBraced id _ _ -> warnAboutExpansion id-            T_Literal id s ->-                unless (quotesSingleThing a && quotesSingleThing b || isRegex (getPath (parentMap params) trapped)) $+            T_Literal id s+                | not (quotesSingleThing a && quotesSingleThing b || isRegex (getPath (parentMap params) trapped)) ->                     warnAboutLiteral id             _ -> return () @@ -1625,9 +1668,9 @@     doList = doList' . stripCleanup     -- The second parameter is True if we are in a loop     -- In that case we should emit the warning also if `exec' is the last statement-    doList' t@(current:following:_) False = do+    doList' (current:t@(following:_)) False = do         commentIfExec current-        doList (tail t) False+        doList t False     doList' (current:tail) True = do         commentIfExec current         doList tail True@@ -1636,8 +1679,8 @@     commentIfExec (T_Pipeline id _ list) =       mapM_ commentIfExec $ take 1 list     commentIfExec (T_Redirecting _ _ f@(-      T_SimpleCommand id _ (cmd:arg:_))) =-        when (f `isUnqualifiedCommand` "exec") $+      T_SimpleCommand id _ (cmd:arg:_)))+        | f `isUnqualifiedCommand` "exec" =           warn id 2093             "Remove \"exec \" if script should continue after this command."     commentIfExec _ = return ()@@ -1790,14 +1833,28 @@ prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg" prop_checkSpacefulness37= verifyNotTree checkSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}" prop_checkSpacefulness37v = verifyTree checkVerboseSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}"+prop_checkSpacefulness38= verifyTree checkSpacefulness "a=; echo $a"+prop_checkSpacefulness39= verifyNotTree checkSpacefulness "a=''\"\"''; b=x$a; echo $b"+prop_checkSpacefulness40= verifyNotTree checkSpacefulness "a=$((x+1)); echo $a" +data SpaceStatus = SpaceSome | SpaceNone | SpaceEmpty deriving (Eq)+instance Semigroup SpaceStatus where+    SpaceNone <> SpaceNone = SpaceNone+    SpaceSome <> _ = SpaceSome+    _ <> SpaceSome = SpaceSome+    SpaceEmpty <> x = x+    x <> SpaceEmpty = x+instance Monoid SpaceStatus where+    mempty = SpaceEmpty+    mappend = (<>)+ -- This is slightly awkward because we want to support structured -- optional checks based on nearly the same logic checkSpacefulness params = checkSpacefulness' onFind params   where     emit x = tell [x]     onFind spaces token _ =-        when spaces $+        when (spaces /= SpaceNone) $             if isDefaultAssignment (parentMap params) token             then                 emit $ makeComment InfoC (getId token) 2223@@ -1812,7 +1869,6 @@             any (`isPrefixOf` modifier) ["=", ":="]             && isParamTo parents ":" token - prop_checkSpacefulness4v= verifyTree checkVerboseSpacefulness "foo=3; foo=$(echo $foo)" prop_checkSpacefulness8v= verifyTree checkVerboseSpacefulness "a=foo\\ bar; a=foo; rm $a" prop_checkSpacefulness28v = verifyTree checkVerboseSpacefulness "exec {n}>&1; echo $n"@@ -1820,24 +1876,24 @@ checkVerboseSpacefulness params = checkSpacefulness' onFind params   where     onFind spaces token name =-        when (not spaces && name `notElem` specialVariablesWithoutSpaces) $+        when (spaces == SpaceNone && name `notElem` specialVariablesWithoutSpaces) $             tell [makeCommentWithFix StyleC (getId token) 2248                     "Prefer double quoting even when variables don't contain special characters."                     (addDoubleQuotesAround params token)]  addDoubleQuotesAround params token = (surroundWidth (getId token) params "\"") checkSpacefulness'-    :: (Bool -> Token -> String -> Writer [TokenComment] ()) ->+    :: (SpaceStatus -> Token -> String -> Writer [TokenComment] ()) ->             Parameters -> Token -> [TokenComment] checkSpacefulness' onFind params t =     doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)   where-    defaults = zip variablesWithoutSpaces (repeat False)+    defaults = zip variablesWithoutSpaces (repeat SpaceNone) -    hasSpaces name = gets (Map.findWithDefault True name)+    hasSpaces name = gets (Map.findWithDefault SpaceSome name) -    setSpaces name bool =-        modify $ Map.insert name bool+    setSpaces name status =+        modify $ Map.insert name status      readF _ token name = do         spaces <- hasSpaces name@@ -1854,13 +1910,13 @@       where         emit x = tell [x] -    writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []-    writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []+    writeF _ _ name (DataString SourceExternal) = setSpaces name SpaceSome >> return []+    writeF _ _ name (DataString SourceInteger) = setSpaces name SpaceNone >> return []      writeF _ _ name (DataString (SourceFrom vals)) = do         map <- get         setSpaces name-            (isSpacefulWord (\x -> Map.findWithDefault True x map) vals)+            (isSpacefulWord (\x -> Map.findWithDefault SpaceSome x map) vals)         return []      writeF _ _ _ _ = return []@@ -1872,24 +1928,28 @@             (T_DollarBraced _ _ _ ) -> True             _ -> False -    isSpacefulWord :: (String -> Bool) -> [Token] -> Bool-    isSpacefulWord f = any (isSpaceful f)-    isSpaceful :: (String -> Bool) -> Token -> Bool+    isSpacefulWord :: (String -> SpaceStatus) -> [Token] -> SpaceStatus+    isSpacefulWord f = mconcat . map (isSpaceful f)+    isSpaceful :: (String -> SpaceStatus) -> Token -> SpaceStatus     isSpaceful spacefulF x =         case x of-          T_DollarExpansion _ _ -> True-          T_Backticked _ _ -> True-          T_Glob _ _         -> True-          T_Extglob {}       -> True-          T_Literal _ s      -> s `containsAny` globspace-          T_SingleQuoted _ s -> s `containsAny` globspace+          T_DollarExpansion _ _ -> SpaceSome+          T_Backticked _ _ -> SpaceSome+          T_Glob _ _         -> SpaceSome+          T_Extglob {}       -> SpaceSome+          T_DollarArithmetic _ _ -> SpaceNone+          T_Literal _ s      -> fromLiteral s+          T_SingleQuoted _ s -> fromLiteral s           T_DollarBraced _ _ _ -> spacefulF $ getBracedReference $ bracedString x           T_NormalWord _ w   -> isSpacefulWord spacefulF w           T_DoubleQuoted _ w -> isSpacefulWord spacefulF w-          _ -> False+          _ -> SpaceEmpty       where         globspace = "*?[] \t\n"         containsAny s = any (`elem` s)+        fromLiteral "" = SpaceEmpty+        fromLiteral s | s `containsAny` globspace = SpaceSome+        fromLiteral _ = SpaceNone  prop_CheckVariableBraces1 = verify checkVariableBraces "a='123'; echo $a" prop_CheckVariableBraces2 = verifyNot checkVariableBraces "a='123'; echo ${a}"@@ -1897,8 +1957,8 @@ prop_CheckVariableBraces4 = verifyNot checkVariableBraces "echo $* $1" checkVariableBraces params t =     case t of-        T_DollarBraced id False _ ->-            unless (name `elem` unbracedVariables) $+        T_DollarBraced id False _+            | name `notElem` unbracedVariables ->                 styleWithFix id 2250                     "Prefer putting braces around variable references even when not strictly required."                     (fixFor t)@@ -1923,7 +1983,7 @@ checkQuotesInLiterals params t =     doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)   where-    getQuotes name = fmap (Map.lookup name) get+    getQuotes name = gets (Map.lookup name)     setQuotes name ref = modify $ Map.insert name ref     deleteQuotes = modify . Map.delete     parents = parentMap params@@ -1958,18 +2018,18 @@      readF _ expr name = do         assignment <- getQuotes name-        return-          (if isJust assignment-              && not (isParamTo parents "eval" expr)+        return $ case assignment of+          Just j+              | not (isParamTo parents "eval" expr)               && not (isQuoteFree parents expr)               && not (squashesQuotes expr)-              then [-                  makeComment WarningC (fromJust assignment) 2089 $+              -> [+                  makeComment WarningC j 2089 $                       "Quotes/backslashes will be treated literally. " ++ suggestion,                   makeComment WarningC (getId expr) 2090                       "Quotes/backslashes in this variable will not be respected."                 ]-              else [])+          _ -> []     suggestion =         if supportsArrays (shellType params)         then "Use an array."@@ -2035,7 +2095,7 @@         in when ('=' `elem` string) $             modify ((takeWhile (/= '=') string, getId arg):) -    checkArg cmd (_, arg) = potentially $ do+    checkArg cmd (_, arg) = sequence_ $ do         literalArg <- getUnquotedLiteral arg  -- only consider unquoted literals         definitionId <- Map.lookup literalArg functions         return $ do@@ -2091,6 +2151,7 @@ prop_checkUnused44= verifyNotTree checkUnusedAssignments "DEFINE_string \"foo$ibar\" x y" prop_checkUnused45= verifyTree checkUnusedAssignments "readonly foo=bar" prop_checkUnused46= verifyTree checkUnusedAssignments "readonly foo=(bar)"+prop_checkUnused47= verifyNotTree checkUnusedAssignments "a=1; alias hello='echo $a'" checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)   where     flow = variableFlow params@@ -2151,6 +2212,8 @@ prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\"" prop_checkUnassignedReferences37= verifyNotTree checkUnassignedReferences "var=howdy; printf -v 'array[0]' %s \"$var\"; printf %s \"${array[0]}\";" prop_checkUnassignedReferences38= verifyTree (checkUnassignedReferences' True) "echo $VAR"+prop_checkUnassignedReferences39= verifyNotTree checkUnassignedReferences "builtin export var=4; echo $var"+prop_checkUnassignedReferences40= verifyNotTree checkUnassignedReferences ": ${foo=bar}"  checkUnassignedReferences = checkUnassignedReferences' False checkUnassignedReferences' includeGlobals params t = warnings@@ -2197,14 +2260,14 @@                     match <- getBestMatch var                     return $ " (did you mean '" ++ match ++ "'?)" -    warningFor var place = do+    warningFor (var, place) = do         guard $ isVariableName var         guard . not $ isInArray var place || isGuarded place         (if includeGlobals || isLocal var          then warningForLocals          else warningForGlobals) var place -    warnings = execWriter . sequence $ mapMaybe (uncurry warningFor) unassigned+    warnings = execWriter . sequence $ mapMaybe warningFor unassigned      -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.     -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.@@ -2258,43 +2321,79 @@ prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo" prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file" prop_checkWhileReadPitfalls8 = verifyNot checkWhileReadPitfalls "while read foo; do ssh -n $foo uptime; done < file"+prop_checkWhileReadPitfalls9 = verify checkWhileReadPitfalls "while read foo; do ffmpeg -i foo.mkv bar.mkv -an; done"+prop_checkWhileReadPitfalls10 = verify checkWhileReadPitfalls "while read foo; do mplayer foo.ogv > file; done"+prop_checkWhileReadPitfalls11 = verifyNot checkWhileReadPitfalls "while read foo; do mplayer foo.ogv <<< q; done"+prop_checkWhileReadPitfalls12 = verifyNot checkWhileReadPitfalls "while read foo\ndo\nmplayer foo.ogv << EOF\nq\nEOF\ndone"+prop_checkWhileReadPitfalls13 = verify checkWhileReadPitfalls "while read foo; do x=$(ssh host cmd); done"+prop_checkWhileReadPitfalls14 = verify checkWhileReadPitfalls "while read foo; do echo $(ssh host cmd) < /dev/null; done" -checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)+checkWhileReadPitfalls params (T_WhileExpression id [command] contents)         | isStdinReadCommand command =     mapM_ checkMuncher contents   where-    munchers = [ "ssh", "ffmpeg", "mplayer", "HandBrakeCLI" ]-    preventionFlags = ["n", "noconsolecontrols" ]+    -- Map of munching commands to a function that checks if the flags should exclude it+    munchers = Map.fromList [+        ("ssh", (hasFlag, addFlag, "-n")),+        ("ffmpeg", (hasArgument, addFlag, "-nostdin")),+        ("mplayer", (hasArgument, addFlag, "-noconsolecontrols")),+        ("HandBrakeCLI", (\_ _ -> False, addRedirect, "< /dev/null"))+        ]+    -- Use flag parsing, e.g. "-an" -> "a", "n"+    hasFlag ('-':flag) = elem flag . map snd . getAllFlags+    -- Simple string match, e.g. "-an" -> "-an"+    hasArgument arg = elem arg . mapMaybe getLiteralString . fromJust . getCommandArgv+    addFlag string cmd = fixWith [replaceEnd (getId $ getCommandTokenOrThis cmd) params 0 (' ':string)]+    addRedirect string cmd = fixWith [replaceEnd (getId cmd) params 0 (' ':string)]      isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) =         let plaintext = oversimplify cmd-        in head (plaintext ++ [""]) == "read"+        in headOrDefault "" plaintext == "read"             && ("-u" `notElem` plaintext)-            && all (not . stdinRedirect) redirs+            && not (any stdinRedirect redirs)     isStdinReadCommand _ = False -    checkMuncher (T_Pipeline _ _ (T_Redirecting _ redirs cmd:_)) | not $ any stdinRedirect redirs =+    checkMuncher :: Token -> Writer [TokenComment] ()+    checkMuncher (T_Pipeline _ _ (T_Redirecting _ redirs cmd:_)) = do+        -- Check command substitutions regardless of the command         case cmd of-            (T_IfExpression _ thens elses) ->-              mapM_ checkMuncher . concat $ map fst thens ++ map snd thens ++ [elses]+            T_SimpleCommand _ vars args ->+                mapM_ checkMuncher $ concat $ concatMap getCommandSequences $ concatMap getWords $ vars ++ args+            _ -> return () -            _ -> potentially $ do+        unless (any stdinRedirect redirs) $ do+            -- Recurse into ifs/loops/groups/etc if this doesn't redirect+            mapM_ checkMuncher $ concat $ getCommandSequences cmd++            -- Check the actual command+            sequence_ $ do                 name <- getCommandBasename cmd-                guard $ name `elem` munchers+                (check, fix, flag) <- Map.lookup name munchers+                guard $ not (check flag cmd) -                -- Sloppily check if the command has a flag to prevent eating stdin.-                let flags = getAllFlags cmd-                guard . not $ any (`elem` preventionFlags) $ map snd flags                 return $ do                     info id 2095 $                         name ++ " may swallow stdin, preventing this loop from working properly."-                    warn (getId cmd) 2095 $-                        "Add < /dev/null to prevent " ++ name ++ " from swallowing stdin."+                    warnWithFix (getId cmd) 2095+                        ("Use " ++ name ++ " " ++ flag ++ " to prevent " ++ name ++ " from swallowing stdin.")+                        (fix flag cmd)     checkMuncher _ = return () -    stdinRedirect (T_FdRedirect _ fd _)-        | fd == "" || fd == "0" = True+    stdinRedirect (T_FdRedirect _ fd op)+        | fd == "0" = True+        | fd == "" =+            case op of+                T_IoFile _ (T_Less _) _ -> True+                T_IoDuplicate _ (T_LESSAND _) _ -> True+                T_HereString _ _ -> True+                T_HereDoc {} -> True+                _ -> False     stdinRedirect _ = False++    getWords t =+        case t of+            T_Assignment _ _ _ _ x -> getWordParts x+            _ -> getWordParts t checkWhileReadPitfalls _ _ = return ()  @@ -2325,6 +2424,7 @@ prop_checkCharRangeGlob3 = verify checkCharRangeGlob "ls [10-15]" prop_checkCharRangeGlob4 = verifyNot checkCharRangeGlob "ls [a-zA-Z]" prop_checkCharRangeGlob5 = verifyNot checkCharRangeGlob "tr -d [a-zA-Z]" -- tr has 2060+prop_checkCharRangeGlob6 = verifyNot checkCharRangeGlob "[[ $x == [!!]* ]]" checkCharRangeGlob p t@(T_Glob id str) |   isCharClass str && not (isParamTo (parentMap p) "tr" t) =     if ":" `isPrefixOf` contents@@ -2336,8 +2436,13 @@             info id 2102 "Ranges can only match single chars (mentioned due to duplicates)."   where     isCharClass str = "[" `isPrefixOf` str && "]" `isSuffixOf` str-    contents = drop 1 . take (length str - 1) $ str+    contents = dropNegation . drop 1 . take (length str - 1) $ str     hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents+    dropNegation s =+        case s of+            '!':rest -> rest+            '^':rest -> rest+            x -> x checkCharRangeGlob _ _ = return ()  @@ -2370,7 +2475,7 @@                 else findCdPair (b:rest)             _ -> Nothing -    doList list = potentially $ do+    doList list = sequence_ $ do         cd <- findCdPair $ mapMaybe getCandidate list         return $ info cd 2103 "Use a ( subshell ) to avoid having to cd back." @@ -2457,12 +2562,10 @@         map (\t@(T_Function _ _ _ name _) -> (name,t)) functions     functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root -    findFunction t@(T_Function id _ _ name body) =-        let flow = getVariableFlow params body-        in-          if any (isPositionalReference t) flow && not (any isPositionalAssignment flow)-            then return t-            else Nothing+    findFunction t@(T_Function id _ _ name body)+        | any (isPositionalReference t) flow && not (any isPositionalAssignment flow)+        = return t+        where flow = getVariableFlow params body     findFunction _ = Nothing      isPositionalAssignment x =@@ -2484,7 +2587,7 @@      referenceList :: [(String, Bool, Token)]     referenceList = execWriter $-        doAnalysis (fromMaybe (return ()) . checkCommand) root+        doAnalysis (sequence_ . checkCommand) root     checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())     checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do         str <- getLiteralString cmd@@ -2500,13 +2603,12 @@     updateWith x@(name, _, _) = Map.insertWith (++) name [x]      warnForGroup group =-        when (all isArgumentless group) $-            -- Allow ignoring SC2120 on the function to ignore all calls-            let (name, func) = getFunction group-                ignoring = shouldIgnoreCode params 2120 func-            in unless ignoring $ do-                mapM_ suggestParams group-                warnForDeclaration func name+        -- Allow ignoring SC2120 on the function to ignore all calls+        when (all isArgumentless group && not ignoring) $ do+            mapM_ suggestParams group+            warnForDeclaration func name+        where (name, func) = getFunction group+              ignoring = shouldIgnoreCode params 2120 func      suggestParams (name, _, thing) =         info (getId thing) 2119 $@@ -2516,7 +2618,7 @@             name ++ " references arguments, but none are ever passed."      getFunction ((name, _, _):_) =-        (name, fromJust $ Map.lookup name functionMap)+        (name, functionMap Map.! name)   prop_checkOverridingPath1 = verify checkOverridingPath "PATH=\"$var/$foo\""@@ -2530,11 +2632,11 @@ checkOverridingPath _ (T_SimpleCommand _ vars []) =     mapM_ checkVar vars   where-    checkVar (T_Assignment id Assign "PATH" [] word) =-        let string = concat $ oversimplify word-        in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do+    checkVar (T_Assignment id Assign "PATH" [] word)+        | not $ any (`isInfixOf` string) ["/bin", "/sbin" ] = do             when ('/' `elem` string && ':' `notElem` string) $ notify id             when (isLiteral word && ':' `notElem` string && '/' `notElem` string) $ notify id+        where string = concat $ oversimplify word     checkVar _ = return ()     notify id = warn id 2123 "PATH is the shell search path. Use another name." checkOverridingPath _ _ = return ()@@ -2545,12 +2647,12 @@ checkTildeInPath _ (T_SimpleCommand _ vars _) =     mapM_ checkVar vars   where-    checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts)) =-        when (any (\x -> isQuoted x && hasTilde x) parts) $+    checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts))+        | any (\x -> isQuoted x && hasTilde x) parts =             warn id 2147 "Literal tilde in PATH works poorly across programs."     checkVar _ = return () -    hasTilde t = fromMaybe False (liftM2 elem (return '~') (getLiteralStringExt (const $ return "") t))+    hasTilde t = '~' `elem` onlyLiteralString t     isQuoted T_DoubleQuoted {} = True     isQuoted T_SingleQuoted {} = True     isQuoted _ = False@@ -2560,13 +2662,13 @@ prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac" prop_checkUnsupported5 = verify checkUnsupported "#!/bin/bash\necho \"${ ls; }\"" checkUnsupported params t =-    when (not (null support) && (shellType params `notElem` support)) $+    unless (null support || (shellType params `elem` support)) $         report name  where     (name, support) = shellSupport t     report s = err (getId t) 2127 $         "To use " ++ s ++ ", specify #!/usr/bin/env " ++-            (map toLower . intercalate " or " . map show $ support)+            (intercalate " or " . map (map toLower . show) $ support)  -- TODO: Move more of these checks here shellSupport t =@@ -2590,8 +2692,8 @@   where     checkList list =         mapM_ checkGroup (groupWith (fmap fst) $ map getTarget list)-    checkGroup (f:_:_:_) | isJust f =-        style (snd $ fromJust f) 2129+    checkGroup (Just (_,id):_:_:_) =+        style id 2129             "Consider using { cmd1; cmd2; } >> file instead of individual redirects."     checkGroup _ = return ()     getTarget (T_Annotation _ _ t) = getTarget t@@ -2606,21 +2708,23 @@  prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\"" prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'"-checkSuspiciousIFS params (T_Assignment id Assign "IFS" [] value) =-    potentially $ do-        str <- getLiteralString value-        return $ check str+prop_checkSuspiciousIFS3 = verify checkSuspiciousIFS "IFS=' \\t\\n'"+checkSuspiciousIFS params (T_Assignment _ _ "IFS" [] value) =+    mapM_ check $ getLiteralString value   where-    n = if shellType params == Sh then "'<literal linefeed here>'" else "$'\\n'"-    t = if shellType params == Sh then "\"$(printf '\\t')\"" else "$'\\t'"+    hasDollarSingle = shellType params == Bash || shellType params == Ksh+    n = if hasDollarSingle then  "$'\\n'" else "'<literal linefeed here>'"+    t = if hasDollarSingle then  "$'\\t'" else "\"$(printf '\\t')\""     check value =         case value of             "\\n" -> suggest n-            "/n" -> suggest n             "\\t" -> suggest t-            "/t" -> suggest t+            x | '\\' `elem` x -> suggest2 "a literal backslash"+            x | 'n' `elem` x -> suggest2 "the literal letter 'n'"+            x | 't' `elem` x -> suggest2 "the literal letter 't'"             _ -> return ()-    suggest r = warn id 2141 $ "Did you mean IFS=" ++ r ++ " ?"+    suggest r = warn (getId value) 2141 $ "This backslash is literal. Did you mean IFS=" ++ r ++ " ?"+    suggest2 desc = warn (getId value) 2141 $ "This IFS value contains " ++ desc ++ ". For tabs/linefeeds/escapes, use $'..', literal, or printf." checkSuspiciousIFS _ _ = return ()  @@ -2631,7 +2735,7 @@ prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)" prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]" checkShouldUseGrepQ params t =-    potentially $ case t of+    sequence_ $ case t of         TC_Nullary id _ token -> check id True token         TC_Unary id _ "-n" token -> check id True token         TC_Unary id _ "-z" token -> check id False token@@ -2678,6 +2782,9 @@ prop_checkTestArgumentSplitting16 = verifyNot checkTestArgumentSplitting "[[ -v foo[123] ]]" prop_checkTestArgumentSplitting17 = verifyNot checkTestArgumentSplitting "#!/bin/ksh\n[ -e foo* ]" prop_checkTestArgumentSplitting18 = verify checkTestArgumentSplitting "#!/bin/ksh\n[ -d foo* ]"+prop_checkTestArgumentSplitting19 = verifyNot checkTestArgumentSplitting "[[ var[x] -eq 2*3 ]]"+prop_checkTestArgumentSplitting20 = verify checkTestArgumentSplitting "[ var[x] -eq 2 ]"+prop_checkTestArgumentSplitting21 = verify checkTestArgumentSplitting "[ 6 -eq 2*3 ]" checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] () checkTestArgumentSplitting params t =     case t of@@ -2707,6 +2814,18 @@          (TC_Unary _ typ op token) -> checkAll typ token +        (TC_Binary _ typ op lhs rhs) | op `elem` arithmeticBinaryTestOps ->+            if typ == DoubleBracket+            then+                mapM_ (\c -> do+                        checkArrays typ c+                        checkBraces typ c) [lhs, rhs]+            else+                mapM_ (\c -> do+                        checkNumericalGlob typ c+                        checkArrays typ c+                        checkBraces typ c) [lhs, rhs]+         (TC_Binary _ typ op lhs rhs) ->             if op `elem` ["=", "==", "!=", "=~"]             then do@@ -2739,13 +2858,18 @@             then warn (getId token) 2202 "Globs don't work as operands in [ ]. Use a loop."             else err (getId token) 2203 "Globs are ignored in [[ ]] except right of =/!=. Use a loop." +    checkNumericalGlob SingleBracket token =+        -- var[x] and x*2 look like globs+        when (shellType params /= Ksh && isGlob token) $+            err (getId token) 2255 "[ ] does not apply arithmetic evaluation. Evaluate with $((..)) for numbers, or use string comparator for strings." + prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }" prop_checkMaskedReturns2 = verify checkMaskedReturns "declare a=$(false)" prop_checkMaskedReturns3 = verify checkMaskedReturns "declare a=\"`false`\"" prop_checkMaskedReturns4 = verifyNot checkMaskedReturns "declare a; a=$(false)" prop_checkMaskedReturns5 = verifyNot checkMaskedReturns "f() { local -r a=$(false); }"-checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = potentially $ do+checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = sequence_ $ do     name <- getCommandName t     guard $ name `elem` ["declare", "export"]         || name == "local" && "r" `notElem` map snd (getAllFlags t)@@ -2764,9 +2888,20 @@  prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo" prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"-checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =-    unless ("r" `elem` map snd (getAllFlags t)) $+prop_checkReadWithoutR3 = verifyNot checkReadWithoutR "read -t 0"+prop_checkReadWithoutR4 = verifyNot checkReadWithoutR "read -t 0 && read --d '' -r bar"+prop_checkReadWithoutR5 = verifyNot checkReadWithoutR "read -t 0 foo < file.txt"+prop_checkReadWithoutR6 = verifyNot checkReadWithoutR "read -u 3 -t 0"+checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read"+    && "r" `notElem` map snd flags && not has_t0 =         info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes."+  where+    flags = getAllFlags t+    has_t0 = Just "0" == do+        parsed <- getOpts flagsForRead flags+        t <- lookup "t" parsed+        getLiteralString t+ checkReadWithoutR _ _ = return ()  prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"@@ -2806,15 +2941,15 @@         []     else execWriter $ doAnalysis checkElement root   where-    checkElement t@T_SimpleCommand {} = do-        let name = getName t-        when(name `elem` ["cd", "pushd", "popd"]+    checkElement t@T_SimpleCommand {}+        | name `elem` ["cd", "pushd", "popd"]             && not (isSafeDir t)             && not (name `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))-            && not (isCondition $ getPath (parentMap params) t)) $+            && not (isCondition $ getPath (parentMap params) t) =                 warnWithFix (getId t) 2164                     ("Use '" ++ name ++ " ... || exit' or '" ++ name ++ " ... || return' in case " ++ name ++ " fails.")                     (fixWith [replaceEnd (getId t) params 0 " || exit"])+        where name = getName t     checkElement _ = return ()     getName t = fromMaybe "" $ getCommandName t     isSafeDir t = case oversimplify t of@@ -2826,14 +2961,14 @@ prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done" prop_checkLoopVariableReassignment3 = verifyNot checkLoopVariableReassignment "for i in *; do for j in *.bar; do true; done; done" checkLoopVariableReassignment params token =-    potentially $ case token of+    sequence_ $ case token of         T_ForIn {} -> check         T_ForArithmetic {} -> check         _ -> Nothing   where     check = do         str <- loopVariable token-        next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path+        next <- find (\x -> loopVariable x == Just str) path         return $ do             warn (getId token) 2165 "This nested loop overrides the index variable of its parent."             warn (getId next)  2167 "This parent loop has its index variable overridden."@@ -2859,16 +2994,15 @@         T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token         _ -> return ()   where-    check t command =-        case t of-            T_NormalWord id [T_Literal _ str] -> potentially $ do-                guard $ str `elem` [ "]]", "]" ]-                let opposite = invert str-                    parameters = oversimplify command-                guard $ opposite `notElem` parameters-                return $ warn id 2171 $-                    "Found trailing " ++ str ++ " outside test. Add missing " ++ opposite ++ " or quote if intentional."-            _ -> return ()+    check (T_NormalWord id [T_Literal _ str]) command+        | str `elem` [ "]]", "]" ]+        && opposite `notElem` parameters+        = warn id 2171 $+            "Found trailing " ++ str ++ " outside test. Add missing " ++ opposite ++ " or quote if intentional."+        where+            opposite = invert str+            parameters = oversimplify command+    check _ _ = return ()     invert s =         case s of             "]]" -> "[["@@ -2888,10 +3022,10 @@     case token of         TC_Binary id _ _ lhs rhs -> check lhs rhs         TA_Binary id _ lhs rhs -> check lhs rhs-        TA_Unary id _ exp ->-            when (isExitCode exp) $ message (getId exp)-        TA_Sequence _ [exp] ->-            when (isExitCode exp) $ message (getId exp)+        TA_Unary id _ exp+            | isExitCode exp -> message (getId exp)+        TA_Sequence _ [exp]+            | isExitCode exp -> message (getId exp)         _ -> return ()   where     check lhs rhs =@@ -2915,12 +3049,12 @@ prop_checkRedirectedNowhere8 = verifyNot checkRedirectedNowhere "var=`< file`" checkRedirectedNowhere params token =     case token of-        T_Pipeline _ _ [single] -> potentially $ do+        T_Pipeline _ _ [single] -> sequence_ $ do             redir <- getDanglingRedirect single             guard . not $ isInExpansion token             return $ warn (getId redir) 2188 "This redirection doesn't have a command. Move to its command (or use 'true' as no-op)." -        T_Pipeline _ _ list -> forM_ list $ \x -> potentially $ do+        T_Pipeline _ _ list -> forM_ list $ \x -> sequence_ $ do             redir <- getDanglingRedirect x             return $ err (getId redir) 2189 "You can't have | between this redirection and the command it should apply to." @@ -2944,9 +3078,13 @@ prop_checkArrayAssignmentIndices4 = verifyTree checkArrayAssignmentIndices "typeset -A foo; foo+=(bar)" prop_checkArrayAssignmentIndices5 = verifyTree checkArrayAssignmentIndices "arr=( [foo]= bar )" prop_checkArrayAssignmentIndices6 = verifyTree checkArrayAssignmentIndices "arr=( [foo] = bar )"-prop_checkArrayAssignmentIndices7 = verifyTree checkArrayAssignmentIndices "arr=( var=value )"+prop_checkArrayAssignmentIndices7 = verifyNotTree checkArrayAssignmentIndices "arr=( var=value )" prop_checkArrayAssignmentIndices8 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=bar )" prop_checkArrayAssignmentIndices9 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=\"\" )"+prop_checkArrayAssignmentIndices10 = verifyTree checkArrayAssignmentIndices "declare -A arr; arr=( var=value )"+prop_checkArrayAssignmentIndices11 = verifyTree checkArrayAssignmentIndices "arr=( 1=value )"+prop_checkArrayAssignmentIndices12 = verifyTree checkArrayAssignmentIndices "arr=( $a=value )"+prop_checkArrayAssignmentIndices13 = verifyTree checkArrayAssignmentIndices "arr=( $((1+1))=value )" checkArrayAssignmentIndices params root =     runNodeAnalysis check params root   where@@ -2967,11 +3105,9 @@              T_NormalWord _ parts ->                 let literalEquals = do-                    part <- parts-                    (id, str) <- case part of-                        T_Literal id str -> [(id,str)]-                        _ -> []-                    guard $ '=' `elem` str+                    T_Literal id str <- parts+                    let (before, after) = break ('=' ==) str+                    guard $ all isDigit before && not (null after)                     return $ warnWithFix id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it." (surroundWidth id params "\"")                 in                     if null literalEquals && isAssociative@@ -2980,6 +3116,7 @@              _ -> return () + prop_checkUnmatchableCases1 = verify checkUnmatchableCases "case foo in bar) true; esac" prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac" prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac"@@ -3000,7 +3137,7 @@             if isConstant word                 then warn (getId word) 2194                         "This word is constant. Did you forget the $ on a variable?"-                else  potentially $ do+                else  sequence_ $ do                     pg <- wordToPseudoGlob word                     return $ mapM_ (check pg) allpatterns @@ -3015,19 +3152,18 @@     fst3 (x,_,_) = x     snd3 (_,x,_) = x     tp = tokenPositions params-    check target candidate = potentially $ do+    check target candidate = sequence_ $ do         candidateGlob <- wordToPseudoGlob candidate         guard . not $ pseudoGlobsCanOverlap target candidateGlob         return $ warn (getId candidate) 2195                     "This pattern will never match the case statement's word. Double check them." -    tupMap f l = zip l (map f l)+    tupMap f l = map (\x -> (x, f x)) l     checkDoms ((glob, Just x), rest) =-        case filter (\(_, p) -> x `pseudoGlobIsSuperSetof` p) valids of-            ((first,_):_) -> do+        forM_ (find (\(_, p) -> x `pseudoGlobIsSuperSetof` p) valids) $+            \(first,_) -> do                 warn (getId glob) 2221 $ "This pattern always overrides a later one" <> patternContext (getId first)                 warn (getId first) 2222 $ "This pattern never matches because of a previous pattern" <> patternContext (getId glob)-            _ -> return ()       where         patternContext :: Id -> String         patternContext id =@@ -3035,9 +3171,7 @@               Just l -> " on line " <> show l <> "."               _      -> "." -        valids = concatMap f rest-        f (x, Just y) = [(x,y)]-        f _ = []+        valids = [(x,y) | (x, Just y) <- rest]     checkDoms _ = return ()  @@ -3063,9 +3197,9 @@       checkParams id first second = do-        when (fromMaybe False $ (`elem` unaryTestOps) <$> getLiteralString first) $+        when (maybe False (`elem` unaryTestOps) $ getLiteralString first) $             err id 2204 "(..) is a subshell. Did you mean [ .. ], a test expression?"-        when (fromMaybe False $ (`elem` binaryTestOps) <$> getLiteralString second) $+        when (maybe False (`elem` binaryTestOps) $ getLiteralString second) $             warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?"  @@ -3092,7 +3226,7 @@         T_DollarBraced id _ str |             not (isCountingReference part)             && not (isQuotedAlternativeReference part)-            && not (getBracedReference (bracedString part) `elem` variablesWithoutSpaces)+            && getBracedReference (bracedString part) `notElem` variablesWithoutSpaces             -> warn id 2206 $                 if shellType params == Ksh                 then "Quote to prevent word splitting/globbing, or split robustly with read -A or while read."@@ -3111,7 +3245,7 @@ prop_checkRedirectionToNumber3 = verifyNot checkRedirectionToNumber "echo foo > '2'" prop_checkRedirectionToNumber4 = verifyNot checkRedirectionToNumber "foo 1>&2" checkRedirectionToNumber _ t = case t of-    T_IoFile id _ word -> potentially $ do+    T_IoFile id _ word -> sequence_ $ do         file <- getUnquotedLiteral word         guard $ all isDigit file         return $ warn id 2210 "This is a file redirection. Was it supposed to be a comparison or fd operation?"@@ -3121,8 +3255,8 @@ prop_checkGlobAsCommand2 = verify checkGlobAsCommand "$(var[i])" prop_checkGlobAsCommand3 = verifyNot checkGlobAsCommand "echo foo*" checkGlobAsCommand _ t = case t of-    T_SimpleCommand _ _ (first:_) ->-        when (isGlob first) $+    T_SimpleCommand _ _ (first:_)+        | isGlob first ->             warn (getId first) 2211 "This is a glob used as a command name. Was it supposed to be in ${..}, array, or is it missing quoting?"     _ -> return () @@ -3132,8 +3266,8 @@ prop_checkFlagAsCommand3 = verifyNot checkFlagAsCommand "'--myexec--' args" prop_checkFlagAsCommand4 = verifyNot checkFlagAsCommand "var=cmd --arg"  -- Handled by SC2037 checkFlagAsCommand _ t = case t of-    T_SimpleCommand _ [] (first:_) ->-        when (isUnquotedFlag first) $+    T_SimpleCommand _ [] (first:_)+        | isUnquotedFlag first ->             warn (getId first) 2215 "This flag is used as a command name. Bad line break or missing [ .. ]?"     _ -> return () @@ -3157,10 +3291,10 @@ checkPipeToNowhere _ t =     case t of         T_Pipeline _ _ (first:rest) -> mapM_ checkPipe rest-        T_Redirecting _ redirects cmd -> when (any redirectsStdin redirects) $ checkRedir cmd+        T_Redirecting _ redirects cmd | any redirectsStdin redirects -> checkRedir cmd         _ -> return ()   where-    checkPipe redir = potentially $ do+    checkPipe redir = sequence_ $ do         cmd <- getCommand redir         name <- getCommandBasename cmd         guard $ name `elem` nonReadingCommands@@ -3173,7 +3307,7 @@         return $ warn (getId cmd) 2216 $             "Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion -    checkRedir cmd = potentially $ do+    checkRedir cmd = sequence_ $ do         name <- getCommandBasename cmd         guard $ name `elem` nonReadingCommands         guard . not $ hasAdditionalConsumers cmd@@ -3186,9 +3320,8 @@             "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+    hasAdditionalConsumers t = isNothing $         doAnalysis (guard . not . mayConsume) t-        return False      mayConsume t =         case t of@@ -3221,7 +3354,7 @@                 mapM_ (checkUsage m) $ concatMap recursiveSequences cmds         _ -> return () -    checkUsage map cmd = potentially $ do+    checkUsage map cmd = sequence_ $ do         name <- getCommandName cmd         def <- Map.lookup name map         return $@@ -3338,8 +3471,8 @@ 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+        T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands+            && 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 () @@ -3378,6 +3511,23 @@   where     fix id = fixWith [replaceStart id params 2 "\"$"] +prop_checkTranslatedStringVariable1 = verify checkTranslatedStringVariable "foo_bar2=val; $\"foo_bar2\""+prop_checkTranslatedStringVariable2 = verifyNot checkTranslatedStringVariable "$\"foo_bar2\""+prop_checkTranslatedStringVariable3 = verifyNot checkTranslatedStringVariable "$\"..\""+prop_checkTranslatedStringVariable4 = verifyNot checkTranslatedStringVariable "var=val; $\"$var\""+prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\""+checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s])+  | all isVariableChar s+  && Map.member s assignments+  = warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id)+  where+    assignments = foldl (flip ($)) Map.empty (map insertAssignment $ variableFlow params)+    insertAssignment (Assignment (_, token, name, _)) | isVariableName name =+        Map.insert name token+    insertAssignment _ = Prelude.id+    fix id = fixWith [replaceStart id params 2 "\"$"]+checkTranslatedStringVariable _ _ = return ()+ prop_checkDefaultCase1 = verify checkDefaultCase "case $1 in a) true ;; esac" prop_checkDefaultCase2 = verify checkDefaultCase "case $1 in ?*?) true ;; *? ) true ;; esac" prop_checkDefaultCase3 = verifyNot checkDefaultCase "case $1 in x|*) true ;; esac"@@ -3440,6 +3590,41 @@             [_] -> []             x:rest -> x : dropLast rest             _ -> []++prop_checkModifiedArithmeticInRedirection1 = verify checkModifiedArithmeticInRedirection "ls > $((i++))"+prop_checkModifiedArithmeticInRedirection2 = verify checkModifiedArithmeticInRedirection "cat < \"foo$((i++)).txt\""+prop_checkModifiedArithmeticInRedirection3 = verifyNot checkModifiedArithmeticInRedirection "while true; do true; done > $((i++))"+prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))"+prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n"+checkModifiedArithmeticInRedirection _ t =+    case t of+        T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs+        _ -> return ()+  where+    checkRedirs t =+        case t of+            T_FdRedirect _ _ (T_IoFile _ _ word) ->+                mapM_ checkArithmetic $ getWordParts word+            T_FdRedirect _ _ (T_HereString _ word) ->+                mapM_ checkArithmetic $ getWordParts word+            T_FdRedirect _ _ (T_HereDoc _ _ _ _ list) ->+                mapM_ checkArithmetic list+            _ -> return ()+    checkArithmetic t =+        case t of+            T_DollarArithmetic _ x -> checkModifying x+            _ -> return ()+    checkModifying t =+        case t of+            TA_Sequence _ list -> mapM_ checkModifying list+            TA_Unary id s _ | s `elem` ["|++", "++|", "|--", "--|"] -> warnFor id+            TA_Assignment id _ _ _ -> warnFor id+            TA_Binary _ _ x y -> mapM_ checkModifying [x ,y]+            TA_Trinary _ x y z -> mapM_ checkModifying [x, y, z]+            _ -> return ()+    warnFor id =+        warn id 2257 "Arithmetic modifications in command redirections may be discarded. Do them separately."+  return [] runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
src/ShellCheck/Analyzer.hs view
@@ -35,17 +35,18 @@     arComments =         filterByAnnotation spec params . nub $             runAnalytics spec-            ++ runChecker params (checkers params)+            ++ runChecker params (checkers spec params) }   where     params = makeParameters spec -checkers params = mconcat $ map ($ params) [-    ShellCheck.Checks.Commands.checker,+checkers spec params = mconcat $ map ($ params) [+    ShellCheck.Checks.Commands.checker spec,     ShellCheck.Checks.Custom.checker,     ShellCheck.Checks.ShellSupport.checker     ]  optionalChecks = mconcat $ [-    ShellCheck.Analytics.optionalChecks+    ShellCheck.Analytics.optionalChecks,+    ShellCheck.Checks.Commands.optionalChecks     ]
src/ShellCheck/AnalyzerLib.hs view
@@ -239,19 +239,14 @@  determineShellTest = determineShellTest' Nothing determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript-determineShell fallbackShell t = fromMaybe Bash $ do-    shellString <- foldl mplus Nothing $ getCandidates t+determineShell fallbackShell t = fromMaybe Bash $     shellForExecutable shellString `mplus` fallbackShell   where-    forAnnotation t =-        case t of-            (ShellOverride s) -> return s-            _                 -> fail ""-    getCandidates :: Token -> [Maybe String]-    getCandidates t@T_Script {} = [Just $ fromShebang t]-    getCandidates (T_Annotation _ annotations s) =-        map forAnnotation annotations ++-           [Just $ fromShebang s]+    shellString = getCandidate t+    getCandidate :: Token -> String+    getCandidate t@T_Script {} = fromShebang t+    getCandidate (T_Annotation _ annotations s) =+        headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]     fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s  -- Given a string like "/bin/bash" or "/usr/bin/env dash",@@ -259,7 +254,7 @@ executableFromShebang :: String -> String executableFromShebang = shellFor   where-    shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])+    shellFor s | "/env " `isInfixOf` s = headOrDefault "" (drop 1 $ words s)     shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s     shellFor s = reverse . takeWhile (/= '/') . reverse $ s @@ -299,7 +294,7 @@  isQuoteFreeNode strict tree t =     (isQuoteFreeElement t == Just True) ||-        head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])+        headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))   where     -- Is this node self-quoting in itself?     isQuoteFreeElement t =@@ -454,7 +449,7 @@         T_BatsTest {} -> SubshellScope "@bats test"         T_CoProcBody _ _  -> SubshellScope "coproc"         T_Redirecting {}  ->-            if fromMaybe False causesSubshell+            if causesSubshell == Just True             then SubshellScope "pipeline"             else NoneScope         _ -> NoneScope@@ -513,7 +508,7 @@         T_DollarBraced _ _ l -> maybeToList $ do             let string = bracedString t             let modifier = getBracedModifier string-            guard $ ":=" `isPrefixOf` modifier+            guard $ any (`isPrefixOf` modifier) ["=", ":="]             return (t, t, getBracedReference string, DataString $ SourceFrom [l])          t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo@@ -548,8 +543,9 @@             else []         "trap" ->             case rest of-                head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head+                head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head                 _ -> []+        "alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]         _ -> []   where     getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]@@ -567,9 +563,11 @@ --   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)) =+getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T_Literal _ x:_):rest)) =    filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $     case x of+        "builtin" ->+            getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest         "read" ->             let params = map getLiteral rest                 readArrayVars = getReadArrayVariables rest@@ -610,8 +608,7 @@         _ -> []   where     flags = map snd $ getAllFlags base-    stripEquals s = let rest = dropWhile (/= '=') s in-        if rest == "" then "" else tail rest+    stripEquals s = drop 1 $ dropWhile (/= '=') s     stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =         T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)     stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =@@ -642,7 +639,7 @@     getModifierParam _ _ = []      letParamToLiteral token =-          if var == ""+          if null var             then []             else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]         where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token@@ -761,9 +758,8 @@         _          -> Nothing      getIfReference context token = maybeToList $ do-            str <- getLiteralStringExt literalizer token-            guard . not $ null str-            when (isDigit $ head str) $ fail "is a number"+            str@(h:_) <- getLiteralStringExt literalizer token+            when (isDigit h) $ fail "is a number"             return (context, token, getBracedReference str)      isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])@@ -783,8 +779,8 @@ -- Compare a command to a literal. Like above, but checks full path. isUnqualifiedCommand token str = isCommandMatch token (== str) -isCommandMatch token matcher = fromMaybe False $-    fmap matcher (getCommandName token)+isCommandMatch token matcher = maybe False+    matcher (getCommandName token)  -- Does this regex look like it was intended as a glob? -- True:  *foo*@@ -805,7 +801,7 @@ isVariableName _     = False  getVariablesFromLiteralToken token =-    getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)+    getVariablesFromLiteral (getLiteralStringDef " " token)  -- Try to get referenced variables from a literal string like "$foo" -- Ignores tons of cases like arithmetic evaluation and array indices.@@ -855,7 +851,7 @@ prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz" prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo" prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"-getBracedModifier s = fromMaybe "" . listToMaybe $ do+getBracedModifier s = headOrDefault "" $ do     let var = getBracedReference s     a <- dropModifier s     dropPrefix var a@@ -869,15 +865,6 @@  -- Useful generic functions. --- Run an action in a Maybe (or do nothing).--- Example:--- potentially $ do---   s <- getLiteralString cmd---   guard $ s `elem` ["--recursive", "-r"]---   return $ warn .. "Something something recursive"-potentially :: Monad m => Maybe (m ()) -> m ()-potentially = fromMaybe (return ())- -- Get element 0 or a default. Like `head` but safe. headOrDefault _ (a:_) = a headOrDefault def _   = def@@ -932,12 +919,11 @@ --     Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)] -- where flags with arguments map to arguments, while others map to themselves. -- Any unrecognized flag will result in Nothing.-getGnuOpts = getOpts getAllFlags-getBsdOpts = getOpts getLeadingFlags-getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]-getOpts flagTokenizer string cmd = process flags+getGnuOpts str t = getOpts str $ getAllFlags t+getBsdOpts str t = getOpts str $ getLeadingFlags t+getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]+getOpts string flags = process flags   where-    flags = flagTokenizer cmd     flagList (c:':':rest) = ([c], True) : flagList rest     flagList (c:rest)     = ([c], False) : flagList rest     flagList []           = []@@ -952,7 +938,7 @@         takesArg <- Map.lookup flag1 flagMap         if takesArg             then do-                guard $ flag2 == ""+                guard $ null flag2                 more <- process rest                 return $ (flag1, token2) : more             else do
src/ShellCheck/Checker.hs view
@@ -48,7 +48,7 @@   where     fail = error "Internal shellcheck error: id doesn't exist. Please report!" -shellFromFilename filename = foldl mplus Nothing candidates+shellFromFilename filename = listToMaybe candidates   where     shellExtensions = [(".ksh", Ksh)                       ,(".bash", Bash)@@ -57,7 +57,7 @@                       -- The `.sh` is too generic to determine the shell:                       -- We fallback to Bash in this case and emit SC2148 if there is no shebang     candidates =-        map (\(ext,sh) -> if ext `isSuffixOf` filename then Just sh else Nothing) shellExtensions+        [sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]  checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult checkScript sys spec = do@@ -88,9 +88,9 @@                     asOptionalChecks = csOptionalChecks spec                 } where as = newAnalysisSpec root         let analysisMessages =-                fromMaybe [] $+                maybe []                     (arComments . analyzeScript . analysisSpec)-                        <$> prRoot result+                        $ prRoot result         let translator = tokenToPosition tokenPositions         return . nub . sortMessages . filter shouldInclude $             (parseMessages ++ map translator analysisMessages)@@ -104,7 +104,7 @@             code     = cCode (pcComment pc)             severity = cSeverity (pcComment pc) -    sortMessages = sortBy (comparing order)+    sortMessages = sortOn order     order pc =         let pos = pcStartPos pc             comment = pcComment pc in@@ -198,11 +198,11 @@                 }  prop_annotationDisablesBadShebang =-    [] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"+    null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"   prop_canParseDevNull =-    [] == check "source /dev/null"+    null $ check "source /dev/null"  prop_failsWhenNotSourcing =     [1091, 2154] == check "source lol; echo \"$bar\""@@ -218,7 +218,7 @@  -- FIXME: This should really be giving [1093], "recursively sourced" prop_noInfiniteSourcing =-    [] == checkWithIncludes  [("lib", "source lib")] "source lib"+    null $ checkWithIncludes  [("lib", "source lib")] "source lib"  prop_canSourceBadSyntax =     [1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"@@ -239,10 +239,10 @@     [1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"  prop_nonRecursiveAnalysis =-    [] == checkWithIncludes [("lib", "echo $1")] "source lib"+    null $ checkWithIncludes [("lib", "echo $1")] "source lib"  prop_nonRecursiveParsing =-    [] == checkWithIncludes [("lib", "echo \"$10\"")] "source lib"+    null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib"  prop_sourceDirectiveDoesntFollowFile =     null $ checkWithIncludes@@ -288,6 +288,13 @@         csScript = "(( 3.14 ))"     } +prop_canDisableShebangWarning = null $ result+  where+    result = checkWithSpec [] emptyCheckSpec {+        csFilename = "file.sh",+        csScript = "#shellcheck disable=SC2148\nfoo"+    }+ prop_shExtensionDoesntMatter = result == [2148]   where     result = checkWithSpec [] emptyCheckSpec {@@ -328,7 +335,7 @@     [2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"  -prop_readsRcFile = result == []+prop_readsRcFile = null result   where     result = checkWithRc "disable=2086" emptyCheckSpec {         csScript = "#!/bin/sh\necho $1",
src/ShellCheck/Checks/Commands.hs view
@@ -21,7 +21,7 @@ {-# LANGUAGE FlexibleContexts #-}  -- This module contains checks that examine specific commands by name.-module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where+module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where  import ShellCheck.AST import ShellCheck.ASTLib@@ -34,6 +34,7 @@ import Control.Monad import Control.Monad.RWS import Data.Char+import Data.Functor.Identity import Data.List import Data.Maybe import qualified Data.Map.Strict as Map@@ -90,13 +91,30 @@     ,checkMvArguments, checkCpArguments, checkLnArguments     ,checkFindRedirections     ,checkReadExpansions-    ,checkWhich     ,checkSudoRedirect     ,checkSudoArgs     ,checkSourceArgs     ,checkChmodDashr     ] +optionalChecks = map fst optionalCommandChecks+optionalCommandChecks :: [(CheckDescription, CommandCheck)]+optionalCommandChecks = [+    (newCheckDescription {+        cdName = "deprecate-which",+        cdDescription = "Suggest 'command -v' instead of 'which'",+        cdPositive = "which javac",+        cdNegative = "command -v javac"+    }, checkWhich)+    ]+optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks++prop_verifyOptionalExamples = all check optionalCommandChecks+  where+    check (desc, check) =+      verify check (cdPositive desc)+      && verifyNot check (cdNegative desc)+ buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap = foldl' addCheck Map.empty   where@@ -105,12 +123,16 @@   checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis-checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do+checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do     name <- getLiteralString cmd     return $         if '/' `elem` name         then             Map.findWithDefault nullCheck (Basename $ basename name) map t+        else if name == "builtin" && not (null rest) then+            let t' = T_SimpleCommand id cmdPrefix rest+                selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest+            in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'         else do             Map.findWithDefault nullCheck (Exactly name) map t             Map.findWithDefault nullCheck (Basename name) map t@@ -128,8 +150,14 @@     map = buildCommandMap list  -checker :: Parameters -> Checker-checker params = getChecker commandChecks+checker :: AnalysisSpec -> Parameters -> Checker+checker spec params = getChecker $ commandChecks ++ optionals+  where+    keys = asOptionalChecks spec+    optionals =+        if "all" `elem` keys+        then map snd optionalCommandChecks+        else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys  prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"@@ -169,15 +197,14 @@ prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)" prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'" checkFindNameGlob = CommandCheck (Basename "find") (f . arguments)  where-    acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]-    acceptsGlob _ = False+    acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]     f [] = return ()-    f [x] = return ()-    f (a:b:r) = do-        when (acceptsGlob (getLiteralString a) && isGlob b) $ do-            let (Just s) = getLiteralString a+    f (x:xs) = g x xs+    g _ [] = return ()+    g a (b:r) = do+        forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $             warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."-        f (b:r)+        g b r   prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"@@ -221,13 +248,12 @@ checkGrepRe = CommandCheck (Basename "grep") check where     check cmd = f cmd (arguments cmd)     -- --regex=*(extglob) doesn't work. Fixme?-    skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s-    skippable _ = False+    skippable s = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s     f _ [] = return ()     f cmd (x:r) =-        let str = getLiteralStringExt (const $ return "_") x+        let str = getLiteralStringDef "_" x         in-            if str `elem` [Just "--", Just "-e", Just "--regex"]+            if str `elem` ["--", "-e", "--regex"]             then checkRE cmd r -- Regex is *after* this             else                 if skippable str@@ -243,7 +269,7 @@             let string = concat $ oversimplify re             if isConfusedGlobRegex string then                 warn (getId re) 2063 "Grep uses regex, but this looks like a glob."-              else potentially $ do+              else sequence_ $ do                 char <- getSuspiciousRegexWildcard string                 return $ info (getId re) 2022 $                     "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."@@ -252,10 +278,10 @@         grepGlobFlags = ["fixed-strings", "F", "include", "exclude", "exclude-dir", "o", "only-matching"]      wordStartingWith c =-        head . filter ([c] `isPrefixOf`) $ candidates+        headOrDefault (c:"test") . filter ([c] `isPrefixOf`) $ candidates       where         candidates =-            sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]+            sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords      getSuspiciousRegexWildcard str =         if not $ str `matches` contra@@ -318,10 +344,10 @@             invalid (getId value)     f _ = return () -    isInvalid s = s == "" || any (not . isDigit) s || length s > 5+    isInvalid s = null s || any (not . isDigit) s || length s > 5         || let value = (read s :: Integer) in value > 255 -    literal token = fromJust $ getLiteralStringExt lit token+    literal token = runIdentity $ getLiteralStringExt lit token     lit (T_DollarBraced {}) = return "0"     lit (T_DollarArithmetic {}) = return "0"     lit (T_DollarExpansion {}) = return "0"@@ -338,7 +364,7 @@     check (exec:arg:term:_) = do         execS <- getLiteralString exec         termS <- getLiteralString term-        cmdS <- getLiteralStringExt (const $ return " ") arg+        let cmdS = getLiteralStringDef " " arg          guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]         guard $ cmdS `matches` commandRegex@@ -434,7 +460,7 @@ prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin" checkMkdirDashPM = CommandCheck (Basename "mkdir") check   where-    check t = potentially $ do+    check t = sequence_ $ do         let flags = getAllFlags t         dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags         dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags@@ -460,7 +486,7 @@         first:rest -> unless (isFlag first) $ mapM_ check rest         _ -> return () -    check param = potentially $ do+    check param = sequence_ $ do         str <- getLiteralString param         let id = getId param         return $ sequence_ $ mapMaybe (\f -> f id str) [@@ -548,7 +574,7 @@     f _ = return ()      check format more = do-        fromMaybe (return ()) $ do+        sequence_ $ do             string <- getLiteralString format             let formats = getPrintfFormats string             let formatCount = length formats@@ -573,7 +599,7 @@          unless ('%' `elem` concat (oversimplify format) || isLiteral format) $           info (getId format) 2059-              "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."+              "Don't use variables in the printf format string. Use printf '..%s..' \"$foo\"."       where         onlyTrailingTs format argCount =             all (== 'T') $ drop argCount format@@ -660,7 +686,7 @@ prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}" checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)   where-    check t = potentially $ do+    check t = sequence_ $ do         var <- getSingleUnmodifiedVariable t         let name = bracedString var         return . warn (getId t) 2163 $@@ -676,13 +702,13 @@ prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}" checkReadExpansions = CommandCheck (Exactly "read") check   where-    options = getGnuOpts "sreu:n:N:i:p:a:"+    options = getGnuOpts flagsForRead     getVars cmd = fromMaybe [] $ do         opts <- options cmd-        return . map snd $ filter (\(x,_) -> x == "" || x == "a") opts+        return [y | (x,y) <- opts, null x || x == "a"]      check cmd = mapM_ warning $ getVars cmd-    warning t = potentially $ do+    warning t = sequence_ $ do         var <- getSingleUnmodifiedVariable t         let name = bracedString var         guard $ isVariableName name   -- e.g. not $1@@ -708,7 +734,7 @@     re = mkRegex "\\$\\{?[0-9*@]"     f = mapM_ checkArg     checkArg arg =-        let string = fromJust $ getLiteralStringExt (const $ return "_") arg in+        let string = getLiteralStringDef "_" arg in             when ('=' `elem` string && string `matches` re) $                 err (getId arg) 2142                     "Aliases can't use positional parameters. Use a function."@@ -721,7 +747,7 @@   where     f = mapM_ checkArg     checkArg arg | '=' `elem` concat (oversimplify arg) =-        forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $+        forM_ (find (not . isLiteral) $ getWordParts arg) $             \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."     checkArg _ = return () @@ -754,7 +780,7 @@     -- 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+        let flag = getLiteralStringDef "___" first in             not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest     hasPath [] = False     isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag@@ -832,7 +858,7 @@     f :: Token -> Analysis     f t@(T_SimpleCommand _ _ (cmd:arg1:_))  = do         path <- getPathM t-        potentially $ do+        sequence_ $ do             options <- getLiteralString arg1             (T_WhileExpression _ _ body) <- findFirst whileLoop path             caseCmd <- mapMaybe findCase body !!! 0@@ -859,7 +885,7 @@     warnUnhandled optId caseId str =         warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'." -    warnRedundant (key, expr) = potentially $ do+    warnRedundant (key, expr) = sequence_ $ do         str <- key         guard $ str `notElem` ["*", ":", "?"]         return $ warn (getId expr) 2214 "This case is not specified by getopts."@@ -918,7 +944,7 @@             Nothing ->                 checkWord' token -    checkWord' token = fromMaybe (return ()) $ do+    checkWord' token = sequence_ $ do         filename <- getPotentialPath token         let path = fixPath filename         return . when (path `elem` importantPaths) $@@ -968,10 +994,9 @@         _ -> return ()   where     args = getAllFlags token-    params = map fst $ filter (\(_,x) -> x == "") args+    params = [x | (x,"") <- args]     hasTarget =-        any (\x -> x /= "" && x `isPrefixOf` "target-directory") $-            map snd args+        any (\(_,x) -> x /= "" && x `isPrefixOf` "target-directory") args  prop_checkMvArguments1 = verify    checkMvArguments "mv 'foo bar'" prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"@@ -1031,7 +1056,7 @@             Just (T_Redirecting _ redirs _) ->                 mapM_ warnAbout redirs     warnAbout (T_FdRedirect _ s (T_IoFile id op file))-        | (s == "" || s == "&") && not (special file) =+        | (null s || s == "&") && not (special file) =         case op of             T_Less _ ->               info (getId op) 2024@@ -1055,9 +1080,9 @@ prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo" checkSudoArgs = CommandCheck (Basename "sudo") f   where-    f t = potentially $ do+    f t = sequence_ $ do         opts <- parseOpts t-        let nonFlags = map snd $ filter (\(flag, _) -> flag == "") opts+        let nonFlags = [x | ("",x) <- opts]         commandArg <- nonFlags !!! 0         command <- getLiteralString commandArg         guard $ command `elem` builtins@@ -1083,7 +1108,7 @@ checkChmodDashr = CommandCheck (Basename "chmod") f   where     f t = mapM_ check $ arguments t-    check t = potentially $ do+    check t = sequence_ $ do         flag <- getLiteralString t         guard $ flag == "-r"         return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
src/ShellCheck/Checks/ShellSupport.hs view
@@ -30,6 +30,7 @@ import Control.Monad import Control.Monad.RWS import Data.Char+import Data.Functor.Identity import Data.List import Data.Maybe import qualified Data.Map as Map@@ -73,7 +74,7 @@ prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" checkForDecimals = ForShell [Sh, Dash, Bash] f   where-    f t@(TA_Expansion id _) = potentially $ do+    f t@(TA_Expansion id _) = sequence_ $ do         str <- getLiteralString t         first <- str !!! 0         guard $ isDigit first && '.' `elem` str@@ -205,7 +206,7 @@         | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =             unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"     bashism (TC_Binary id SingleBracket op _ _)-        | op `elem` [ "-nt", "-ef" ] =+        | op `elem` [ "-ot", "-nt", "-ef" ] =             unless isDash $ warnMsg id $ op ++ " is"     bashism (TC_Binary id SingleBracket "==" _ _) =             warnMsg id "== in place of = is"@@ -337,17 +338,17 @@         in do             when (name `elem` unsupportedCommands) $                 warnMsg id $ "'" ++ name ++ "' is"-            potentially $ do+            sequence_ $ do                 allowed' <- Map.lookup name allowedFlags                 allowed <- allowed'-                (word, flag) <- listToMaybe $-                    filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags+                (word, flag) <- find+                    (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags                 return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"              when (name == "source") $ warnMsg id "'source' in place of '.' is"             when (name == "trap") $                 let-                    check token = potentially $ do+                    check token = sequence_ $ do                         str <- getLiteralString token                         let upper = map toUpper str                         return $ do@@ -362,7 +363,7 @@                 in                     mapM_ check (drop 1 rest) -            when (name == "printf") $ potentially $ do+            when (name == "printf") $ sequence_ $ do                 format <- rest !!! 0  -- flags are covered by allowedFlags                 let literal = onlyLiteralString format                 guard $ "%q" `isInfixOf` literal@@ -456,11 +457,10 @@      -- This should have used backreferences, but TDFA doesn't support them     sedRe = mkRegex "^s(.)([^\n]*)g?$"-    isSimpleSed s = fromMaybe False $ do-        [first,rest] <- matchRegex sedRe s-        let delimiters = filter (== head first) rest+    isSimpleSed s = isJust $ do+        [h:_,rest] <- matchRegex sedRe s+        let delimiters = filter (== h) rest         guard $ length delimiters == 2-        return True     checkIn id s =         when (isSimpleSed s) $             style id 2001 "See if you can use ${variable//search/replace} instead."@@ -487,11 +487,11 @@             T_DollarBraced {} -> return "$"             T_DollarExpansion {} -> return "$"             T_DollarArithmetic {} -> return "$"-            otherwise -> return "-"-    toString t = fromJust $ getLiteralStringExt literalExt t+            _ -> return "-"+    toString t = runIdentity $ getLiteralStringExt literalExt t     isEvaled t = do         cmd <- getClosestCommandM t-        return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"+        return $ maybe False (`isUnqualifiedCommand` "eval") cmd   prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
src/ShellCheck/Data.hs view
@@ -114,6 +114,10 @@     "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="   ] +arithmeticBinaryTestOps = [+    "-eq", "-ne", "-lt", "-le", "-gt", "-ge"+  ]+ unaryTestOps = [     "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",     "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",@@ -131,4 +135,6 @@         "ksh"   -> return Ksh         "ksh88" -> return Ksh         "ksh93" -> return Ksh-        otherwise -> Nothing+        _ -> Nothing++flagsForRead = "sreu:n:N:i:p:a:t:"
src/ShellCheck/Fixer.hs view
@@ -200,7 +200,7 @@     let si = fromIntegral (start-1)         ei = fromIntegral (end-1)         (x, xs) = splitAt si o-        (y, z) = splitAt (ei - si) xs+        z = drop (ei - si) xs     in     x ++ r ++ z @@ -295,7 +295,7 @@     -- Trivial O(n * m) implementation     dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]     dumbPrefixSums kvs targets =-        let prefixSum target = sum . map snd . filter (\(k,v) -> k <= target) $ kvs+        let prefixSum target = sum [v | (k,v) <- kvs, k <= target]         in map prefixSum targets     -- PSTree O(n * log m) implementation     smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
src/ShellCheck/Formatter/Diff.hs view
@@ -43,14 +43,15 @@  format :: FormatterOptions -> IO Formatter format options = do-    didOutput <- newIORef False+    foundIssues <- newIORef False+    reportedIssues <- newIORef False     shouldColor <- shouldOutputColor (foColorOption options)     let color = if shouldColor then colorize else nocolor     return Formatter {         header = return (),-        footer = checkFooter didOutput color,+        footer = checkFooter foundIssues reportedIssues color,         onFailure = reportFailure color,-        onResult  = reportResult didOutput color+        onResult  = reportResult foundIssues reportedIssues color     }  @@ -69,9 +70,10 @@ printErr color = hPutStrLn stderr . color bold . color red reportFailure color file msg = printErr color $ file ++ ": " ++ msg -checkFooter didOutput color = do-    output <- readIORef didOutput-    unless output $+checkFooter foundIssues reportedIssues color = do+    found <- readIORef foundIssues+    output <- readIORef reportedIssues+    when (found && not output) $             printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."  type ColorFunc = (Int -> String -> String)@@ -79,9 +81,10 @@ data DiffDoc a = DiffDoc String LFStatus [DiffRegion a] data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a] -reportResult :: (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()-reportResult didOutput color result sys = do+reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()+reportResult foundIssues reportedIssues color result sys = do     let comments = crComments result+    unless (null comments) $ writeIORef foundIssues True     let suggestedFixes = mapMaybe pcFix comments     let fixmap = buildFixMap suggestedFixes     mapM_ output $ M.toList fixmap@@ -91,7 +94,7 @@         case file of             Right contents -> do                 putStrLn $ formatDoc color $ makeDiff name contents fix-                writeIORef didOutput True+                writeIORef reportedIssues True             Left msg -> reportFailure color name msg  hasTrailingLinefeed str =
src/ShellCheck/Interface.hs view
@@ -256,9 +256,6 @@ data InsertionPoint = InsertBefore | InsertAfter     deriving (Show, Eq, Generic, NFData) -instance Ord Replacement where-    compare r1 r2 = (repStartPos r1) `compare` (repStartPos r2)- newReplacement = Replacement {     repStartPos = newPosition,     repEndPos = newPosition,@@ -316,10 +313,10 @@     siGetConfig = const $ return Nothing }   where-    rf file =-        case filter ((== file) . fst) files of-            [] -> return $ Left "File not included in mock."-            [(_, contents)] -> return $ Right contents+    rf file = return $+        case find ((== file) . fst) files of+            Nothing -> Left "File not included in mock."+            Just (_, contents) -> Right contents     fs _ _ file = return file  mockRcFile rcfile mock = mock {
src/ShellCheck/Parser.hs view
@@ -34,7 +34,7 @@ import Control.Monad.Trans import Data.Char import Data.Functor-import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub)+import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find) import Data.Maybe import Data.Monoid import Debug.Trace@@ -186,12 +186,12 @@  -- Get an ID starting from the first token of the list, and ending after the last getNextIdSpanningTokenList list =-    if null list-    then do+    case list of+    [] -> do         pos <- getPosition         getNextIdBetween pos pos-    else-        getNextIdSpanningTokens (head list) (last list)+    (h:_) ->+        getNextIdSpanningTokens h (last list)  -- Get the span covered by an id getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)@@ -246,6 +246,10 @@             parseNotes = n : parseNotes state         } +ignoreProblemsOf p = do+    systemState <- lift . lift $ Ms.get+    p <* (lift . lift . Ms.put $ systemState)+ shouldIgnoreCode code = do     context <- getCurrentContexts     checkSourced <- Mr.asks checkSourced@@ -321,16 +325,15 @@     parseProblemAt pos level code msg  setCurrentContexts c = Ms.modify (\state -> state { contextStack = c })-getCurrentContexts = contextStack <$> Ms.get+getCurrentContexts = Ms.gets contextStack  popContext = do     v <- getCurrentContexts-    if not $ null v-        then do-            let (a:r) = v+    case v of+        (a:r) -> do             setCurrentContexts r             return $ Just a-        else+        [] ->             return Nothing  pushContext c = do@@ -437,6 +440,7 @@     readCondContents `attempting` lookAhead (do                                 pos <- getPosition                                 s <- readVariableName+                                spacing1                                 when (s `elem` commonCommands) $                                     parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.") @@ -582,9 +586,9 @@             return $ TC_Nullary id typ x           ) -    checkTrailingOp x = fromMaybe (return ()) $ do+    checkTrailingOp x = sequence_ $ do         (T_Literal id str) <- getTrailingUnquotedLiteral x-        trailingOp <- listToMaybe (filter (`isSuffixOf` str) binaryTestOps)+        trailingOp <- find (`isSuffixOf` str) binaryTestOps         return $ parseProblemAtId id ErrorC 1108 $             "You need a space before and after the " ++ trailingOp ++ " ." @@ -910,6 +914,8 @@ prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]" prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]" prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"+prop_readCondition24 = isWarning readCondition "[[ 1 == 2 ]]]"+prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]" readCondition = called "test expression" $ do     opos <- getPosition     start <- startSpan@@ -938,6 +944,11 @@     id <- endSpan start     when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"     when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"+    optional $ lookAhead $ do+        pos <- getPosition+        notFollowedBy2 readCmdWord <|>+            parseProblemAt pos ErrorC 1136+                ("Unexpected characters after terminating " ++ close ++ ". Missing semicolon/linefeed?")     spacing     many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?     return $ T_Condition id typ condition@@ -1032,14 +1043,16 @@ prop_readNormalWord10 = isWarning readNormalWord "\x201Chello\x201D" prop_readNormalWord11 = isWarning readNormalWord "\x2018hello\x2019" prop_readNormalWord12 = isWarning readNormalWord "hello\x2018"-readNormalWord = readNormalishWord ""+readNormalWord = readNormalishWord "" ["do", "done", "then", "fi", "esac"] -readNormalishWord end = do+readPatternWord = readNormalishWord "" ["esac"]++readNormalishWord end terms = do     start <- startSpan     pos <- getPosition     x <- many1 (readNormalWordPart end)     id <- endSpan start-    checkPossibleTermination pos x+    checkPossibleTermination pos x terms     return $ T_NormalWord id x  readIndexSpan = do@@ -1059,10 +1072,10 @@         id <- endSpan start         return $ T_Literal id str -checkPossibleTermination pos [T_Literal _ x] =-    when (x `elem` ["do", "done", "then", "fi", "esac"]) $+checkPossibleTermination pos [T_Literal _ x] terminators =+    when (x `elem` terminators) $         parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."-checkPossibleTermination _ _ = return ()+checkPossibleTermination _ _ _ = return ()  readNormalWordPart end = do     notFollowedBy2 $ oneOf end@@ -1541,7 +1554,7 @@ readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable   where     arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->-        parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")+        parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")  prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'" readDollarSingleQuote = called "$'..' expression" $ do@@ -1733,6 +1746,7 @@ 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= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n" 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@@ -1753,14 +1767,17 @@     addPendingHereDoc doc     return doc   where-    quotes = "\"'\\"+    unquote :: String -> (Quoted, String)+    unquote "" = (Unquoted, "")+    unquote [c] = (Unquoted, [c])+    unquote s@(cl:tl) =+      case reverse tl of+        (cr:tr) | cr == cl && cl `elem` "\"'" -> (Quoted, reverse tr)+        _ -> (if '\\' `elem` s then (Quoted, filter ((/=) '\\') s) else (Unquoted, s))     -- Fun fact: bash considers << foo"" quoted, but not << <("foo").-    -- Instead of replicating this, just read a token and strip quotes.     readToken = do         str <- readStringForParser readNormalWord-        return (if any (`elem` quotes) str then Quoted else Unquoted,-                filter (not . (`elem` quotes)) str)-+        return $ unquote str  readPendingHereDocs = do     docs <- popPendingHereDocs@@ -1809,7 +1826,7 @@             let thereIsNoTrailer = null trailingSpace && null trailer             let leaderIsOk = null leadingSpace                     || dashed == Dashed && leadingSpacesAreTabs-            let trailerStart = if null trailer then '\0' else head trailer+            let trailerStart = case trailer of [] -> '\0'; (h:_) -> h             let hasTrailingSpace = not $ null trailingSpace             let hasTrailer = not $ null trailer             let ppt = parseProblemAt trailerPos ErrorC@@ -2035,7 +2052,11 @@        Just cmd -> do             validateCommand cmd-            suffix <- option [] $ getParser readCmdSuffix cmd [+            -- We have to ignore possible parsing problems from the lookAhead parser+            firstArgument <- ignoreProblemsOf . optionMaybe . try . lookAhead $ readCmdWord+            suffix <- option [] $ getParser readCmdSuffix+                    -- If `export` or other modifier commands are called with `builtin` we have to look at the first argument+                    (if isCommand ["builtin"] cmd then fromMaybe cmd firstArgument else cmd) [                         (["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),                         (["time"], readTimeSuffix),                         (["let"], readLetSuffix),@@ -2283,6 +2304,7 @@ prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi" 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"+prop_readIfClause6 = isWarning readIfClause "if true\nthen\nDo the thing\nfi" readIfClause = called "if expression" $ do     start <- startSpan     pos <- getPosition@@ -2442,6 +2464,7 @@   prop_readForClause = isOk readForClause "for f in *; do rm \"$f\"; done"+prop_readForClause1 = isOk readForClause "for f in *; { rm \"$f\"; }" prop_readForClause3 = isOk readForClause "for f; do foo; done" prop_readForClause4 = isOk readForClause "for((i=0; i<10; i++)); do echo $i; done" prop_readForClause5 = isOk readForClause "for ((i=0;i<10 && n>x;i++,--n))\ndo \necho $i\ndone"@@ -2481,7 +2504,7 @@             "Don't use $ on the iterator name in for loops."         name <- readVariableName `thenSkip` allspacing         values <- readInClause <|> (optional readSequentialSep >> return [])-        group <- readDoGroup id+        group <- readBraced <|> readDoGroup id         return $ T_ForIn id name values group  prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done"@@ -2519,6 +2542,7 @@ prop_readCaseClause3 = isOk readCaseClause "case foo\n in * ) echo bar & ;; esac" prop_readCaseClause4 = isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac" prop_readCaseClause5 = isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac"+prop_readCaseClause6 = isOk readCaseClause "case foo\n in if) :;; done) :;; esac" readCaseClause = called "case expression" $ do     start <- startSpan     g_Case@@ -2642,14 +2666,14 @@         return $ T_CoProcBody id body  -readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)+readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)  prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null" readCompoundCommand = do     cmd <- choice [         readBraceGroup,         readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->-            parseNoteAt pos WarningC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),+            parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),         readSubshell,         readCondition,         readWhileClause,@@ -2867,6 +2891,7 @@  tryWordToken s t = tryParseWordToken s t `thenSkip` spacing tryParseWordToken keyword t = try $ do+    pos <- getPosition     start <- startSpan     str <- anycaseString keyword     id <- endSpan start@@ -2882,9 +2907,10 @@             _ -> return ()      lookAhead keywordSeparator-    when (str /= keyword) $-        parseProblem ErrorC 1081 $-            "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."+    when (str /= keyword) $ do+        parseProblemAt pos ErrorC 1081 $+            "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "' (or quote if literal)."+        fail ""     return $ t id  anycaseString =@@ -3145,7 +3171,7 @@             Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify."      isValidShell s =-        let good = s == "" || any (`isPrefixOf` s) goodShells+        let good = null s || any (`isPrefixOf` s) goodShells             bad = any (`isPrefixOf` s) badShells         in             if good@@ -3425,4 +3451,3 @@  return [] runTests = $quickCheckAll-