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 +15/−8
- Setup.hs +2/−36
- ShellCheck.cabal +5/−13
- manpage +4/−0
- shellcheck.1 +0/−449
- shellcheck.1.md +4/−4
- shellcheck.hs +9/−3
- src/ShellCheck/AST.hs +214/−327
- src/ShellCheck/ASTLib.hs +22/−7
- src/ShellCheck/Analytics.hs +409/−224
- src/ShellCheck/Analyzer.hs +5/−4
- src/ShellCheck/AnalyzerLib.hs +28/−42
- src/ShellCheck/Checker.hs +18/−11
- src/ShellCheck/Checks/Commands.hs +68/−43
- src/ShellCheck/Checks/ShellSupport.hs +14/−14
- src/ShellCheck/Data.hs +7/−1
- src/ShellCheck/Fixer.hs +2/−2
- src/ShellCheck/Formatter/Diff.hs +12/−9
- src/ShellCheck/Interface.hs +4/−7
- src/ShellCheck/Parser.hs +59/−34
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-