ShellCheck 0.4.6 → 0.4.7
raw patch · 20 files changed
+1495/−620 lines, 20 filesdep −ShellCheckdep ~containersdep ~jsondep ~parsecPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies removed: ShellCheck
Dependency ranges changed: containers, json, parsec
API changes (from Hackage documentation)
- ShellCheck.AST: data FunctionKeyword
- ShellCheck.AST: data FunctionParentheses
- ShellCheck.AST: data Id
- ShellCheck.AST: data Root
- ShellCheck.Interface: data AnalysisResult
- ShellCheck.Interface: data FormatterOptions
- ShellCheck.Interface: data SystemInterface m
+ ShellCheck.AST: TC_Empty :: Id -> ConditionType -> Token
+ ShellCheck.AST: newtype FunctionKeyword
+ ShellCheck.AST: newtype FunctionParentheses
+ ShellCheck.AST: newtype Id
+ ShellCheck.AST: newtype Root
+ ShellCheck.ASTLib: getLeadingUnquotedString :: Token -> Maybe String
+ ShellCheck.ASTLib: isFlag :: Token -> Bool
+ ShellCheck.ASTLib: isUnquotedFlag :: Token -> Bool
+ ShellCheck.ASTLib: pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool
+ ShellCheck.ASTLib: wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
+ ShellCheck.AnalyzerLib: SourceChecked :: DataSource
+ ShellCheck.AnalyzerLib: [hasLastpipe] :: Parameters -> Bool
+ ShellCheck.AnalyzerLib: [hasSetE] :: Parameters -> Bool
+ ShellCheck.AnalyzerLib: containsLastpipe :: Token -> Bool
+ ShellCheck.AnalyzerLib: containsSetE :: Token -> Bool
+ ShellCheck.AnalyzerLib: findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
+ ShellCheck.AnalyzerLib: isCountingReference :: Token -> Bool
+ ShellCheck.AnalyzerLib: isQuotedAlternativeReference :: Token -> Bool
+ ShellCheck.Data: nonReadingCommands :: [[Char]]
+ ShellCheck.Formatter.Format: sourceFile :: PositionedComment -> String
+ ShellCheck.Interface: [asCheckSourced] :: AnalysisSpec -> Bool
+ ShellCheck.Interface: [csCheckSourced] :: CheckSpec -> Bool
+ ShellCheck.Interface: [psCheckSourced] :: ParseSpec -> Bool
+ ShellCheck.Interface: newtype AnalysisResult
+ ShellCheck.Interface: newtype FormatterOptions
+ ShellCheck.Interface: newtype SystemInterface m
- ShellCheck.AST: T_AndIf :: Id -> (Token) -> (Token) -> Token
+ ShellCheck.AST: T_AndIf :: Id -> Token -> Token -> Token
- ShellCheck.AST: T_OrIf :: Id -> (Token) -> (Token) -> Token
+ ShellCheck.AST: T_OrIf :: Id -> Token -> Token -> Token
- ShellCheck.AnalyzerLib: Parameters :: [StackData] -> Map Id Token -> Shell -> Bool -> Token -> Parameters
+ ShellCheck.AnalyzerLib: Parameters :: Bool -> Bool -> [StackData] -> Map Id Token -> Shell -> Bool -> Token -> Parameters
- ShellCheck.AnalyzerLib: filterByAnnotation :: Token -> [TokenComment] -> [TokenComment]
+ ShellCheck.AnalyzerLib: filterByAnnotation :: AnalysisSpec -> Parameters -> [TokenComment] -> [TokenComment]
- ShellCheck.AnalyzerLib: getVariableFlow :: Shell -> Map Id Token -> Token -> [StackData]
+ ShellCheck.AnalyzerLib: getVariableFlow :: Parameters -> Token -> [StackData]
- ShellCheck.AnalyzerLib: isConfusedGlobRegex :: [Char] -> Bool
+ ShellCheck.AnalyzerLib: isConfusedGlobRegex :: String -> Bool
- ShellCheck.AnalyzerLib: leadType :: Shell -> Map Id Token -> Token -> Scope
+ ShellCheck.AnalyzerLib: leadType :: Parameters -> Token -> Scope
- ShellCheck.Formatter.Format: Formatter :: IO () -> (CheckResult -> String -> IO ()) -> (FilePath -> ErrorMessage -> IO ()) -> IO () -> Formatter
+ ShellCheck.Formatter.Format: Formatter :: IO () -> (CheckResult -> SystemInterface IO -> IO ()) -> (FilePath -> ErrorMessage -> IO ()) -> IO () -> Formatter
- ShellCheck.Formatter.Format: [onResult] :: Formatter -> CheckResult -> String -> IO ()
+ ShellCheck.Formatter.Format: [onResult] :: Formatter -> CheckResult -> SystemInterface IO -> IO ()
- ShellCheck.Interface: AnalysisSpec :: Token -> Maybe Shell -> ExecutionMode -> AnalysisSpec
+ ShellCheck.Interface: AnalysisSpec :: Token -> Maybe Shell -> ExecutionMode -> Bool -> AnalysisSpec
- ShellCheck.Interface: CheckSpec :: String -> String -> [Integer] -> Maybe Shell -> CheckSpec
+ ShellCheck.Interface: CheckSpec :: String -> String -> Bool -> [Integer] -> Maybe Shell -> CheckSpec
- ShellCheck.Interface: ParseSpec :: String -> String -> ParseSpec
+ ShellCheck.Interface: ParseSpec :: String -> String -> Bool -> ParseSpec
Files
- README.md +221/−146
- ShellCheck.cabal +1/−3
- ShellCheck/AST.hs +13/−12
- ShellCheck/ASTLib.hs +69/−19
- ShellCheck/Analytics.hs +345/−191
- ShellCheck/Analyzer.hs +1/−1
- ShellCheck/AnalyzerLib.hs +163/−47
- ShellCheck/Checker.hs +23/−7
- ShellCheck/Checks/Commands.hs +178/−9
- ShellCheck/Checks/ShellSupport.hs +33/−1
- ShellCheck/Data.hs +8/−0
- ShellCheck/Formatter/CheckStyle.hs +17/−4
- ShellCheck/Formatter/Format.hs +2/−1
- ShellCheck/Formatter/GCC.hs +15/−4
- ShellCheck/Formatter/TTY.hs +10/−3
- ShellCheck/Interface.hs +10/−5
- ShellCheck/Parser.hs +288/−136
- shellcheck.1 +18/−1
- shellcheck.1.md +12/−0
- shellcheck.hs +68/−30
README.md view
@@ -6,37 +6,65 @@ The goals of ShellCheck are - - To point out and clarify typical beginner's syntax issues- that cause a shell to give cryptic error messages.+- To point out and clarify typical beginner's syntax issues that cause a shell+ to give cryptic error messages. - - To point out and clarify typical intermediate level semantic problems- that cause a shell to behave strangely and counter-intuitively.+- To point out and clarify typical intermediate level semantic problems that+ cause a shell to behave strangely and counter-intuitively. - - To point out subtle caveats, corner cases and pitfalls that may cause an- advanced user's otherwise working script to fail under future circumstances.+- To point out subtle caveats, corner cases and pitfalls that may cause an+ advanced user's otherwise working script to fail under future circumstances. See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify! +## Table of Contents +- [How to use](#how-to-use)+ - [On the web](#on-the-web)+ - [From your terminal](#from-your-terminal)+ - [In your editor](#in-your-editor)+ - [In your build or test suites](#in-your-build-or-test-suites)+- [Installing](#installing)+- [Travis CI Setup](#travis-ci-setup)+- [Compiling from source](#compiling-from-source)+ - [Installing Cabal](#installing-cabal)+ - [Compiling ShellCheck](#compiling-shellcheck)+ - [Running tests](#running-tests)+- [Gallery of bad code](#gallery-of-bad-code)+ - [Quoting](#quoting)+ - [Conditionals](#conditionals)+ - [Frequently misused commands](#frequently-misused-commands)+ - [Common beginner's mistakes](#common-beginners-mistakes)+ - [Style](#style)+ - [Data and typing errors](#data-and-typing-errors)+ - [Robustness](#robustness)+ - [Portability](#portability)+ - [Miscellaneous](#miscellaneous)+- [Testimonials](#testimonials)+- [Ignoring issues](#ignoring-issues)+- [Reporting bugs](#reporting-bugs)+- [Contributing](#contributing)+- [Copyright](#copyright)+ ## How to use-There are a variety of ways to use ShellCheck! +There are a number of ways to use ShellCheck! -#### On the web+### On the web+ Paste a shell script on http://www.shellcheck.net for instant feedback. -[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends!+[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends! +### From your terminal -#### From your terminal Run `shellcheck yourscript` in your terminal for instant output, as seen above. --#### In your editor+### In your editor You can see ShellCheck suggestions directly in a variety of editors. -* Vim, through [ALE](https://github.com/w0rp/ale) or [Syntastic](https://github.com/scrooloose/syntastic):+* Vim, through [ALE](https://github.com/w0rp/ale), [Neomake](https://github.com/neomake/neomake), or [Syntastic](https://github.com/scrooloose/syntastic): . @@ -48,28 +76,16 @@ * Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck). +* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).+ * Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats). +### In your build or test suites -#### In your build or test suites While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites. ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation. -## Travis CI Setup--If you want to use ShellCheck in Travis CI, setting it up is simple :tada:.--```yml-language: bash-addons:- apt:- sources:- - debian-sid # Grab ShellCheck from the Debian repo- packages:- - shellcheck-```- ## Installing The easiest way to install ShellCheck locally is through your package manager.@@ -79,10 +95,19 @@ cabal update cabal install ShellCheck +On systems with Stack (installs to `~/.local/bin`):++ stack update+ stack install ShellCheck+ On Debian based distros: apt-get install shellcheck +On Arch Linux based distros:++ pacman -S shellcheck+ On Gentoo based distros: emerge --ask shellcheck@@ -110,48 +135,92 @@ On other openSUSE distributions: -add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell+Add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo zypper in ShellCheck -or use OneClickInstall - https://software.opensuse.org/package/ShellCheck+Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck +On Solus:++ eopkg install shellcheck+ From Docker Hub: - docker pull koalaman/shellcheck+```sh+docker pull koalaman/shellcheck:latest # Or :v0.4.6 for a release version+docker run -v "$PWD:/mnt" koalaman/shellcheck myscript+``` +or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend.++Alternatively, get freshly built binaries for the latest commit here:++* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-latest.linux.x86_64.tar.xz) (statically linked)+* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-latest.zip)++or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums and release builds.++## Travis CI Setup++If you want to use ShellCheck in Travis CI, you can most easily install it via `apt`:++```yml+language: bash+addons:+ apt:+ sources:+ - debian-sid # Grab ShellCheck from the Debian repo+ packages:+ - shellcheck+```++ ## Compiling from source This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile. --#### Installing Cabal+### Installing Cabal ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`). +On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.++ brew install cask+ brew cask install haskell-platform+ cabal install cabal-install+ On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/ Verify that `cabal` is installed and update its dependency list with $ cabal update -#### Compiling ShellCheck+### Compiling ShellCheck `git clone` this repository, and `cd` to the ShellCheck source directory to build/install: $ cabal install +Or if you intend to run the tests:++ $ cabal install --enable-tests+ This will compile ShellCheck and install it to your `~/.cabal/bin` directory. Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): - export PATH="$HOME/.cabal/bin:$PATH"+```sh+export PATH="$HOME/.cabal/bin:$PATH"+``` Log out and in again, and verify that your PATH is set up correctly: - $ which shellcheck- ~/.cabal/bin/shellcheck+```sh+$ which shellcheck+~/.cabal/bin/shellcheck+``` On native Windows, the `PATH` should already be set up, but the system may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,@@ -165,155 +234,161 @@ > [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -#### Running tests+### Running tests To run the unit test suite: $ cabal test - ## Gallery of bad code+ So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues. -#### Quoting+### Quoting ShellCheck can recognize several types of incorrect quoting: - echo $1 # Unquoted variables- find . -name *.ogg # Unquoted find/grep patterns- rm "~/my file.txt" # Quoted tilde expansion- v='--verbose="true"'; cmd $v # Literal quotes in variables- for f in "*.ogg" # Incorrectly quoted 'for' loops- touch $@ # Unquoted $@- echo 'Don't forget to restart!' # Singlequote closed by apostrophe- echo 'Don\'t try this at home' # Attempting to escape ' in ''- echo 'Path is $PATH' # Variables in single quotes- trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap-+```sh+echo $1 # Unquoted variables+find . -name *.ogg # Unquoted find/grep patterns+rm "~/my file.txt" # Quoted tilde expansion+v='--verbose="true"'; cmd $v # Literal quotes in variables+for f in "*.ogg" # Incorrectly quoted 'for' loops+touch $@ # Unquoted $@+echo 'Don't forget to restart!' # Singlequote closed by apostrophe+echo 'Don\'t try this at home' # Attempting to escape ' in ''+echo 'Path is $PATH' # Variables in single quotes+trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap+``` -#### Conditionals+### Conditionals ShellCheck can recognize many types of incorrect test statements. - [[ n != 0 ]] # Constant test expressions- [[ -e *.mpg ]] # Existence checks of globs- [[ $foo==0 ]] # Always true due to missing spaces- [[ -n "$foo " ]] # Always true due to literals- [[ $foo =~ "fo+" ]] # Quoted regex in =~- [ foo =~ re ] # Unsupported [ ] operators- [ $1 -eq "shellcheck" ] # Numerical comparison of strings- [ $n && $m ] # && in [ .. ]- [ grep -q foo file ] # Command without $(..)- [[ "$$file" == *.jpg ]] # Comparisons that can't succeed- (( 1 -lt 2 )) # Using test operators in ((..))-+```sh+[[ n != 0 ]] # Constant test expressions+[[ -e *.mpg ]] # Existence checks of globs+[[ $foo==0 ]] # Always true due to missing spaces+[[ -n "$foo " ]] # Always true due to literals+[[ $foo =~ "fo+" ]] # Quoted regex in =~+[ foo =~ re ] # Unsupported [ ] operators+[ $1 -eq "shellcheck" ] # Numerical comparison of strings+[ $n && $m ] # && in [ .. ]+[ grep -q foo file ] # Command without $(..)+[[ "$$file" == *.jpg ]] # Comparisons that can't succeed+(( 1 -lt 2 )) # Using test operators in ((..))+``` -#### Frequently misused commands+### Frequently misused commands ShellCheck can recognize instances where commands are used incorrectly: - grep '*foo*' file # Globs in regex contexts- find . -exec foo {} && bar {} \; # Prematurely terminated find -exec- sudo echo 'Var=42' > /etc/profile # Redirecting sudo- time --format=%s sleep 10 # Passing time(1) flags to time builtin- while read h; do ssh "$h" uptime # Commands eating while loop input- alias archive='mv $1 /backup' # Defining aliases with arguments- tr -cd '[a-zA-Z0-9]' # [] around ranges in tr- exec foo; echo "Done!" # Misused 'exec'- find -name \*.bak -o -name \*~ -delete # Implicit precedence in find- f() { whoami; }; sudo f # External use of internal functions-+```sh+grep '*foo*' file # Globs in regex contexts+find . -exec foo {} && bar {} \; # Prematurely terminated find -exec+sudo echo 'Var=42' > /etc/profile # Redirecting sudo+time --format=%s sleep 10 # Passing time(1) flags to time builtin+while read h; do ssh "$h" uptime # Commands eating while loop input+alias archive='mv $1 /backup' # Defining aliases with arguments+tr -cd '[a-zA-Z0-9]' # [] around ranges in tr+exec foo; echo "Done!" # Misused 'exec'+find -name \*.bak -o -name \*~ -delete # Implicit precedence in find+f() { whoami; }; sudo f # External use of internal functions+``` -#### Common beginner's mistakes+### Common beginner's mistakes ShellCheck recognizes many common beginner's syntax errors: - var = 42 # Spaces around = in assignments- $foo=42 # $ in assignments- for $var in *; do ... # $ in for loop variables- var$n="Hello" # Wrong indirect assignment- echo ${var$n} # Wrong indirect reference- var=(1, 2, 3) # Comma separated arrays- array=( [index] = value ) # Incorrect index initialization- echo "Argument 10 is $10" # Positional parameter misreference- if $(myfunction); then ..; fi # Wrapping commands in $()- else if othercondition; then .. # Using 'else if'- -+```sh+var = 42 # Spaces around = in assignments+$foo=42 # $ in assignments+for $var in *; do ... # $ in for loop variables+var$n="Hello" # Wrong indirect assignment+echo ${var$n} # Wrong indirect reference+var=(1, 2, 3) # Comma separated arrays+array=( [index] = value ) # Incorrect index initialization+echo "Argument 10 is $10" # Positional parameter misreference+if $(myfunction); then ..; fi # Wrapping commands in $()+else if othercondition; then .. # Using 'else if'+``` -#### Style+### Style ShellCheck can make suggestions to improve style: - [[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead- a >> log; b >> log; c >> log # Use a redirection block instead- echo "The time is `date`" # Use $() instead- cd dir; process *; cd ..; # Use subshells instead- echo $[1+2] # Use standard $((..)) instead of old $[]- echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))- echo "$(date)" # Useless use of echo- cat file | grep foo # Useless use of cat-+```sh+[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead+a >> log; b >> log; c >> log # Use a redirection block instead+echo "The time is `date`" # Use $() instead+cd dir; process *; cd ..; # Use subshells instead+echo $[1+2] # Use standard $((..)) instead of old $[]+echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))+echo "$(date)" # Useless use of echo+cat file | grep foo # Useless use of cat+``` -#### Data and typing errors+### Data and typing errors ShellCheck can recognize issues related to data and typing: - args="$@" # Assigning arrays to strings- files=(foo bar); echo "$files" # Referencing arrays as strings- declare -A arr=(foo bar) # Associative arrays without index- printf "%s\n" "Arguments: $@." # Concatenating strings and arrays- [[ $# > 2 ]] # Comparing numbers as strings- var=World; echo "Hello " var # Unused lowercase variables- echo "Hello $name" # Unassigned lowercase variables- cmd | read bar; echo $bar # Assignments in subshells-+```sh+args="$@" # Assigning arrays to strings+files=(foo bar); echo "$files" # Referencing arrays as strings+declare -A arr=(foo bar) # Associative arrays without index+printf "%s\n" "Arguments: $@." # Concatenating strings and arrays+[[ $# > 2 ]] # Comparing numbers as strings+var=World; echo "Hello " var # Unused lowercase variables+echo "Hello $name" # Unassigned lowercase variables+cmd | read bar; echo $bar # Assignments in subshells+``` -#### Robustness+### Robustness ShellCheck can make suggestions for improving the robustness of a script: - rm -rf "$STEAMROOT/"* # Catastrophic rm- touch ./-l; ls * # Globs that could become options- find . -exec sh -c 'a && b {}' \; # Find -exec shell injection- printf "Hello $name" # Variables in printf format- for f in $(ls *.txt); do # Iterating over ls output- export MYVAR=$(cmd) # Masked exit codes-+```sh+rm -rf "$STEAMROOT/"* # Catastrophic rm+touch ./-l; ls * # Globs that could become options+find . -exec sh -c 'a && b {}' \; # Find -exec shell injection+printf "Hello $name" # Variables in printf format+for f in $(ls *.txt); do # Iterating over ls output+export MYVAR=$(cmd) # Masked exit codes+``` -#### Portability+### Portability ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`: -- echo {1..$n} # Works in ksh, but not bash/dash/sh- echo {1..10} # Works in ksh and bash, but not dash/sh- echo -n 42 # Works in ksh, bash and dash, undefined in sh- trap 'exit 42' sigint # Unportable signal spec- cmd &> file # Unportable redirection operator- read foo < /dev/tcp/host/22 # Unportable intercepted files- foo-bar() { ..; } # Undefined/unsupported function name- [ $UID = 0 ] # Variable undefined in dash/sh- local var=value # local is undefined in sh- time sleep 1 | sleep 5 # Undefined uses of 'time'-+```sh+echo {1..$n} # Works in ksh, but not bash/dash/sh+echo {1..10} # Works in ksh and bash, but not dash/sh+echo -n 42 # Works in ksh, bash and dash, undefined in sh+trap 'exit 42' sigint # Unportable signal spec+cmd &> file # Unportable redirection operator+read foo < /dev/tcp/host/22 # Unportable intercepted files+foo-bar() { ..; } # Undefined/unsupported function name+[ $UID = 0 ] # Variable undefined in dash/sh+local var=value # local is undefined in sh+time sleep 1 | sleep 5 # Undefined uses of 'time'+``` -#### Miscellaneous+### Miscellaneous ShellCheck recognizes a menagerie of other issues: - PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]- PATH="$PATH:~/bin" # Literal tilde in $PATH- rm “file” # Unicode quotes- echo "Hello world" # Carriage return / DOS line endings- echo hello \ # Trailing spaces after \- var=42 echo $var # Expansion of inlined environment- #!/bin/bash -x -e # Common shebang errors- echo $((n/180*100)) # Unnecessary loss of precision- ls *[:digit:].txt # Bad character class globs- sed 's/foo/bar/' file > file # Redirecting to input--+```sh+PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]+PATH="$PATH:~/bin" # Literal tilde in $PATH+rm “file” # Unicode quotes+echo "Hello world" # Carriage return / DOS line endings+echo hello \ # Trailing spaces after \+var=42 echo $var # Expansion of inlined environment+#!/bin/bash -x -e # Common shebang errors+echo $((n/180*100)) # Unnecessary loss of precision+ls *[:digit:].txt # Bad character class globs+sed 's/foo/bar/' file > file # Redirecting to input+``` ## Testimonials @@ -334,14 +409,14 @@ https://github.com/koalaman/shellcheck/issues - ## Contributing -Please submit patches to code or documentation as GitHub pull requests!+Please submit patches to code or documentation as GitHub pull requests! Check+out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the+ShellCheck Wiki. Contributions must be licensed under the GNU GPLv3. The contributor retains the copyright.- ## Copyright
ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name: ShellCheck-Version: 0.4.6+Version: 0.4.7 Synopsis: Shell script analysis tool License: GPL-3 License-file: LICENSE@@ -70,7 +70,6 @@ executable shellcheck build-depends:- ShellCheck, base >= 4 && < 5, containers, directory,@@ -84,7 +83,6 @@ test-suite test-shellcheck type: exitcode-stdio-1.0 build-depends:- ShellCheck, base >= 4 && < 5, containers, directory,
ShellCheck/AST.hs view
@@ -19,21 +19,21 @@ -} module ShellCheck.AST where -import Control.Monad import Control.Monad.Identity import Text.Parsec import qualified ShellCheck.Regex as Re+import Prelude hiding (id) -data Id = Id Int deriving (Show, Eq, Ord)+newtype Id = Id Int deriving (Show, Eq, Ord) data Quoted = Quoted | Unquoted deriving (Show, Eq) data Dashed = Dashed | Undashed deriving (Show, Eq) data AssignmentMode = Assign | Append deriving (Show, Eq)-data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)-data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)+newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)+newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq) data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq) -data Root = Root Token+newtype Root = Root Token data Token = TA_Binary Id String Token Token | TA_Assignment Id String Token Token@@ -48,8 +48,9 @@ | 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_AndIf Id Token Token | T_Arithmetic Id Token | T_Array Id [Token] | T_IndexedElement Id [Token] Token@@ -110,7 +111,7 @@ | T_NEWLINE Id | T_NormalWord Id [Token] | T_OR_IF Id- | T_OrIf Id (Token) (Token)+ | 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]@@ -162,11 +163,6 @@ i newT roundAll = mapM round - roundMaybe Nothing = return Nothing- roundMaybe (Just v) = do- s <- round v- return (Just s)- dl l v = do x <- roundAll l return $ v x@@ -277,6 +273,7 @@ delve (T_Include id includer script) = d2 includer script $ T_Include id delve t = return t +getId :: Token -> Id getId t = case t of T_AND_IF id -> id T_OR_IF id -> id@@ -376,10 +373,14 @@ T_CoProcBody id _ -> id T_Include id _ _ -> id T_UnparsedIndex id _ _ -> id+ TC_Empty id _ -> id blank :: Monad m => Token -> m () blank = const $ return ()+doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token doAnalysis f = analyze f blank return+doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token doStackAnalysis startToken endToken = analyze startToken endToken return+doTransform :: (Token -> Token) -> Token -> Token doTransform i = runIdentity . analyze blank blank (return . i)
ShellCheck/ASTLib.hs view
@@ -48,8 +48,8 @@ T_NormalWord _ l -> any willSplit l _ -> False -isGlob (T_Extglob {}) = True-isGlob (T_Glob {}) = True+isGlob T_Extglob {} = True+isGlob T_Glob {} = True isGlob (T_NormalWord _ l) = any isGlob l isGlob _ = False @@ -119,7 +119,17 @@ -- Check if a command has a flag. hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd) +-- Is this token a word that starts with a dash?+isFlag token =+ case getWordParts token of+ T_Literal _ ('-':_) : _ -> True+ _ -> False +-- Is this token a flag where the - is unquoted?+isUnquotedFlag token = fromMaybe False $ do+ str <- getLeadingUnquotedString token+ return $ "-" `isPrefixOf` str+ -- Given a T_DollarBraced, return a simplified version of the string contents. bracedString (T_DollarBraced _ l) = concat $ oversimplify l bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"@@ -144,9 +154,9 @@ -- Is it certain that this word will becomes multiple words? willBecomeMultipleArgs t = willConcatInAssignment t || f t where- f (T_Extglob {}) = True- f (T_Glob {}) = True- f (T_BraceExpansion {}) = True+ f T_Extglob {} = True+ f T_Glob {} = True+ f T_BraceExpansion {} = True f (T_DoubleQuoted _ parts) = any f parts f (T_NormalWord _ parts) = any f parts f _ = False@@ -154,7 +164,7 @@ -- This does token cause implicit concatenation in assignments? willConcatInAssignment token = case token of- t@(T_DollarBraced {}) -> isArrayExpansion t+ t@T_DollarBraced {} -> isArrayExpansion t (T_DoubleQuoted _ parts) -> any willConcatInAssignment parts (T_NormalWord _ parts) -> any willConcatInAssignment parts _ -> False@@ -169,7 +179,7 @@ -- Maybe get a literal string, but only if it's an unquoted argument. getUnquotedLiteral (T_NormalWord _ list) =- liftM concat $ mapM str list+ concat <$> mapM str list where str (T_Literal _ s) = return s str _ = Nothing@@ -186,9 +196,16 @@ where from t = case t of- (T_Literal {}) -> return t+ T_Literal {} -> return t _ -> Nothing +-- Get the leading, unquoted, literal string of a token (if any).+getLeadingUnquotedString :: Token -> Maybe String+getLeadingUnquotedString t =+ case t of+ T_NormalWord _ ((T_Literal _ s) : _) -> return s+ _ -> Nothing+ -- Maybe get the literal string of this token and any globs in it. getGlobOrLiteralString = getLiteralStringExt f where@@ -200,7 +217,7 @@ getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String getLiteralStringExt more = g where- allInList = liftM concat . mapM g+ allInList = fmap concat . mapM g g (T_DoubleQuoted _ l) = allInList l g (T_DollarDoubleQuoted _ l) = allInList l g (T_NormalWord _ l) = allInList l@@ -237,7 +254,7 @@ T_Redirecting _ _ w -> getCommand w T_SimpleCommand _ _ (w:_) -> return t T_Annotation _ _ t -> getCommand t- otherwise -> Nothing+ _ -> Nothing -- Maybe get the command name of a token representing a command getCommandName t = do@@ -259,13 +276,13 @@ T_DollarExpansion _ [c] -> extract c T_Backticked _ [c] -> extract c T_DollarBraceCommandExpansion _ [c] -> extract c- otherwise -> Nothing+ _ -> Nothing where extract (T_Pipeline _ _ [cmd]) = getCommandName cmd extract _ = Nothing -- Get the basename of a token representing a command-getCommandBasename = liftM basename . getCommandName+getCommandBasename = fmap basename . getCommandName where basename = reverse . takeWhile (/= '/') . reverse @@ -275,7 +292,7 @@ T_SimpleCommand _ (w:_) [] -> True T_Assignment {} -> True T_Annotation _ _ w -> isAssignment w- otherwise -> False+ _ -> False isOnlyRedirection t = case t of@@ -283,7 +300,7 @@ T_Annotation _ _ w -> isOnlyRedirection w T_Redirecting _ (_:_) c -> isOnlyRedirection c T_SimpleCommand _ [] [] -> True- otherwise -> False+ _ -> False isFunction t = case t of T_Function {} -> True; _ -> False @@ -291,6 +308,7 @@ -- Get the lists of commands from tokens that contain them, such as -- the body of while loops or branches of if statements.+getCommandSequences :: Token -> [[Token]] getCommandSequences t = case t of T_Script _ _ cmds -> [cmds]@@ -301,16 +319,18 @@ T_ForIn _ _ _ cmds -> [cmds] T_ForArithmetic _ _ _ _ cmds -> [cmds] T_IfExpression _ thens elses -> map snd thens ++ [elses]- otherwise -> []+ T_Annotation _ _ t -> getCommandSequences t+ _ -> [] -- Get a list of names of associative arrays getAssociativeArrays t = nub . execWriter $ doAnalysis f t where f :: Token -> Writer [String] ()- f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do+ f t@T_SimpleCommand {} = fromMaybe (return ()) $ do name <- getCommandName t- guard $ name == "declare" || name == "typeset"+ let assocNames = ["declare","local","typeset"]+ guard $ elem name assocNames let flags = getAllFlags t guard $ elem "A" $ map snd flags let args = map fst . filter ((==) "" . snd) $ flags@@ -321,7 +341,7 @@ nameAssignments t = case t of T_Assignment _ _ name _ _ -> return name- otherwise -> Nothing+ _ -> Nothing -- A Pseudoglob is a wildcard pattern used for checking if a match can succeed. -- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which@@ -333,7 +353,7 @@ -- PGMany. wordToPseudoGlob :: Token -> Maybe [PseudoGlob] wordToPseudoGlob word =- simplifyPseudoGlob <$> concat <$> mapM f (getWordParts word)+ simplifyPseudoGlob . concat <$> mapM f (getWordParts word) where f x = case x of T_Literal _ s -> return $ map PGChar s@@ -351,6 +371,19 @@ _ -> return [PGMany] +-- Turn a word into a PG pattern, but only if we can preserve+-- exact semantics.+wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]+wordToExactPseudoGlob word =+ simplifyPseudoGlob . concat <$> mapM f (getWordParts word)+ where+ f x = case x of+ T_Literal _ s -> return $ map PGChar s+ T_SingleQuoted _ s -> return $ map PGChar s+ T_Glob _ "?" -> return [PGAny]+ T_Glob _ "*" -> return [PGMany]+ _ -> fail "Unknown token type"+ -- Reorder a PseudoGlob for more efficient matching, e.g. -- f?*?**g -> f??*g simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]@@ -381,6 +414,23 @@ matchable (PGMany : rest) [] = matchable rest [] matchable (_:_) [] = False matchable [] r = matchable r []++-- Check whether the first pattern always overlaps the second.+pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool+pseudoGlobIsSuperSetof = matchable+ where+ matchable x@(xf:xs) y@(yf:ys) =+ case (xf, yf) of+ (PGMany, PGMany) -> matchable x ys+ (PGMany, _) -> matchable x ys || matchable xs y+ (_, PGMany) -> False+ (PGAny, _) -> matchable xs ys+ (_, PGAny) -> False+ (_, _) -> xf == yf && matchable xs ys++ matchable [] [] = True+ matchable (PGMany : rest) [] = matchable rest []+ matchable _ _ = False wordsCanBeEqual x y = fromMaybe True $ liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
ShellCheck/Analytics.hs view
@@ -41,7 +41,7 @@ import Data.Maybe import Data.Ord import Debug.Trace-import qualified Data.Map as Map+import qualified Data.Map.Strict as Map import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) @@ -61,8 +61,9 @@ ,checkArrayWithoutIndex ,checkShebang ,checkUnassignedReferences- ,checkUncheckedCd+ ,checkUncheckedCdPushdPopd ,checkArrayAssignmentIndices+ ,checkUseBeforeDefinition ] runAnalytics :: AnalysisSpec -> [TokenComment]@@ -141,7 +142,6 @@ ,checkWrongArithmeticAssignment ,checkConditionalAndOrs ,checkFunctionDeclarations- ,checkCatastrophicRm ,checkStderrPipe ,checkOverridingPath ,checkArrayAsString@@ -160,6 +160,12 @@ ,checkRedirectedNowhere ,checkUnmatchableCases ,checkSubshellAsTest+ ,checkSplittingInArrays+ ,checkRedirectionToNumber+ ,checkGlobAsCommand+ ,checkFlagAsCommand+ ,checkEmptyCondition+ ,checkPipeToNowhere ] @@ -255,15 +261,25 @@ prop_checkAssignAteCommand2 = verify checkAssignAteCommand "A=ls --sort=$foo" prop_checkAssignAteCommand3 = verify checkAssignAteCommand "A=cat foo | grep bar" prop_checkAssignAteCommand4 = verifyNot checkAssignAteCommand "A=foo ls -l"-prop_checkAssignAteCommand5 = verifyNot checkAssignAteCommand "PAGER=cat grep bar"-checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) (firstWord:_)) =- when ("-" `isPrefixOf` concat (oversimplify firstWord) ||- isCommonCommand (getLiteralString assignmentTerm)- && not (isCommonCommand (getLiteralString firstWord))) $- warn id 2037 "To assign the output of a command, use var=$(cmd) ."+prop_checkAssignAteCommand5 = verify checkAssignAteCommand "PAGER=cat grep bar"+prop_checkAssignAteCommand6 = verifyNot checkAssignAteCommand "PAGER=\"cat\" grep bar"+prop_checkAssignAteCommand7 = verify checkAssignAteCommand "here=pwd"+checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) list) =+ -- Check if first word is intended as an argument (flag or glob).+ if firstWordIsArg list+ then+ err id 2037 "To assign the output of a command, use var=$(cmd) ."+ else+ -- Check if it's a known, unquoted command name.+ when (isCommonCommand $ getUnquotedLiteral assignmentTerm) $+ warn id 2209 "Use var=$(command) to assign output (or quote to assign string)." where isCommonCommand (Just s) = s `elem` commonCommands isCommonCommand _ = False+ firstWordIsArg list = fromMaybe False $ do+ head <- list !!! 0+ return $ isGlob head || isUnquotedFlag head+ checkAssignAteCommand _ _ = return () prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1"@@ -349,21 +365,18 @@ ]) $ warn (getId find) 2038 "Use -print0/-0 or -exec + to allow for non-alphanumeric filenames." - for ["?", "echo"] $- \(_:echo:_) -> info (getId echo) 2008 "echo doesn't read from stdin, are you sure you should be piping to it?"- for' ["ps", "grep"] $ \x -> info x 2009 "Consider using pgrep instead of grepping ps output." for ["grep", "wc"] $ \(grep:wc:_) ->- let flagsGrep = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep- flagsWc = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand wc+ let flagsGrep = fromMaybe [] $ map snd . getAllFlags <$> getCommand grep+ flagsWc = fromMaybe [] $ 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) || ((length flagsWc) == 0)) $+ 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." - didLs <- liftM or . sequence $ [+ didLs <- fmap or . sequence $ [ for' ["ls", "grep"] $ \x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.", for' ["ls", "xargs"] $@@ -439,7 +452,7 @@ 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- || (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $+ || (fmap wouldHaveBeenGlob (getLiteralString word) == Just True)) $ err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once." checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) = warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . "@@ -622,13 +635,11 @@ prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done" prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"+prop_checkDollarStar3 = verifyNot checkDollarStar "[[ $* = 'a b' ]]" checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _)]) | bracedString b == "*" =- unless isAssigned $+ unless (isStrictlyQuoteFree (parentMap p) t) $ warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."- where- path = getPath (parentMap p) t- isAssigned = any isAssignment . take 2 $ path checkDollarStar _ _ = return () @@ -642,15 +653,12 @@ prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done" prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\"" 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 ->- unless (isAlternative x) $+ unless (isQuotedAlternativeReference x) $ err (getId x) 2068 "Double quote array expansions to avoid re-splitting elements."- where- -- Fixme: should detect whether the alternative is quoted- isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b- isAlternative _ = False checkUnquotedDollarAt _ _ = return () prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\""@@ -658,7 +666,7 @@ prop_checkConcatenatedDollarAt3 = verify checkConcatenatedDollarAt "echo $a$@" prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@" prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\""-checkConcatenatedDollarAt p word@(T_NormalWord {})+checkConcatenatedDollarAt p word@T_NormalWord {} | not $ isQuoteFree (parentMap p) word = unless (null $ drop 1 parts) $ mapM_ for array@@ -777,6 +785,7 @@ prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'" prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'" prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"+prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]" checkSingleQuotedVariables params t@(T_SingleQuoted id s) = when (s `matches` re) $ if "sed" == commandName@@ -814,6 +823,7 @@ isOkAssignment t = case t of T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted+ TC_Unary _ _ "-v" _ -> True _ -> False re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`"@@ -848,32 +858,41 @@ prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]" prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]" prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"-prop_checkNumberComparisons11= verify checkNumberComparisons "[ $foo -eq 'N' ]"-prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]"+prop_checkNumberComparisons11 = verify checkNumberComparisons "[ $foo -eq 'N' ]"+prop_checkNumberComparisons12 = verify checkNumberComparisons "[ x$foo -gt x${N} ]"+prop_checkNumberComparisons13 = verify checkNumberComparisons "[ $foo > $bar ]"+prop_checkNumberComparisons14 = verifyNot checkNumberComparisons "[[ foo < bar ]]"+prop_checkNumberComparisons15 = verifyNot checkNumberComparisons "[ $foo '>' $bar ]" checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do- if isNum lhs && not (isNonNum rhs)- || isNum rhs && not (isNonNum lhs)+ if isNum lhs || isNum rhs then do when (isLtGt op) $ err id 2071 $ op ++ " is for string comparisons. Use " ++ eqv op ++ " instead."- when (isLeGe op) $+ when (isLeGe op && hasStringComparison) $ err id 2071 $ op ++ " is not a valid operator. " ++ "Use " ++ eqv op ++ " ." else do when (isLeGe op || isLtGt op) $ mapM_ checkDecimals [lhs, rhs] - when (isLeGe op) $+ when (isLeGe op && hasStringComparison) $ err id 2122 $ op ++ " is not a valid operator. " ++- "Use '! a " ++ invert op ++ " b' instead."+ "Use '! a " ++ esc ++ invert op ++ " b' instead." + when (typ == SingleBracket && op `elem` ["<", ">"]) $+ case shellType params of+ Sh -> return () -- These are unsupported and will be caught by bashism checks.+ 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 mapM_ checkDecimals [lhs, rhs] when (typ == SingleBracket) $ checkStrings [lhs, rhs] where+ hasStringComparison = shellType params /= Sh isLtGt = flip elem ["<", "\\<", ">", "\\>"] isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="] @@ -883,8 +902,8 @@ decimalError = "Decimals are not supported. " ++ "Either use integers only, or use bc or awk to compare." - checkStrings hs =- mapM_ stringError . take 1 . filter isNonNum $ hs+ checkStrings =+ mapM_ stringError . take 1 . filter isNonNum isNonNum t = fromMaybe False $ do s <- getLiteralStringExt (const $ return "") t@@ -923,26 +942,19 @@ invert "<=" = ">" invert ">=" = "<" - floatRegex = mkRegex "^[0-9]+\\.[0-9]+$"+ floatRegex = mkRegex "^[-+]?[0-9]+\\.[0-9]+$" checkNumberComparisons _ _ = return () prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]"-prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]"-prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]"-prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done"-prop_checkSingleBracketOperators6 = verifyNot checkSingleBracketOperators "[ $foo '>' $bar ]"-checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)- | typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] =- err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]."-checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)- | typ == SingleBracket && op == "=~" =- err id 2074 $ "Can't use " ++ op ++" in [ ]. Use [[..]] instead."+checkSingleBracketOperators params (TC_Binary id SingleBracket "=~" lhs rhs) =+ when (shellType params `elem` [Bash, Ksh]) $+ err id 2074 $ "Can't use =~ in [ ]. Use [[..]] instead." checkSingleBracketOperators _ _ = return () prop_checkDoubleBracketOperators1 = verify checkDoubleBracketOperators "[[ 3 \\< 4 ]]" prop_checkDoubleBracketOperators3 = verifyNot checkDoubleBracketOperators "[[ foo < bar ]]" checkDoubleBracketOperators _ x@(TC_Binary id typ op lhs rhs)- | typ == DoubleBracket && op `elem` ["\\<", "\\>", "\\<=", "\\>="] =+ | typ == DoubleBracket && op `elem` ["\\<", "\\>"] = err id 2075 $ "Escaping " ++ op ++" is required in [..], but invalid in [[..]]" checkDoubleBracketOperators _ _ = return () @@ -967,7 +979,7 @@ (TC_Or id SingleBracket "-o" _ _) -> warn id 2166 "Prefer [ p ] || [ q ] as [ p -o q ] is not well defined." - otherwise -> return ()+ _ -> return () prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]" prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]"@@ -1115,11 +1127,13 @@ prop_checkArithmeticDeref11= verifyNot checkArithmeticDeref "a[$foo]=wee" prop_checkArithmeticDeref12= verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done" prop_checkArithmeticDeref13= verifyNot checkArithmeticDeref "(( $$ ))"+prop_checkArithmeticDeref14= verifyNot checkArithmeticDeref "(( $! ))"+prop_checkArithmeticDeref15= verifyNot checkArithmeticDeref "(( ${!var} ))" checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) = unless (isException $ bracedString b) getWarning where isException [] = True- isException s = any (`elem` "/.:#%?*@$-") s || isDigit (head s)+ isException s = any (`elem` "/.:#%?*@$-!") s || isDigit (head s) getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t warningFor t = case t of@@ -1238,7 +1252,7 @@ unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $ when (all couldBeOptimized vars) $ style id 2116 "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."- otherwise -> return ()+ _ -> return () prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"@@ -1254,12 +1268,12 @@ suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors, case t of -- and >> and similar redirections because these are probably not comparisons. T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op- otherwise -> False+ _ -> False isComparison t = case t of T_Greater _ -> True T_Less _ -> True- otherwise -> False+ _ -> False checkTestRedirects _ _ = return () prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"@@ -1350,10 +1364,11 @@ prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\"" prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\"" prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}"+prop_checkInexplicablyUnquoted8 = verifyNot checkInexplicablyUnquoted " 'foo'\\\n 'bar'" checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens) where check (T_SingleQuoted _ _:T_Literal id str:_)- | all isAlphaNum str =+ | not (null str) && all isAlphaNum str = info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? " check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) =@@ -1502,10 +1517,11 @@ prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@" prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar" prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n"+prop_subshellAssignmentCheck19 = verifyNotTree subshellAssignmentCheck "#!/bin/bash\nshopt -s lastpipe; echo a | read -r b; echo \"$b\"" subshellAssignmentCheck params t = let flow = variableFlow params check = findSubshelled flow [("oops",[])] Map.empty- in snd $ runWriter check+ in execWriter check findSubshelled [] _ _ = return ()@@ -1586,6 +1602,7 @@ prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]" prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done" prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"+prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}" checkSpacefulness params t = doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)@@ -1604,9 +1621,9 @@ return [makeComment InfoC (getId token) 2086 warning | isExpansion token && spaced && not (isArrayExpansion token) -- There's another warning for this- && not (isCounting token)+ && not (isCountingReference token) && not (isQuoteFree parents token)- && not (isQuotedAlternative token)+ && not (isQuotedAlternativeReference token) && not (usedAsCommandName parents token)] where warning = "Double quote to prevent globbing and word splitting."@@ -1629,19 +1646,6 @@ (T_DollarBraced _ _ ) -> True _ -> False - isCounting (T_DollarBraced id token) =- case concat $ oversimplify token of- '#':_ -> True- _ -> False- isCounting _ = False-- -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}- isQuotedAlternative t =- case t of- T_DollarBraced _ _ ->- ":+" `isInfixOf` bracedString t- _ -> False- isSpacefulWord :: (String -> Bool) -> [Token] -> Bool isSpacefulWord f = any (isSpaceful f) isSpaceful :: (String -> Bool) -> Token -> Bool@@ -1675,7 +1679,7 @@ checkQuotesInLiterals params t = doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) where- getQuotes name = liftM (Map.lookup name) get+ getQuotes name = fmap (Map.lookup name) get setQuotes name ref = modify $ Map.insert name ref deleteQuotes = modify . Map.delete parents = parentMap params@@ -1706,7 +1710,7 @@ squashesQuotes t = case t of T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t- otherwise -> False+ _ -> False readF _ expr name = do assignment <- getQuotes name@@ -1805,6 +1809,8 @@ prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]" prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t" prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"+prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"+prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-" checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) where flow = variableFlow params@@ -1855,6 +1861,10 @@ prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b" prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}" prop_checkUnassignedReferences28= verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n"+prop_checkUnassignedReferences29= verifyNotTree checkUnassignedReferences "if [[ -v foo ]]; then echo $foo; fi"+prop_checkUnassignedReferences30= verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"+prop_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"+prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi" checkUnassignedReferences params t = warnings where (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)@@ -1863,7 +1873,7 @@ tally (Assignment (_, _, name, _)) = modify (\(read, written) -> (read, Map.insert name () written)) tally (Reference (_, place, name)) =- modify (\(read, written) -> (Map.insertWith' (const id) name place read, written))+ modify (\(read, written) -> (Map.insertWith (const id) name place read, written)) tally _ = return () unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned@@ -1909,7 +1919,7 @@ -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these. isInArray var t = any isArray $ getPath (parentMap params) t where- isArray (T_Array {}) = True+ isArray T_Array {} = True isArray b@(T_DollarBraced _ _) | var /= getBracedReference (bracedString b) = True isArray _ = False @@ -1929,8 +1939,9 @@ prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt" prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*" prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"+prop_checkGlobsAsOptions4 = verifyNot checkGlobsAsOptions "*.txt" checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =- mapM_ check $ takeWhile (not . isEndOfArgs) args+ mapM_ check $ takeWhile (not . isEndOfArgs) (drop 1 args) where check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" = info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options."@@ -2007,7 +2018,7 @@ check (t:rest) = case t of T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars- otherwise -> check rest+ _ -> check rest checkVar (T_Assignment aId mode aName [] value) | aName == name && (aId `notElem` idPath) = do warn aId 2097 "This assignment is only seen by the forked process."@@ -2092,7 +2103,7 @@ where name = getCommandName t path = let p = getPath (parentMap params) t in filter relevant p- subshellType t = case leadType (shellType params) (parentMap params) t of+ subshellType t = case leadType params t of NoneScope -> Nothing SubshellScope str -> return str relevant t = isLoop t || isFunction t || isJust (subshellType t)@@ -2121,73 +2132,7 @@ checkFunctionDeclarations _ _ = return () -prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"-prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"-prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"-prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"-prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"-prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"-prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"-prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"-prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"-prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"-prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"-prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"-prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"-prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"-checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =- when (any isRecursiveFlag simpleArgs) $- mapM_ (mapM_ checkWord . braceExpand) tokens- where- simpleArgs = oversimplify t - checkWord token =- case getLiteralString token of- Just str ->- when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $- warn (getId token) 2114 "Warning: deletes a system directory. Use 'rm --' to disable this message."- Nothing ->- checkWord' token-- checkWord' token = fromMaybe (return ()) $ do- filename <- getPotentialPath token- let path = fixPath filename- return . when (path `elem` importantPaths) $- warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."-- fixPath filename =- let normalized = skipRepeating '/' . skipRepeating '*' $ filename in- if normalized == "/" then normalized else stripTrailing '/' normalized-- getPotentialPath = getLiteralStringExt f- where- f (T_Glob _ str) = return str- f (T_DollarBraced _ word) =- let var = onlyLiteralString word in- if any (flip isInfixOf var) [":?", ":-", ":="]- then Nothing- else return ""- f _ = return ""-- isRecursiveFlag "--recursive" = True- isRecursiveFlag ('-':'-':_) = False- isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str- isRecursiveFlag _ = False-- stripTrailing c = reverse . dropWhile (== c) . reverse- skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)- skipRepeating c (a:r) = a:skipRepeating c r- skipRepeating _ [] = []-- paths = [- "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",- "/var", "/lib"- ]- importantPaths = filter (not . null) $- ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)-checkCatastrophicRm _ _ = return ()-- prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar" prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar" checkStderrPipe params =@@ -2210,6 +2155,7 @@ prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;" prop_checkUnpassedInFunctions10= verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;" prop_checkUnpassedInFunctions11= verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;"+prop_checkUnpassedInFunctions12= verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;" checkUnpassedInFunctions params root = execWriter $ mapM_ warnForGroup referenceGroups where@@ -2219,7 +2165,7 @@ functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root findFunction t@(T_Function id _ _ name body) =- let flow = getVariableFlow (shellType params) (parentMap params) body+ let flow = getVariableFlow params body in if any (isPositionalReference t) flow && not (any isPositionalAssignment flow) then return t@@ -2236,7 +2182,11 @@ _ -> False isDirectChildOf child parent = fromMaybe False $ do- function <- find (\x -> case x of T_Function {} -> True; _ -> False) $ getPath (parentMap params) child+ function <- find (\x -> case x of+ T_Function {} -> True+ T_Script {} -> True -- for sourced files+ _ -> False) $+ getPath (parentMap params) child return $ getId parent == getId function referenceList :: [(String, Bool, Token)]@@ -2245,12 +2195,12 @@ checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ()) checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do str <- getLiteralString cmd- unless (Map.member str functionMap) $ fail "irrelevant"+ guard $ Map.member str functionMap return $ tell [(str, null args, t)] checkCommand _ = Nothing isPositional str = str == "*" || str == "@"- || (all isDigit str && str /= "0")+ || (all isDigit str && str /= "0" && str /= "") isArgumentless (_, b, _) = b referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList@@ -2301,8 +2251,8 @@ checkVar _ = return () hasTilde t = fromMaybe False (liftM2 elem (return '~') (getLiteralStringExt (const $ return "") t))- isQuoted (T_DoubleQuoted {}) = True- isQuoted (T_SingleQuoted {}) = True+ isQuoted T_DoubleQuoted {} = True+ isQuoted T_SingleQuoted {} = True isQuoted _ = False checkTildeInPath _ _ = return () @@ -2323,7 +2273,7 @@ case t of T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list) T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Ksh])- otherwise -> ("", [])+ _ -> ("", []) where forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash]) forCase seps | CaseFallThrough `elem` seps = ("cases with ;&", [Bash, Ksh])@@ -2339,7 +2289,7 @@ mapM_ checkList $ getCommandSequences t where checkList list =- mapM_ checkGroup (groupWith (liftM fst) $ map getTarget list)+ mapM_ checkGroup (groupWith (fmap fst) $ map getTarget list) checkGroup (f:_:_:_) | isJust f = style (snd $ fromJust f) 2129 "Consider using { cmd1; cmd2; } >> file instead of individual redirects."@@ -2350,7 +2300,7 @@ file <- mapMaybe getAppend list !!! 0 return (file, id) getTarget _ = Nothing- getAppend (T_FdRedirect _ _ (T_IoFile _ (T_DGREAT {}) f)) = return f+ getAppend (T_FdRedirect _ _ (T_IoFile _ T_DGREAT {} f)) = return f getAppend _ = Nothing @@ -2425,12 +2375,19 @@ prop_checkTestArgumentSplitting13 = verify checkTestArgumentSplitting "[ \"$@\" == \"\" ]" prop_checkTestArgumentSplitting14 = verify checkTestArgumentSplitting "[[ \"$@\" == \"\" ]]" prop_checkTestArgumentSplitting15 = verifyNot checkTestArgumentSplitting "[[ \"$*\" == \"\" ]]"+prop_checkTestArgumentSplitting16 = verifyNot checkTestArgumentSplitting "[[ -v foo[123] ]]" checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] () checkTestArgumentSplitting _ t = case t of- (TC_Unary _ _ op token) | isGlob token ->- err (getId token) 2144 $- op ++ " doesn't work with globs. Use a for loop."+ (TC_Unary _ typ op token) | isGlob token ->+ if op == "-v"+ then+ when (typ == SingleBracket) $+ err (getId token) 2208 $+ "Use [[ ]] or quote arguments to -v to avoid glob expansion."+ else+ err (getId token) 2144 $+ op ++ " doesn't work with globs. Use a for loop." (TC_Nullary _ typ token) -> do checkBraces typ token@@ -2497,38 +2454,53 @@ prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo" prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"-checkReadWithoutR _ t@(T_SimpleCommand {}) | t `isUnqualifiedCommand` "read" =+checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" = unless ("r" `elem` map snd (getAllFlags t)) $ info (getId t) 2162 "read without -r will mangle backslashes." checkReadWithoutR _ _ = return () -prop_checkUncheckedCd1 = verifyTree checkUncheckedCd "cd ~/src; rm -r foo"-prop_checkUncheckedCd2 = verifyNotTree checkUncheckedCd "cd ~/src || exit; rm -r foo"-prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCd "set -e; cd ~/src; rm -r foo"-prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi"-prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi"-prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .."-prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar"-prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar"-checkUncheckedCd params root =- if hasSetE then [] else execWriter $ doAnalysis checkElement root+prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"+prop_checkUncheckedCd2 = verifyNotTree checkUncheckedCdPushdPopd "cd ~/src || exit; rm -r foo"+prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; cd ~/src; rm -r foo"+prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCdPushdPopd "if cd foo; then rm foo; fi"+prop_checkUncheckedCd5 = verifyTree checkUncheckedCdPushdPopd "if true; then cd foo; fi"+prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."+prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"+prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar"+prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"+prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"+prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo"+prop_checkUncheckedPushd4 = verifyNotTree checkUncheckedCdPushdPopd "if pushd foo; then rm foo; fi"+prop_checkUncheckedPushd5 = verifyTree checkUncheckedCdPushdPopd "if true; then pushd foo; fi"+prop_checkUncheckedPushd6 = verifyNotTree checkUncheckedCdPushdPopd "pushd .."+prop_checkUncheckedPushd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npushd foo\nrm bar"+prop_checkUncheckedPushd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; pushd foo; rm bar"+prop_checkUncheckedPushd9 = verifyNotTree checkUncheckedCdPushdPopd "pushd -n foo"+prop_checkUncheckedPopd1 = verifyTree checkUncheckedCdPushdPopd "popd; rm -r foo"+prop_checkUncheckedPopd2 = verifyNotTree checkUncheckedCdPushdPopd "popd || exit; rm -r foo"+prop_checkUncheckedPopd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; popd; rm -r foo"+prop_checkUncheckedPopd4 = verifyNotTree checkUncheckedCdPushdPopd "if popd; then rm foo; fi"+prop_checkUncheckedPopd5 = verifyTree checkUncheckedCdPushdPopd "if true; then popd; fi"+prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"+prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"+prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"++checkUncheckedCdPushdPopd params root =+ if hasSetE params then+ []+ else execWriter $ doAnalysis checkElement root where- checkElement t@(T_SimpleCommand {}) =- when(t `isUnqualifiedCommand` "cd"- && not (isCdDotDot t)+ checkElement t@T_SimpleCommand {} =+ when(name t `elem` ["cd", "pushd", "popd"]+ && not (isSafeDir t)+ && not (name t == "pushd" && ("n" `elem` map snd (getAllFlags t))) && not (isCondition $ getPath (parentMap params) t)) $ warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails." checkElement _ = return ()- isCdDotDot t = oversimplify t == ["cd", ".."]- hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root- isSetE t =- case t of- T_Script _ str _ -> str `matches` re- T_SimpleCommand {} ->- t `isUnqualifiedCommand` "set" &&- ("errexit" `elem` oversimplify t || "e" `elem` map snd (getAllFlags t))- _ -> False- re = mkRegex "[[:space:]]-[^-]*e"+ name t = fromMaybe "" $ getCommandName t+ isSafeDir t = case oversimplify t of+ [_, ".."] -> True;+ _ -> False prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done" prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"@@ -2565,7 +2537,7 @@ checkTrailingBracket _ token = case token of T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token- otherwise -> return ()+ _ -> return () where check t command = case t of@@ -2576,7 +2548,7 @@ guard $ opposite `notElem` parameters return $ warn id 2171 $ "Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?"- otherwise -> return ()+ _ -> return () invert s = case s of "]]" -> "[["@@ -2600,7 +2572,7 @@ when (isExitCode exp) $ message (getId exp) TA_Sequence _ [exp] -> when (isExitCode exp) $ message (getId exp)- otherwise -> return ()+ _ -> return () where check lhs rhs = if isZero rhs && isExitCode lhs@@ -2609,8 +2581,8 @@ isZero t = getLiteralString t == Just "0" isExitCode t = case getWordParts t of- [exp@(T_DollarBraced {})] -> bracedString exp == "?"- otherwise -> False+ [exp@T_DollarBraced {}] -> bracedString exp == "?"+ _ -> False message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?." prop_checkRedirectedNowhere1 = verify checkRedirectedNowhere "> file"@@ -2682,7 +2654,7 @@ guard $ '=' `elem` str return $ warn id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it." in- if (null literalEquals && isAssociative)+ if null literalEquals && isAssociative then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ." else sequence_ literalEquals @@ -2692,23 +2664,51 @@ prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac" prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac" prop_checkUnmatchableCases4 = verifyNot checkUnmatchableCases "case foo-$bar in foo*|*bar|*baz*) true; esac"+prop_checkUnmatchableCases5 = verify checkUnmatchableCases "case $f in *.txt) true;; f??.txt) false;; esac"+prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"+prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac"+prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac" checkUnmatchableCases _ t = case t of- T_CaseExpression _ word list ->+ T_CaseExpression _ word list -> do+ let patterns = concatMap snd3 list+ if isConstant word- then warn (getId word) 2194- "This word is constant. Did you forget the $ on a variable?"- else potentially $ do- pg <- wordToPseudoGlob word- return $ mapM_ (check pg) (concatMap (\(_,x,_) -> x) list)+ then warn (getId word) 2194+ "This word is constant. Did you forget the $ on a variable?"+ else potentially $ do+ pg <- wordToPseudoGlob word+ return $ mapM_ (check pg) patterns++ let exactGlobs = tupMap wordToExactPseudoGlob patterns+ let fuzzyGlobs = tupMap wordToPseudoGlob patterns+ let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)++ mapM_ checkDoms dominators+ _ -> return () where+ snd3 (_,x,_) = x check target candidate = potentially $ 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)+ checkDoms ((glob, Just x), rest) =+ case filter (\(_, p) -> x `pseudoGlobIsSuperSetof` p) valids of+ ((first,_):_) -> do+ warn (getId glob) 2221 "This pattern always overrides a later one."+ warn (getId first) 2222 "This pattern never matches because of a previous pattern."+ _ -> return ()+ where+ valids = concatMap f rest+ f (x, Just y) = [(x,y)]+ f _ = []+ checkDoms _ = return ()++ prop_checkSubshellAsTest1 = verify checkSubshellAsTest "( -e file )" prop_checkSubshellAsTest2 = verify checkSubshellAsTest "( 1 -gt 2 )" prop_checkSubshellAsTest3 = verifyNot checkSubshellAsTest "( grep -c foo bar )"@@ -2736,6 +2736,160 @@ when (fromMaybe False $ (`elem` binaryTestOps) <$> getLiteralString second) $ warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?" ++prop_checkSplittingInArrays1 = verify checkSplittingInArrays "a=( $var )"+prop_checkSplittingInArrays2 = verify checkSplittingInArrays "a=( $(cmd) )"+prop_checkSplittingInArrays3 = verifyNot checkSplittingInArrays "a=( \"$var\" )"+prop_checkSplittingInArrays4 = verifyNot checkSplittingInArrays "a=( \"$(cmd)\" )"+prop_checkSplittingInArrays5 = verifyNot checkSplittingInArrays "a=( $! $$ $# )"+prop_checkSplittingInArrays6 = verifyNot checkSplittingInArrays "a=( ${#arr[@]} )"+prop_checkSplittingInArrays7 = verifyNot checkSplittingInArrays "a=( foo{1,2} )"+prop_checkSplittingInArrays8 = verifyNot checkSplittingInArrays "a=( * )"+checkSplittingInArrays params t =+ case t of+ T_Array _ elements -> mapM_ check elements+ _ -> return ()+ where+ check word = case word of+ T_NormalWord _ parts -> mapM_ checkPart parts+ _ -> return ()+ checkPart part = case part of+ T_DollarExpansion id _ -> forCommand id+ T_DollarBraceCommandExpansion id _ -> forCommand id+ T_Backticked id _ -> forCommand id+ T_DollarBraced id str |+ not (isCountingReference part)+ && not (isQuotedAlternativeReference part)+ && not (getBracedReference (bracedString part) `elem` variablesWithoutSpaces)+ -> warn id 2206 $+ if shellType params == Ksh+ then "Quote to prevent word splitting, or split robustly with read -A or while read."+ else "Quote to prevent word splitting, or split robustly with mapfile or read -a."+ _ -> return ()++ forCommand id =+ warn id 2207 $+ if shellType params == Ksh+ then "Prefer read -A or while read to split command output (or quote to avoid splitting)."+ else "Prefer mapfile or read -a to split command output (or quote to avoid splitting)."+++prop_checkRedirectionToNumber1 = verify checkRedirectionToNumber "( 1 > 2 )"+prop_checkRedirectionToNumber2 = verify checkRedirectionToNumber "foo 1>2"+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+ 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?"+ _ -> return ()++prop_checkGlobAsCommand1 = verify checkGlobAsCommand "foo*"+prop_checkGlobAsCommand2 = verify checkGlobAsCommand "$(var[i])"+prop_checkGlobAsCommand3 = verifyNot checkGlobAsCommand "echo foo*"+checkGlobAsCommand _ t = case t of+ T_SimpleCommand _ _ (first:_) ->+ when (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 ()+++prop_checkFlagAsCommand1 = verify checkFlagAsCommand "-e file"+prop_checkFlagAsCommand2 = verify checkFlagAsCommand "foo\n --bar=baz"+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) $+ warn (getId first) 2215 "This flag is used as a command name. Bad line break or missing [ .. ]?"+ _ -> return ()+++prop_checkEmptyCondition1 = verify checkEmptyCondition "if [ ]; then ..; fi"+prop_checkEmptyCondition2 = verifyNot checkEmptyCondition "[ foo -o bar ]"+checkEmptyCondition _ t = case t of+ TC_Empty id _ -> style id 2212 "Use 'false' instead of empty [/[[ conditionals."+ _ -> return ()++prop_checkPipeToNowhere1 = verify checkPipeToNowhere "foo | echo bar"+prop_checkPipeToNowhere2 = verify checkPipeToNowhere "basename < file.txt"+prop_checkPipeToNowhere3 = verify checkPipeToNowhere "printf 'Lol' <<< str"+prop_checkPipeToNowhere4 = verify checkPipeToNowhere "printf 'Lol' << eof\nlol\neof\n"+prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"+prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"+prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"+checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()+checkPipeToNowhere _ t =+ case t of+ T_Pipeline _ _ (first:rest) -> mapM_ checkPipe rest+ T_Redirecting _ redirects cmd -> when (any redirectsStdin redirects) $ checkRedir cmd+ _ -> return ()+ where+ checkPipe redir = potentially $ do+ cmd <- getCommand redir+ name <- getCommandBasename cmd+ guard $ name `elem` nonReadingCommands+ guard . not $ hasAdditionalConsumers cmd+ return $ warn (getId cmd) 2216 $+ "Piping to '" ++ name ++ "', a command that doesn't read stdin. Wrong command or missing xargs?"++ checkRedir cmd = potentially $ do+ name <- getCommandBasename cmd+ guard $ name `elem` nonReadingCommands+ guard . not $ hasAdditionalConsumers cmd+ return $ warn (getId cmd) 2217 $+ "Redirecting to '" ++ name ++ "', a command that doesn't read stdin. Bad quoting or missing xargs?"++ -- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?+ hasAdditionalConsumers t = fromMaybe True $ do+ doAnalysis (guard . not . mayConsume) t+ return False++ mayConsume t =+ case t of+ T_ProcSub {} -> True+ T_Backticked {} -> True+ T_DollarExpansion {} -> True+ _ -> False++ redirectsStdin t =+ case t of+ T_FdRedirect _ _ (T_IoFile _ T_Less {} _) -> True+ T_FdRedirect _ _ T_HereDoc {} -> True+ T_FdRedirect _ _ T_HereString {} -> True+ _ -> False++prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { true; }"+prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f"+prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi"+prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }"+checkUseBeforeDefinition _ t =+ execWriter $ evalStateT (mapM_ examine $ revCommands) Map.empty+ where+ examine t = case t of+ T_Pipeline _ _ [T_Redirecting _ _ (T_Function _ _ _ name _)] ->+ modify $ Map.insert name t+ T_Annotation _ _ w -> examine w+ T_Pipeline _ _ cmds -> do+ m <- get+ unless (Map.null m) $+ mapM_ (checkUsage m) $ concatMap recursiveSequences cmds+ _ -> return ()++ checkUsage map cmd = potentially $ do+ name <- getCommandName cmd+ def <- Map.lookup name map+ return $+ err (getId cmd) 2218+ "This function is only defined later. Move the definition up."++ revCommands = reverse $ concat $ getCommandSequences t+ recursiveSequences x =+ let list = concat $ getCommandSequences x in+ if null list+ then [x]+ else concatMap recursiveSequences list return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
ShellCheck/Analyzer.hs view
@@ -32,7 +32,7 @@ analyzeScript :: AnalysisSpec -> AnalysisResult analyzeScript spec = AnalysisResult { arComments =- filterByAnnotation (asScript spec) . nub $+ filterByAnnotation spec params . nub $ runAnalytics spec ++ runChecker params (checkers params) }
ShellCheck/AnalyzerLib.hs view
@@ -72,11 +72,13 @@ composeAnalyzers f g x = f x >> g x data Parameters = Parameters {- variableFlow :: [StackData],- parentMap :: Map.Map Id Token,- shellType :: Shell,- shellTypeSpecified :: Bool,- rootNode :: Token+ hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.+ hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.+ variableFlow :: [StackData], -- A linear (bad) analysis of data flow+ parentMap :: Map.Map Id Token, -- A map from Id to parent Token+ shellType :: Shell, -- The shell type, such as Bash or Ksh+ shellTypeSpecified :: Bool, -- True if shell type was forced via flags+ rootNode :: Token -- The root node of the AST } -- TODO: Cache results of common AST ops here@@ -94,7 +96,12 @@ data DataType = DataString DataSource | DataArray DataSource deriving (Show) -data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger+data DataSource =+ SourceFrom [Token]+ | SourceExternal+ | SourceDeclaration+ | SourceInteger+ | SourceChecked deriving (Show) data VariableState = Dead Token String | Alive deriving (Show)@@ -102,6 +109,7 @@ defaultSpec root = AnalysisSpec { asScript = root, asShellType = Nothing,+ asCheckSourced = False, asExecutionMode = Executed } @@ -109,7 +117,8 @@ let pSpec = ParseSpec { psFilename = "script",- psScript = s+ psScript = s,+ psCheckSourced = False } in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec @@ -137,13 +146,48 @@ let params = Parameters { rootNode = root, shellType = fromMaybe (determineShell root) $ asShellType spec,+ hasSetE = containsSetE root,+ hasLastpipe =+ case shellType params of+ Bash -> containsLastpipe root+ Dash -> False+ Sh -> False+ Ksh -> True,+ shellTypeSpecified = isJust $ asShellType spec, parentMap = getParentTree root,- variableFlow =- getVariableFlow (shellType params) (parentMap params) root+ variableFlow = getVariableFlow params root } in params where root = asScript spec ++-- Does this script mention 'set -e' anywhere?+-- Used as a hack to disable certain warnings.+containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root+ where+ isSetE t =+ case t of+ T_Script _ str _ -> str `matches` re+ T_SimpleCommand {} ->+ t `isUnqualifiedCommand` "set" &&+ ("errexit" `elem` oversimplify t ||+ "e" `elem` map snd (getAllFlags t))+ _ -> False+ re = mkRegex "[[:space:]]-[^-]*e"++-- Does this script mention 'shopt -s lastpipe' anywhere?+-- Also used as a hack.+containsLastpipe root =+ isNothing $ doAnalysis (guard . not . isShoptLastPipe) root+ where+ isShoptLastPipe t =+ case t of+ T_SimpleCommand {} ->+ t `isUnqualifiedCommand` "shopt" &&+ ("lastpipe" `elem` oversimplify t)+ _ -> False++ prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash@@ -163,7 +207,7 @@ (ShellOverride s) -> return s _ -> fail "" getCandidates :: Token -> [Maybe String]- getCandidates t@(T_Script {}) = [Just $ fromShebang t]+ getCandidates t@T_Script {} = [Just $ fromShebang t] getCandidates (T_Annotation _ annotations s) = map forAnnotation annotations ++ [Just $ fromShebang s]@@ -179,8 +223,10 @@ shellFor s = reverse . takeWhile (/= '/') . reverse $ s ---- Context seeking +-- Given a root node, make a map from Id to parent Token.+-- This is used to populate parentMap in Parameters+getParentTree :: Token -> Map.Map Id Token getParentTree t = snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty) where@@ -190,19 +236,25 @@ case rest of [] -> put (rest, map) (x:_) -> put (rest, Map.insert (getId t) x map) +-- Given a root node, make a map from Id to Token+getTokenMap :: Token -> Map.Map Id Token getTokenMap t = execState (doAnalysis f t) Map.empty where f t = modify (Map.insert (getId t) t) --- Is this node self quoting for a regular element?-isQuoteFree = isQuoteFreeNode False---- Is this node striclty self quoting, for array expansions+-- Is this token in a quoting free context? (i.e. would variable expansion split)+-- True: Assignments, [[ .. ]], here docs, already in double quotes+-- False: Regular words isStrictlyQuoteFree = isQuoteFreeNode True +-- Like above, but also allow some cases where splitting may be desired.+-- True: Like above + for loops+-- False: Like above+isQuoteFree = isQuoteFreeNode False + isQuoteFreeNode strict tree t = (isQuoteFreeElement t == Just True) || head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])@@ -234,6 +286,9 @@ T_SelectIn {} -> return (not strict) _ -> Nothing +-- Check if a token is a parameter to a certain command by name:+-- Example: isParamTo (parentMap params) "sed" t+isParamTo :: Map.Map Id Token -> String -> Token -> Bool isParamTo tree cmd = go where@@ -249,16 +304,23 @@ T_Redirecting {} -> isCommand t cmd _ -> False +-- Get the parent command (T_Redirecting) of a Token, if any.+getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token getClosestCommand tree t =- msum . map getCommand $ getPath tree t+ findFirst findCommand $ getPath tree t where- getCommand t@(T_Redirecting {}) = return t- getCommand _ = Nothing+ findCommand t =+ case t of+ T_Redirecting {} -> return True+ T_Script {} -> return False+ _ -> Nothing +-- Like above, if koala_man knew Haskell when starting this project. getClosestCommandM t = do tree <- asks parentMap return $ getClosestCommand tree t +-- Is the token used as a command name (the first word in a T_SimpleCommand)? usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) where go currentId (T_NormalWord id [word]:rest)@@ -269,7 +331,7 @@ | currentId == getId word = True go _ _ = False --- A list of the element and all its parents+-- A list of the element and all its parents up to the root node. getPath tree t = t : case Map.lookup (getId t) tree of Nothing -> []@@ -290,6 +352,18 @@ parents <- reader parentMap return $ getPath parents t +-- Find the first match in a list where the predicate is Just True.+-- Stops if it's Just False and ignores Nothing.+findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a+findFirst p l =+ case l of+ [] -> Nothing+ (x:xs) ->+ case p x of+ Just True -> return x+ Just False -> Nothing+ Nothing -> findFirst p xs+ -- Check whether a word is entirely output from a single command tokenIsJustCommandOutput t = case t of T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds@@ -302,29 +376,29 @@ check _ = False -- TODO: Replace this with a proper Control Flow Graph-getVariableFlow shell parents t =+getVariableFlow params t = let (_, stack) = runState (doStackAnalysis startScope endScope t) [] in reverse stack where startScope t =- let scopeType = leadType shell parents t+ let scopeType = leadType params t in do when (scopeType /= NoneScope) $ modify (StackScope scopeType:) when (assignFirst t) $ setWritten t endScope t =- let scopeType = leadType shell parents t+ let scopeType = leadType params t in do setRead t unless (assignFirst t) $ setWritten t when (scopeType /= NoneScope) $ modify (StackScopeEnd:) - assignFirst (T_ForIn {}) = True- assignFirst (T_SelectIn {}) = True+ assignFirst T_ForIn {} = True+ assignFirst T_SelectIn {} = True assignFirst _ = False setRead t =- let read = getReferencedVariables parents t+ let read = getReferencedVariables (parentMap params) t in mapM_ (\v -> modify (Reference v:)) read setWritten t =@@ -332,7 +406,7 @@ in mapM_ (\v -> modify (Assignment v:)) written -leadType shell parents t =+leadType params t = case t of T_DollarExpansion _ _ -> SubshellScope "$(..) expansion" T_Backticked _ _ -> SubshellScope "`..` expansion"@@ -346,7 +420,7 @@ _ -> NoneScope where parentPipeline = do- parent <- Map.lookup (getId t) parents+ parent <- Map.lookup (getId t) (parentMap params) case parent of T_Pipeline {} -> return parent _ -> Nothing@@ -355,17 +429,10 @@ (T_Pipeline _ _ list) <- parentPipeline if length list <= 1 then return False- else if lastCreatesSubshell+ else if not $ hasLastpipe params then return True else return . not $ (getId . head $ reverse list) == getId t - lastCreatesSubshell =- case shell of- Bash -> True- Dash -> True- Sh -> True- Ksh -> False- getModifiedVariables t = case t of T_SimpleCommand _ vars [] ->@@ -374,7 +441,7 @@ [(x, x, name, dataTypeFrom DataString w)] _ -> [] ) vars- c@(T_SimpleCommand {}) ->+ c@T_SimpleCommand {} -> getModifiedVariableCommand c TA_Unary _ "++|" var -> maybeToList $ do@@ -388,6 +455,18 @@ name <- getLiteralString lhs return (t, t, name, DataString $ SourceFrom [rhs]) + -- Count [[ -v foo ]] as an "assignment".+ -- This is to prevent [ -v foo ] being unassigned or unused.+ TC_Unary id _ "-v" token -> maybeToList $ do+ str <- fmap (takeWhile (/= '[')) $ -- Quoted index+ flip getLiteralStringExt token $ \x ->+ case x of+ T_Glob _ s -> return s -- Unquoted index+ _ -> Nothing++ guard . not . null $ str+ return (t, token, str, DataString $ SourceChecked)+ T_DollarBraced _ l -> maybeToList $ do let string = bracedString t let modifier = getBracedModifier string@@ -401,15 +480,15 @@ [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] --Points to 'for' rather than variable- T_ForIn id str [] _ -> [(t, t, str, DataString $ SourceExternal)]+ T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)] T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] _ -> [] isClosingFileOp op = case op of- T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True- T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True+ T_IoDuplicate _ (T_GREATAND _) "-" -> True+ T_IoDuplicate _ (T_LESSAND _) "-" -> True _ -> False @@ -496,7 +575,7 @@ getModifierParam def t@(T_Assignment _ _ name _ value) = [(base, t, name, dataTypeFrom def value)]- getModifierParam def t@(T_NormalWord {}) = maybeToList $ do+ getModifierParam def t@T_NormalWord {} = maybeToList $ do name <- getLiteralString t guard $ isVariableName name return (base, t, name, def SourceDeclaration)@@ -546,7 +625,7 @@ offsets <- match !!! 0 return $ matchAllStrings variableNameRegex offsets where- re = mkRegex "^ *:(.*)"+ re = mkRegex "^ *:([^-=?+].*)" getReferencedVariables parents t = case t of@@ -584,8 +663,10 @@ getVariablesFromLiteralToken word else [] - literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x- literalizer _ = Nothing+ literalizer t = case t of+ TA_Index {} -> return "" -- x[0] becomes a reference of x+ T_Glob _ s -> return s -- Also when parsed as globs+ _ -> Nothing getIfReference context token = maybeToList $ do str <- getLiteralStringExt literalizer token@@ -604,13 +685,20 @@ --- Command specific checks +-- Compare a command to a string: t `isCommand` "sed" (also matches /usr/bin/sed) isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)++-- Compare a command to a literal. Like above, but checks full path. isUnqualifiedCommand token str = isCommandMatch token (== str) isCommandMatch token matcher = fromMaybe False $ do cmd <- getCommandName token return $ matcher cmd +-- Does this regex look like it was intended as a glob?+-- True: *foo*+-- False: .*foo.*+isConfusedGlobRegex :: String -> Bool isConfusedGlobRegex ('*':_) = True isConfusedGlobRegex [x,'*'] | x /= '\\' = True isConfusedGlobRegex _ = False@@ -637,6 +725,7 @@ where variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" +-- Get the variable name from an expansion like ${var:-foo} prop_getBracedReference1 = getBracedReference "foo" == "foo" prop_getBracedReference2 = getBracedReference "#foo" == "foo" prop_getBracedReference3 = getBracedReference "#" == "#"@@ -687,13 +776,22 @@ dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest] dropModifier x = [x] --- Useful generic functions+-- 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 +--- Get element n of a list, or Nothing. Like `!!` but safe. (!!!) list i = case drop i list of [] -> Nothing@@ -705,9 +803,10 @@ when (shell `elem` l ) c -filterByAnnotation token =+filterByAnnotation asSpec params = filter (not . shouldIgnore) where+ token = asScript asSpec idFor (TokenComment id _) = id shouldIgnore note = any (shouldIgnoreFor (getCode note)) $@@ -717,10 +816,27 @@ where hasNum (DisableComment ts) = num == ts hasNum _ = False- shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files+ shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec shouldIgnoreFor _ _ = False- parents = getParentTree token+ parents = parentMap params getCode (TokenComment _ (Comment _ c _)) = c++-- Is this a ${#anything}, to get string length or array count?+isCountingReference (T_DollarBraced id token) =+ case concat $ oversimplify token of+ '#':_ -> True+ _ -> False+isCountingReference _ = False++-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}+isQuotedAlternativeReference t =+ case t of+ T_DollarBraced _ _ ->+ getBracedModifier (bracedString t) `matches` re+ _ -> False+ where+ re = mkRegex "(^|\\]):?\\+"+ return []
ShellCheck/Checker.hs view
@@ -54,7 +54,8 @@ checkScript contents = do result <- parseScript sys ParseSpec { psFilename = csFilename spec,- psScript = contents+ psScript = contents,+ psCheckSourced = csCheckSourced spec } let parseMessages = prComments result let analysisMessages =@@ -77,6 +78,7 @@ AnalysisSpec { asScript = root, asShellType = csShellTypeOverride spec,+ asCheckSourced = csCheckSourced spec, asExecutionMode = Executed } @@ -88,14 +90,22 @@ check = checkWithIncludes [] +checkWithSpec includes =+ getErrors (mockedSystemInterface includes)+ checkWithIncludes includes src =- getErrors- (mockedSystemInterface includes)- emptyCheckSpec {- csScript = src,- csExcludedWarnings = [2148]- }+ checkWithSpec includes emptyCheckSpec {+ csScript = src,+ csExcludedWarnings = [2148]+ } +checkRecursive includes src =+ checkWithSpec includes emptyCheckSpec {+ csScript = src,+ csExcludedWarnings = [2148],+ csCheckSourced = True+ }+ prop_findsParseIssue = check "echo \"$12\"" == [1037] prop_commentDisablesParseIssue1 =@@ -152,6 +162,12 @@ prop_canSourceDynamicWhenRedirected = null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""++prop_recursiveAnalysis =+ [2086] == checkRecursive [("lib", "echo $1")] "source lib"++prop_recursiveParsing =+ [1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib" prop_sourceDirectiveDoesntFollowFile = null $ checkWithIncludes
ShellCheck/Checks/Commands.hs view
@@ -38,7 +38,7 @@ import Data.Char import Data.List import Data.Maybe-import qualified Data.Map as Map+import qualified Data.Map.Strict as Map import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) @@ -85,13 +85,16 @@ ,checkDeprecatedTempfile ,checkDeprecatedEgrep ,checkDeprecatedFgrep+ ,checkWhileGetoptsCase+ ,checkCatastrophicRm+ ,checkLetUsage ] buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap = foldl' addCheck Map.empty where addCheck map (CommandCheck name function) =- Map.insertWith' composeAnalyzers name function map+ Map.insertWith composeAnalyzers name function map checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis@@ -195,6 +198,9 @@ prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file" prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo" prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"+prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"+prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"+prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*" checkGrepRe = CommandCheck (Basename "grep") check where check cmd = f cmd (arguments cmd)@@ -202,8 +208,18 @@ skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s skippable _ = False f _ [] = return ()- f cmd (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f cmd r- f cmd (re:_) = do+ f cmd (x:r) =+ let str = getLiteralStringExt (const $ return "_") x+ in+ if str `elem` [Just "--", Just "-e", Just "--regex"]+ then checkRE cmd r -- Regex is *after* this+ else+ if skippable str+ then f cmd r -- Regex is elsewhere+ else checkRE cmd (x:r) -- Regex is this++ checkRE _ [] = return ()+ checkRE cmd (re:_) = do when (isGlob re) $ warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." @@ -385,17 +401,26 @@ prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b" prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b" prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"+prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"+prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"+prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"+prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"+prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"+prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"+prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin" checkMkdirDashPM = CommandCheck (Basename "mkdir") check where check t = potentially $ do let flags = getAllFlags t dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags- guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.+ -- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.+ guard $ any couldHaveSubdirs (drop 1 $ arguments t) return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory." couldHaveSubdirs t = fromMaybe True $ do name <- getLiteralString t- return $ '/' `elem` name+ return $ '/' `elem` name && not (name `matches` re)+ re = mkRegex "^(\\.\\.?\\/)+[^/]+$" prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"@@ -404,9 +429,13 @@ prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL" prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9" prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"+prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' int" checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) where- f = mapM_ check+ f args = case args of+ first:rest -> unless (isFlag first) $ mapM_ check rest+ _ -> return ()+ check param = potentially $ do str <- getLiteralString param let id = getId param@@ -605,6 +634,8 @@ prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find" prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f" prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"+prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."+prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ." checkFindWithoutPath = CommandCheck (Basename "find") f where f (T_SimpleCommand _ _ (cmd:args)) =@@ -613,11 +644,12 @@ -- This is a bit of a kludge. find supports flag arguments both before and after the path, -- as well as multiple non-flag arguments that are not the path. We assume that all the- -- pre-path flags are single characters, which is generally the case.+ -- pre-path flags are single characters, which is generally the case except for -O3. hasPath (first:rest) = let flag = fromJust $ getLiteralStringExt (const $ return "___") first in- not ("-" `isPrefixOf` flag) || length flag <= 2 && hasPath rest+ not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest hasPath [] = False+ isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"@@ -680,6 +712,143 @@ prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files" checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $ \t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."++prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"+prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"+prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"+prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"+prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"+checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f+ where+ f :: Token -> Analysis+ f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do+ path <- getPathM t+ potentially $ do+ options <- getLiteralString arg1+ (T_WhileExpression _ _ body) <- findFirst whileLoop path+ caseCmd <- mapMaybe findCase body !!! 0+ return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd+ f _ = return ()++ check :: Id -> [String] -> Token -> Analysis+ check optId opts (T_CaseExpression id _ list) = do+ unless (Nothing `Map.member` handledMap) $ do+ mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled++ unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $+ warn id 2220 "Invalid flags are not handled. Add a *) case."++ mapM_ warnRedundant $ Map.toList notRequested++ where+ handledMap = Map.fromList (concatMap getHandledStrings list)+ requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts++ notHandled = Map.difference requestedMap handledMap+ notRequested = Map.difference handledMap requestedMap++ warnUnhandled optId caseId str =+ warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."++ warnRedundant (key, expr) = potentially $ do+ str <- key+ guard $ str `notElem` ["*", ":", "?"]+ return $ warn (getId expr) 2214 "This case is not specified by getopts."++ getHandledStrings (_, globs, _) =+ map (\x -> (literal x, x)) globs++ literal :: Token -> Maybe String+ literal t = do+ getLiteralString t <> fromGlob t++ fromGlob t =+ case t of+ T_Glob _ ('[':c:']':[]) -> return [c]+ T_Glob _ "*" -> return "*"+ T_Glob _ "?" -> return "?"+ _ -> Nothing++ whileLoop t =+ case t of+ T_WhileExpression {} -> return True+ T_Script {} -> return False+ _ -> Nothing++ findCase t =+ case t of+ T_Annotation _ _ x -> findCase x+ T_Pipeline _ _ [x] -> findCase x+ T_Redirecting _ _ x@(T_CaseExpression {}) -> return x+ _ -> Nothing++prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"+prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"+prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"+prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"+prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"+prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"+prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"+prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"+prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"+prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"+prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"+prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"+prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"+checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->+ when (isRecursive t) $+ mapM_ (mapM_ checkWord . braceExpand) $ arguments t+ where+ isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags++ checkWord token =+ case getLiteralString token of+ Just str ->+ when (fixPath str `elem` importantPaths) $+ warn (getId token) 2114 "Warning: deletes a system directory."+ Nothing ->+ checkWord' token++ checkWord' token = fromMaybe (return ()) $ do+ filename <- getPotentialPath token+ let path = fixPath filename+ return . when (path `elem` importantPaths) $+ warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."++ fixPath filename =+ let normalized = skipRepeating '/' . skipRepeating '*' $ filename in+ if normalized == "/" then normalized else stripTrailing '/' normalized++ getPotentialPath = getLiteralStringExt f+ where+ f (T_Glob _ str) = return str+ f (T_DollarBraced _ word) =+ let var = onlyLiteralString word in+ -- This shouldn't handle non-colon cases.+ if any (`isInfixOf` var) [":?", ":-", ":="]+ then Nothing+ else return ""+ f _ = return ""++ stripTrailing c = reverse . dropWhile (== c) . reverse+ skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)+ skipRepeating c (a:r) = a:skipRepeating c r+ skipRepeating _ [] = []++ paths = [+ "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",+ "/var", "/lib", "/dev", "/media", "/boot", "/lib64", "/usr/bin"+ ]+ importantPaths = filter (not . null) $+ ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)+++prop_checkLetUsage1 = verify checkLetUsage "let a=1"+prop_checkLetUsage2 = verifyNot checkLetUsage "(( a=1 ))"+checkLetUsage = CommandCheck (Exactly "let") f+ where+ f t = whenShell [Bash,Ksh] $ do+ style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ." return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
ShellCheck/Checks/ShellSupport.hs view
@@ -58,6 +58,7 @@ ,checkEchoSed ,checkBraceExpansionVars ,checkMultiDimensionalArrays+ ,checkPS1Assignments ] testChecker (ForShell _ t) =@@ -160,10 +161,15 @@ bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is" bashism (T_HereString id _) = warnMsg id "here-strings are" bashism (TC_Binary id SingleBracket op _ _)- | op `elem` [ "-nt", "-ef", "\\<", "\\>"] =+ | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =+ unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"+ bashism (TC_Binary id SingleBracket op _ _)+ | op `elem` [ "-nt", "-ef" ] = unless isDash $ warnMsg id $ op ++ " is" bashism (TC_Binary id SingleBracket "==" _ _) = warnMsg id "== in place of = is"+ bashism (TC_Binary id SingleBracket "=~" _ _) =+ warnMsg id "=~ regex matching is" bashism (TC_Unary id _ "-a" _) = warnMsg id "unary -a in place of -e is" bashism (TA_Unary id op _)@@ -382,6 +388,32 @@ re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well isMultiDim t = getBracedModifier (bracedString t) `matches` re++prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"+prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"+prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"+prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"+prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"+prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"+prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"+prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"+prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"+prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"+checkPS1Assignments = ForShell [Bash] f+ where+ f token = case token of+ (T_Assignment _ _ "PS1" _ word) -> warnFor word+ _ -> return ()++ warnFor word =+ let contents = concat $ oversimplify word in+ when (containsUnescaped contents) $+ info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"+ containsUnescaped s =+ let unenclosed = subRegex enclosedRegex s "" in+ isJust $ matchRegex escapeRegex unenclosed+ enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager+ escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" return []
ShellCheck/Data.hs view
@@ -77,6 +77,14 @@ "zcat" ] +nonReadingCommands = [+ "alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",+ "cp", "du", "echo", "export", "fg", "fuser", "getconf", "getopt",+ "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "locale", "mv",+ "nice", "printf", "ps", "pwd", "renice", "rm", "rmdir", "set", "sleep",+ "touch", "trap", "ulimit", "unalias", "uname"+ ]+ sampleWords = [ "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliett", "kilo", "lima", "mike",
ShellCheck/Formatter/CheckStyle.hs view
@@ -34,14 +34,27 @@ putStrLn "<checkstyle version='4.3'>", onFailure = outputError,- onResult = outputResult,+ onResult = outputResults, footer = putStrLn "</checkstyle>" } -outputResult result contents = do- let comments = makeNonVirtual (crComments result) contents- putStrLn . formatFile (crFilename result) $ comments+outputResults cr sys =+ if null comments+ then outputFile (crFilename cr) "" []+ else mapM_ outputGroup fileGroups+ where+ comments = crComments cr+ fileGroups = groupWith sourceFile comments+ outputGroup group = do+ let filename = sourceFile (head group)+ result <- (siReadFile sys) filename+ let contents = either (const "") id result+ outputFile filename contents group++outputFile filename contents warnings = do+ let comments = makeNonVirtual warnings contents+ putStrLn . formatFile filename $ comments formatFile name comments = concat [ "<file ", attr "name" name, ">\n",
ShellCheck/Formatter/Format.hs view
@@ -25,11 +25,12 @@ -- A formatter that carries along an arbitrary piece of data data Formatter = Formatter { header :: IO (),- onResult :: CheckResult -> String -> IO (),+ onResult :: CheckResult -> SystemInterface IO -> IO (), onFailure :: FilePath -> ErrorMessage -> IO (), footer :: IO () } +sourceFile (PositionedComment pos _ _) = posFile pos lineNo (PositionedComment pos _ _) = posLine pos endLineNo (PositionedComment _ end _) = posLine end colNo (PositionedComment pos _ _) = posColumn pos
ShellCheck/Formatter/GCC.hs view
@@ -31,14 +31,25 @@ header = return (), footer = return (), onFailure = outputError,- onResult = outputResult+ onResult = outputAll } outputError file error = hPutStrLn stderr $ file ++ ": " ++ error -outputResult result contents = do- let comments = makeNonVirtual (crComments result) contents- mapM_ (putStrLn . formatComment (crFilename result)) comments+outputAll cr sys = mapM_ f groups+ where+ comments = crComments cr+ groups = groupWith sourceFile comments+ f :: [PositionedComment] -> IO ()+ f group = do+ let filename = sourceFile (head group)+ result <- (siReadFile sys) filename+ let contents = either (const "") id result+ outputResult filename contents group++outputResult filename contents warnings = do+ let comments = makeNonVirtual warnings contents+ mapM_ (putStrLn . formatComment filename) comments formatComment filename c = concat [ filename, ":",
ShellCheck/Formatter/TTY.hs view
@@ -43,15 +43,22 @@ "style" -> 32 -- green "message" -> 1 -- bold "source" -> 0 -- none- otherwise -> 0 -- none+ _ -> 0 -- none outputError options file error = do color <- getColorFunc $ foColorOption options hPutStrLn stderr $ color "error" $ file ++ ": " ++ error -outputResult options result contents = do+outputResult options result sys = do color <- getColorFunc $ foColorOption options let comments = crComments result+ let fileGroups = groupWith sourceFile comments+ mapM_ (outputForFile color sys) fileGroups++outputForFile color sys comments = do+ let fileName = sourceFile (head comments)+ result <- (siReadFile sys) fileName+ let contents = either (const "") id result let fileLines = lines contents let lineCount = fromIntegral $ length fileLines let groups = groupWith lineNo comments@@ -62,7 +69,7 @@ else fileLines !! fromIntegral (lineNum - 1) putStrLn "" putStrLn $ color "message" $- "In " ++ crFilename result ++" line " ++ show lineNum ++ ":"+ "In " ++ fileName ++" line " ++ show lineNum ++ ":" putStrLn (color "source" line) mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x putStrLn ""
ShellCheck/Interface.hs view
@@ -24,7 +24,7 @@ import qualified Data.Map as Map -data SystemInterface m = SystemInterface {+newtype SystemInterface m = SystemInterface { -- Read a file by filename, or return an error siReadFile :: String -> m (Either ErrorMessage String) }@@ -33,6 +33,7 @@ data CheckSpec = CheckSpec { csFilename :: String, csScript :: String,+ csCheckSourced :: Bool, csExcludedWarnings :: [Integer], csShellTypeOverride :: Maybe Shell } deriving (Show, Eq)@@ -42,9 +43,11 @@ crComments :: [PositionedComment] } deriving (Show, Eq) +emptyCheckSpec :: CheckSpec emptyCheckSpec = CheckSpec { csFilename = "", csScript = "",+ csCheckSourced = False, csExcludedWarnings = [], csShellTypeOverride = Nothing }@@ -52,7 +55,8 @@ -- Parser input and output data ParseSpec = ParseSpec { psFilename :: String,- psScript :: String+ psScript :: String,+ psCheckSourced :: Bool } deriving (Show, Eq) data ParseResult = ParseResult {@@ -65,16 +69,17 @@ data AnalysisSpec = AnalysisSpec { asScript :: Token, asShellType :: Maybe Shell,- asExecutionMode :: ExecutionMode+ asExecutionMode :: ExecutionMode,+ asCheckSourced :: Bool } -data AnalysisResult = AnalysisResult {+newtype AnalysisResult = AnalysisResult { arComments :: [TokenComment] } -- Formatter options-data FormatterOptions = FormatterOptions {+newtype FormatterOptions = FormatterOptions { foColorOption :: ColorOption }
ShellCheck/Parser.hs view
@@ -14,7 +14,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - yOU should have received a copy of the GNU General Public License+ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. -} {-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell, FlexibleContexts #-}@@ -47,7 +47,7 @@ import Test.QuickCheck.All (quickCheckAll) -type SCBase m = Mr.ReaderT (SystemInterface m) (Ms.StateT SystemState m)+type SCBase m = Mr.ReaderT (Environment m) (Ms.StateT SystemState m) type SCParser m v = ParsecT String UserState (SCBase m) v backslash :: Monad m => SCParser m Char@@ -62,13 +62,16 @@ doubleQuote = char '"' variableStart = upper <|> lower <|> oneOf "_" variableChars = upper <|> lower <|> digit <|> oneOf "_"-functionChars = variableChars <|> oneOf ":+-.?"+-- Chars to allow in function names+functionChars = variableChars <|> oneOf ":+?-./^"+-- Chars to allow in functions using the 'function' keyword+extendedFunctionChars = functionChars <|> oneOf "[]*=!" specialVariable = oneOf "@*#?-$!" paramSubSpecialChars = oneOf "/:+-=%" quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars quotable = almostSpace <|> oneOf quotableChars bracedQuotable = oneOf "}\"$`'"-doubleQuotableChars = "\"$`"+doubleQuotableChars = "\\\"$`" doubleQuotable = oneOf doubleQuotableChars whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed linewhitespace = oneOf " \t" <|> almostSpace@@ -248,12 +251,14 @@ shouldIgnoreCode code = do context <- getCurrentContexts- return $ any disabling context+ checkSourced <- Mr.asks checkSourced+ return $ any (disabling checkSourced) context where- disabling (ContextAnnotation list) =- any disabling' list- disabling (ContextSource _) = True -- Don't add messages for sourced files- disabling _ = False+ disabling checkSourced item =+ case item of+ ContextAnnotation list -> any disabling' list+ ContextSource _ -> not $ checkSourced+ _ -> False disabling' (DisableComment n) = code == n disabling' _ = False @@ -297,6 +302,11 @@ parseProblems = [] } +data Environment m = Environment {+ systemInterface :: SystemInterface m,+ checkSourced :: Bool+}+ parseProblem level code msg = do pos <- getPosition parseProblemAt pos level code msg@@ -357,10 +367,8 @@ notFollowedBy2 = unexpecting "" -disregard = void- reluctantlyTill p end =- (lookAhead (disregard (try end) <|> eof) >> return []) <|> do+ (lookAhead (void (try end) <|> eof) >> return []) <|> do x <- p more <- reluctantlyTill p end return $ x:more@@ -440,19 +448,9 @@ getOp = do id <- getNextId- op <- anyQuotedOp <|> anyEscapedOp <|> anyOp+ op <- readRegularOrEscaped anyOp return $ TC_Binary id typ op - -- hacks to read quoted operators without having to read a shell word- anyEscapedOp = try $ do- char '\\'- escaped <$> anyOp- anyQuotedOp = try $ do- c <- oneOf "'\""- s <- anyOp- char c- return $ escaped s- anyOp = flagOp <|> flaglessOp <|> fail "Expected comparison operator (don't wrap commands in []/[[]])" flagOp = try $ do@@ -461,10 +459,25 @@ return s flaglessOp = choice $ map (try . string) flaglessOps- escaped s = if any (`elem` s) "<>" then '\\':s else s + -- hacks to read quoted operators without having to read a shell word+ readEscaped p = try $ withEscape <|> withQuotes+ where+ withEscape = do+ char '\\'+ escaped <$> p+ withQuotes = do+ c <- oneOf "'\""+ s <- p+ char c+ return $ escaped s+ escaped s = if any (`elem` s) "<>()" then '\\':s else s++ readRegularOrEscaped p = readEscaped p <|> p++ guardArithmetic = do- try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")+ try . lookAhead $ void (oneOf "+*/%") <|> void (string "- ") parseProblem ErrorC 1076 $ if single then "Trying to do math? Use e.g. [ $((i/2+7)) -ge 18 ]."@@ -507,7 +520,7 @@ parseProblemAt pos ErrorC 1021 "You need a space before the \\)" fail "Missing space before )"- disregard spacing+ void spacing return x where endedWith str (T_NormalWord id s@(_:_)) = case last s of T_Literal id s -> str `isSuffixOf` s@@ -560,30 +573,31 @@ "You need a space before and after the " ++ trailingOp ++ " ." readCondGroup = do- id <- getNextId- pos <- getPosition- lparen <- try $ string "(" <|> string "\\("- when (single && lparen == "(") $- parseProblemAt pos ErrorC 1028 "In [..] you have to escape (). Use [[..]] instead."- when (not single && lparen == "\\(") $- parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ()."- condSpacing single- x <- readCondContents- cpos <- getPosition- rparen <- string ")" <|> string "\\)"- condSpacing single- when (single && rparen == ")") $- parseProblemAt cpos ErrorC 1030 "In [..] you have to escape (). Use [[..]] instead."- when (not single && rparen == "\\)") $- parseProblemAt cpos ErrorC 1031 "In [[..]] you shouldn't escape ()."- when (isEscaped lparen `xor` isEscaped rparen) $- parseProblemAt pos ErrorC 1032 "Did you just escape one half of () but not the other?"- return $ TC_Group id typ x+ id <- getNextId+ pos <- getPosition+ lparen <- try $ readRegularOrEscaped (string "(")+ when (single && lparen == "(") $+ singleWarning pos+ when (not single && lparen == "\\(") $+ doubleWarning pos+ condSpacing single+ x <- readCondContents+ cpos <- getPosition+ rparen <- readRegularOrEscaped (string ")")+ condSpacing single+ when (single && rparen == ")") $+ singleWarning cpos+ when (not single && rparen == "\\)") $+ doubleWarning cpos+ return $ TC_Group id typ x+ where- isEscaped ('\\':_) = True- isEscaped _ = False- xor x y = x && not y || not x && y+ singleWarning pos =+ parseProblemAt pos ErrorC 1028 "In [..] you have to escape \\( \\) or preferably combine [..] expressions."+ doubleWarning pos =+ parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ( or )." + -- Currently a bit of a hack since parsing rules are obscure regexOperatorAhead = lookAhead (do try (string "=~") <|> try (string "~=")@@ -599,7 +613,7 @@ readNormalLiteral "( " <|> readPipeLiteral <|> readGlobLiteral)- disregard spacing+ void spacing return $ T_NormalWord id parts where readGlobLiteral = do@@ -848,11 +862,14 @@ prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]" prop_readCondition16= isOk readCondition "[ foo \\< bar ]" prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"+prop_readCondition18= isOk readCondition "[ ]"+prop_readCondition19= isOk readCondition "[ '(' x \")\" ]" readCondition = called "test expression" $ do opos <- getPosition id <- getNextId open <- try (string "[[") <|> string "[" let single = open == "["+ let typ = if single then SingleBracket else DoubleBracket pos <- getPosition space <- allspacing@@ -864,7 +881,11 @@ when (single && '\n' `elem` space) $ parseProblemAt pos ErrorC 1080 "You need \\ before line feeds to break lines in [ ]." - condition <- readConditionContents single+ condition <- readConditionContents single <|> do+ guard . not . null $ space+ lookAhead $ string "]"+ id <- getNextIdAt pos+ return $ TC_Empty id typ cpos <- getPosition close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"@@ -872,7 +893,7 @@ when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?" spacing many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?- return $ T_Condition id (if single then SingleBracket else DoubleBracket) condition+ return $ T_Condition id typ condition readAnnotationPrefix = do char '#'@@ -883,47 +904,51 @@ prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n" prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n" prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"-readAnnotation = called "shellcheck annotation" $ do+prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"+prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"+readAnnotation = called "shellcheck directive" $ do try readAnnotationPrefix many1 linewhitespace- values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey)- linefeed+ values <- many1 readKey+ optional readAnyComment+ void linefeed <|> do+ parseNote ErrorC 1125 "Invalid key=value pair? Ignoring the rest of this directive starting here."+ many (noneOf "\n")+ void linefeed <|> eof many linewhitespace return $ concat values where- readDisable = forKey "disable" $- readCode `sepBy` char ','- where- readCode = do- optional $ string "SC"- int <- many1 digit- return $ DisableComment (read int)+ readKey = do+ keyPos <- getPosition+ key <- many1 letter+ char '=' <|> fail "Expected '=' after directive key"+ annotations <- case key of+ "disable" -> readCode `sepBy` char ','+ where+ readCode = do+ optional $ string "SC"+ int <- many1 digit+ return $ DisableComment (read int) - readSourceOverride = forKey "source" $ do- filename <- many1 $ noneOf " \n"- return [SourceOverride filename]+ "source" -> do+ filename <- many1 $ noneOf " \n"+ return [SourceOverride filename] - readShellOverride = forKey "shell" $ do- pos <- getPosition- shell <- many1 $ noneOf " \n"- when (isNothing $ shellForExecutable shell) $- parseNoteAt pos ErrorC 1103- "This shell type is unknown. Use e.g. sh or bash."- return [ShellOverride shell]+ "shell" -> do+ pos <- getPosition+ shell <- many1 $ noneOf " \n"+ when (isNothing $ shellForExecutable shell) $+ parseNoteAt pos ErrorC 1103+ "This shell type is unknown. Use e.g. sh or bash."+ return [ShellOverride shell] - forKey s p = do- try $ string s- char '='- value <- p- many linewhitespace- return value+ _ -> do+ parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored."+ anyChar `reluctantlyTill` whitespace+ return [] - anyKey = do- pos <- getPosition- anyChar `reluctantlyTill1` whitespace many linewhitespace- parseNoteAt pos WarningC 1107 "This directive is unknown. It will be ignored."- return []+ return annotations readAnnotations = do annotations <- many (readAnnotation `thenSkip` allspacing)@@ -931,6 +956,9 @@ readComment = do unexpecting "shellcheck annotation" readAnnotationPrefix+ readAnyComment++readAnyComment = do char '#' many $ noneOf "\r\n" @@ -1132,7 +1160,7 @@ verifyEof return cmds backtick =- disregard (char '`') <|> do+ void (char '`') <|> do pos <- getPosition char '´' parseProblemAt pos ErrorC 1077@@ -1173,6 +1201,8 @@ prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\"" prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\"" prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\""+prop_readDoubleQuoted9 = isWarning readDoubleQuoted "\"foo\\n\""+prop_readDoubleQuoted10 = isOk readDoubleQuoted "\"foo\\\\n\"" readDoubleQuoted = called "double quoted string" $ do id <- getNextId startPos <- getPosition@@ -1219,7 +1249,7 @@ return $ T_Literal id (concat s) readDoubleLiteralPart = do- x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars ++ unicodeDoubleQuotes)))+ x <- many1 (readDoubleEscaped <|> many1 (noneOf (doubleQuotableChars ++ unicodeDoubleQuotes))) return $ concat x readNormalLiteral end = do@@ -1339,17 +1369,22 @@ x <- lookAhead anyChar case x of- '\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\''s done'.";+ '\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'."; '\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line." _ -> return () return [s] readDoubleEscaped = do+ pos <- getPosition bs <- backslash (linefeed >> return "") <|> liftM return doubleQuotable- <|> liftM (\ x -> [bs, x]) anyChar+ <|> do+ c <- anyChar+ parseNoteAt pos StyleC 1117 $+ "Backslash is literal in \"\\" ++ [c] ++ "\". Prefer explicit escaping: \"\\\\" ++ [c] ++ "\"."+ return [bs, c] readBraceEscaped = do bs <- backslash@@ -1583,6 +1618,11 @@ prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n" prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n" prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"+prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"+prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"+prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\n"+prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"+prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n" readHereDoc = called "here document" $ do fid <- getNextId pos <- getPosition@@ -1616,20 +1656,37 @@ where readDoc (T_HereDoc id dashed quoted endToken _) = do pos <- getPosition- hereData <- anyChar `reluctantlyTill` do- spacing+ hereData <- concat <$> rawLine `reluctantlyTill` do+ linewhitespace `reluctantlyTill` string endToken string endToken- disregard (char '\n') <|> eof+ void linewhitespace <|> void (oneOf "\n;&#)") <|> eof do- spaces <- spacing+ spaces <- linewhitespace `reluctantlyTill` string endToken verifyHereDoc dashed quoted spaces hereData string endToken+ trailingPos <- getPosition+ trailers <- lookAhead $ many (noneOf "\n")+ let ppt = parseProblemAt trailingPos ErrorC+ unless (null trailers) $+ if all isSpace trailers+ then ppt 1118 "Delete whitespace after the here-doc end token."+ else case (head $ dropWhile isSpace trailers) of+ ')' -> ppt 1119 $ "Add a linefeed between end token and terminating ')'."+ '#' -> ppt 1120 "No comments allowed after here-doc token. Comment the next line instead."+ c | c `elem` ";&" ->+ ppt 1121 "Add ;/& terminators (and other syntax) on the line with the <<, not here."+ _ -> ppt 1122 "Nothing allowed after end token. To continue a command, put it on the line with the <<." parsedData <- parseHereData quoted pos hereData list <- parseHereData quoted pos hereData addToHereDocMap id list `attempting` (eof >> debugHereDoc pos endToken hereData) + rawLine = do+ c <- many $ noneOf "\n"+ void (char '\n') <|> eof+ return $ c ++ "\n"+ parseHereData Quoted startPos hereData = do id <- getNextIdAt startPos return [T_Literal id hereData]@@ -1654,9 +1711,9 @@ debugHereDoc pos endToken doc | endToken `isInfixOf` doc = let lookAt line = when (endToken `isInfixOf` line) $- parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")+ parseProblemAt pos ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').") in do- parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.")+ parseProblemAt pos ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.") mapM_ lookAt (lines doc) | map toLower endToken `isInfixOf` map toLower doc = parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")@@ -1702,6 +1759,7 @@ id <- getNextId n <- readIoSource redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile+ skipAnnotationAndWarn spacing return $ T_FdRedirect id n redir @@ -1745,7 +1803,7 @@ spacing return f -readSequentialSep = disregard (g_Semi >> readLineBreak) <|> disregard readNewlineList+readSequentialSep = void (g_Semi >> readLineBreak) <|> void readNewlineList readSeparator = do separator <- readSeparatorOp@@ -1779,11 +1837,13 @@ prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)" prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi" prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"+prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls" readSimpleCommand = called "simple command" $ do pos <- getPosition id1 <- getNextId id2 <- getNextId prefix <- option [] readCmdPrefix+ skipAnnotationAndWarn cmd <- option Nothing $ do { f <- readCmdName; return $ Just f; } when (null prefix && isNothing cmd) $ fail "Expected a command" case cmd of@@ -1831,7 +1891,7 @@ "This file appears to be recursively sourced. Ignoring." return t else do- sys <- Mr.ask+ sys <- Mr.asks systemInterface input <- if filename == "/dev/null" -- always allow /dev/null then return (Right "")@@ -1879,8 +1939,13 @@ prop_readAndOr2 = isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo" readAndOr = do aid <- getNextId+ apos <- getPosition annotations <- readAnnotations + unless (null annotations) $ optional $ do+ try . lookAhead $ readKeyword+ parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."+ andOr <- withAnnotations annotations $ chainr1 readPipeline $ do op <- g_AND_IF <|> g_OR_IF@@ -1948,9 +2013,25 @@ readSimpleCommand ] -readCmdName = readCmdWord-readCmdWord = readNormalWord <* spacing+readCmdName = do+ -- Ignore alias suppression+ optional . try $ do+ char '\\'+ lookAhead $ variableChars+ readCmdWord +readCmdWord = do+ skipAnnotationAndWarn+ readNormalWord <* spacing++-- Due to poor planning, annotations after commands isn't handled well.+-- At the time this function is used, it's usually too late to skip+-- comments, so you end up with a parse failure instead.+skipAnnotationAndWarn = optional $ do+ try . lookAhead $ readAnnotationPrefix+ parseProblem ErrorC 1126 "Place shellcheck directives before commands, not after."+ readAnyComment+ prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi" prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi" prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"@@ -1990,7 +2071,7 @@ parseProblem ErrorC 1050 "Expected 'then'." return "Expected 'then'" - acceptButWarn g_Semi ErrorC 1051 "No semicolons directly after 'then'."+ acceptButWarn g_Semi ErrorC 1051 "Semicolons directly after 'then' are not allowed. Just remove it." allspacing verifyNotEmptyIf "then" @@ -2009,7 +2090,7 @@ parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?" g_Then- acceptButWarn g_Semi ErrorC 1052 "No semicolons directly after 'then'."+ acceptButWarn g_Semi ErrorC 1052 "Semicolons directly after 'then' are not allowed. Just remove it." allspacing verifyNotEmptyIf "then" action <- readTerm@@ -2021,7 +2102,7 @@ readElsePart = called "else clause" $ do pos <- getPosition g_Else- acceptButWarn g_Semi ErrorC 1053 "No semicolons directly after 'else'."+ acceptButWarn g_Semi ErrorC 1053 "Semicolons directly after 'else' are not allowed. Just remove it." allspacing verifyNotEmptyIf "else" readTerm@@ -2043,10 +2124,13 @@ prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }" prop_readBraceGroup2 = isWarning readBraceGroup "{foo;}"+prop_readBraceGroup3 = isOk readBraceGroup "{(foo)}" readBraceGroup = called "brace group" $ do id <- getNextId char '{'- allspacingOrFail <|> parseProblem ErrorC 1054 "You need a space after the '{'."+ void allspacingOrFail <|> optional (do+ lookAhead $ noneOf "(" -- {( is legal+ parseProblem ErrorC 1054 "You need a space after the '{'.") optional $ do pos <- getPosition lookAhead $ char '}'@@ -2108,6 +2192,7 @@ prop_readForClause9 = isOk readForClause "for i do true; done" prop_readForClause10= isOk readForClause "for ((;;)) { true; }" prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done"+prop_readForClause13= isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done" readForClause = called "for loop" $ do pos <- getPosition (T_For id) <- g_For@@ -2135,7 +2220,7 @@ readRegular id pos = do acceptButWarn (char '$') ErrorC 1086 "Don't use $ on the iterator name in for loops."- name <- readVariableName `thenSkip` spacing+ name <- readVariableName `thenSkip` allspacing values <- readInClause <|> (optional readSequentialSep >> return []) group <- readDoGroup pos return $ T_ForIn id name values group@@ -2159,14 +2244,14 @@ readInClause = do g_In things <- readCmdWord `reluctantlyTill`- (disregard g_Semi <|> disregard linefeed <|> disregard g_Do)+ (void g_Semi <|> void linefeed <|> void g_Do) do { lookAhead g_Do; parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'."; } <|> do { optional g_Semi;- disregard allspacing;+ void allspacing; } return things@@ -2191,9 +2276,12 @@ readCaseItem = called "case item" $ do notFollowedBy2 g_Esac+ optional $ do+ try . lookAhead $ readAnnotationPrefix+ parseProblem ErrorC 1124 "ShellCheck directives are only valid in front of complete commands like 'case' statements, not individual case branches." optional g_Lparen spacing- pattern <- readPattern+ pattern' <- readPattern void g_Rparen <|> do parseProblem ErrorC 1085 "Did you forget to move the ;; after extending this case item?"@@ -2206,7 +2294,7 @@ parseProblemAt pos ErrorC 1074 "Did you forget the ;; after the previous case item?" readLineBreak- return (separator, pattern, list)+ return (separator, pattern', list) readCaseSeparator = choice [ tryToken ";;&" (const ()) >> return CaseContinue,@@ -2225,10 +2313,11 @@ prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }" prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }" prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"+prop_readFunctionDefinition12= isOk readFunctionDefinition "function []!() { true; }" readFunctionDefinition = called "function" $ do functionSignature <- try readFunctionSignature allspacing- disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition."+ void (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition." group <- readBraceGroup <|> readSubshell return $ functionSignature group where@@ -2241,7 +2330,7 @@ string "function" whitespace spacing- name <- readFunctionName+ name <- many1 extendedFunctionChars spaces <- spacing hasParens <- wasIncluded readParens when (not hasParens && null spaces) $@@ -2251,7 +2340,7 @@ readWithoutFunction = try $ do id <- getNextId- name <- readFunctionName+ name <- many1 functionChars guard $ name /= "time" -- Interfers with time ( foo ) spacing readParens@@ -2266,8 +2355,6 @@ g_Rparen return () - readFunctionName = many1 functionChars- prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }" prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" prop_readCoProc3 = isOk readCoProc "coproc echo bar"@@ -2394,6 +2481,9 @@ prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"+prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"+prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"+prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))" readAssignmentWord = readAssignmentWordExt True readWellFormedAssignment = readAssignmentWordExt False readAssignmentWordExt lenient = try $ do@@ -2410,7 +2500,7 @@ pos <- getPosition op <- readAssignmentOp hasRightSpace <- liftM (not . null) spacing- isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof))+ isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof)) if not hasLeftSpace && (hasRightSpace || isEndOfCommand) then do when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $@@ -2420,7 +2510,11 @@ return $ T_Assignment id op variable indices value else do when (hasLeftSpace || hasRightSpace) $- parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments."+ parseNoteAt pos ErrorC 1068 $+ "Don't put spaces around the "+ ++ if op == Append+ then "+= when appending."+ else "= in assignments." value <- readArray <|> readNormalWord spacing return $ T_Assignment id op variable indices value@@ -2453,7 +2547,11 @@ readArray :: Monad m => SCParser m Token readArray = called "array assignment" $ do id <- getNextId+ opening <- getPosition char '('+ optional $ do+ lookAhead $ char '('+ parseProblemAt opening ErrorC 1116 "Missing $ on a $((..)) expression? (or use ( ( for arrays)." allspacing words <- readElement `reluctantlyTill` char ')' char ')' <|> fail "Expected ) to close array assignment"@@ -2466,9 +2564,9 @@ x <- many1 readArrayIndex char '=' return x- value <- readNormalWord <|> nothing+ value <- readRegular <|> nothing return $ T_IndexedElement id index value- readRegular = readNormalWord+ readRegular = readArray <|> readNormalWord nothing = do id <- getNextId@@ -2498,7 +2596,7 @@ try . lookAhead $ char '#' parseProblem ErrorC 1099 "You need a space before the #." - try $ lookAhead keywordSeparator+ lookAhead keywordSeparator when (str /= keyword) $ parseProblem ErrorC 1081 $ "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."@@ -2534,7 +2632,7 @@ g_Until = tryWordToken "until" T_Until g_For = tryWordToken "for" T_For g_Select = tryWordToken "select" T_Select-g_In = tryWordToken "in" T_In+g_In = tryWordToken "in" T_In <* skipAnnotationAndWarn g_Lbrace = tryWordToken "{" T_Lbrace g_Rbrace = do -- handled specially due to ksh echo "${ foo; }bar" id <- getNextId@@ -2557,7 +2655,7 @@ tryToken ";" T_Semi keywordSeparator =- eof <|> disregard whitespace <|> disregard (oneOf ";()[<>&|")+ eof <|> void (try allspacingOrFail) <|> void (oneOf ";()[<>&|") readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ] @@ -2569,7 +2667,13 @@ prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n" prop_readShebang4 = isWarning readShebang "! /bin/sh" readShebang = do- try readCorrect <|> try readSwapped <|> try readMissingHash+ choice $ map try [+ readCorrect,+ readSwapped,+ readTooManySpaces,+ readMissingHash,+ readMissingBang+ ] many linewhitespace str <- many $ noneOf "\r\n" optional carriageReturn@@ -2577,21 +2681,47 @@ return str where readCorrect = void $ string "#!"+ readSwapped = do pos <- getPosition string "!#" parseProblemAt pos ErrorC 1084 "Use #!, not !#, for the shebang." + skipSpaces = liftM (not . null) $ many linewhitespace+ readTooManySpaces = do+ startPos <- getPosition+ startSpaces <- skipSpaces+ char '#'+ middlePos <- getPosition+ middleSpaces <- skipSpaces+ char '!'+ when startSpaces $+ parseProblemAt startPos ErrorC 1114+ "Remove leading spaces before the shebang."+ when middleSpaces $+ parseProblemAt middlePos ErrorC 1115+ "Remove spaces between # and ! in the shebang."+ readMissingHash = do pos <- getPosition char '!'- lookAhead $ do- many linewhitespace- char '/'+ ensurePathAhead parseProblemAt pos ErrorC 1104 "Use #!, not just !, for the shebang." + readMissingBang = do+ char '#'+ pos <- getPosition+ ensurePathAhead+ parseProblemAt pos ErrorC 1113+ "Use #!, not just #, for the shebang."+++ ensurePathAhead = lookAhead $ do+ many linewhitespace+ char '/'+ verifyEof = eof <|> choice [ ifParsable g_Lparen $ parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",@@ -2684,22 +2814,42 @@ script <- readScriptFile reparseIndices script -isWarning p s = parsesCleanly p s == Just False-isOk p s = parsesCleanly p s == Just True-isNotOk p s = parsesCleanly p s == Nothing -testParse string = runIdentity $ do- (res, _) <- runParser (mockedSystemInterface []) readScript "-" string+-- Interactively run a parser in ghci:+-- debugParse readScript "echo 'hello world'"+debugParse p string = runIdentity $ do+ (res, _) <- runParser testEnvironment p "-" string return res +testEnvironment =+ Environment {+ systemInterface = (mockedSystemInterface []),+ checkSourced = False+ }+++isOk p s = parsesCleanly p s == Just True -- The string parses with no warnings+isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings+isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse+ parsesCleanly parser string = runIdentity $ do- (res, sys) <- runParser (mockedSystemInterface [])+ (res, sys) <- runParser testEnvironment (parser >> eof >> getState) "-" string case (res, sys) of (Right userState, systemState) -> return $ Just . null $ parseNotes userState ++ parseProblems systemState (Left _, _) -> return Nothing +-- For printf debugging: print the value of an expression+-- Example: return $ dump $ T_Literal id [c]+dump :: Show a => a -> a+dump x = trace (show x) x++-- Like above, but print a specific expression:+-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]+dumps :: Show x => x -> a -> a+dumps t = trace (show t)+ parseWithNotes parser = do item <- parser state <- getState@@ -2728,22 +2878,22 @@ Message s -> if null s then Nothing else return $ s ++ "." runParser :: Monad m =>- SystemInterface m ->+ Environment m -> SCParser m v -> String -> String -> m (Either ParseError v, SystemState) -runParser sys p filename contents =+runParser env p filename contents = Ms.runStateT (Mr.runReaderT (runParserT p initialUserState filename contents)- sys)+ env) initialSystemState system = lift . lift . lift -parseShell sys name contents = do- (result, state) <- runParser sys (parseWithNotes readScript) name contents+parseShell env name contents = do+ (result, state) <- runParser env (parseWithNotes readScript) name contents case result of Right (script, userstate) -> return ParseResult {@@ -2829,11 +2979,13 @@ parseScript :: Monad m => SystemInterface m -> ParseSpec -> m ParseResult parseScript sys spec =- parseShell sys (psFilename spec) (psScript spec)-+ parseShell env (psFilename spec) (psScript spec)+ where+ env = Environment {+ systemInterface = sys,+ checkSourced = psCheckSourced spec+ } -lt x = trace (show x) x-ltt t = trace (show t) return [] runTests = $quickCheckAll
shellcheck.1 view
@@ -1,4 +1,4 @@-.\" Automatically generated by Pandoc 1.17.2+.\" Automatically generated by Pandoc 1.19.2.1 .\" .TH "SHELLCHECK" "1" "" "Shell script analysis tool" "" .hy@@ -38,6 +38,15 @@ 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 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[].@@ -216,6 +225,14 @@ 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]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 ENVIRONMENT VARIABLES
shellcheck.1.md view
@@ -32,6 +32,12 @@ # OPTIONS +**-a**,\ **--check-sourced**++: Emit warnings in sourced files. Normally, `shellcheck` will only warn+ about issues in the specified files. With this option, any issues in+ sourced files files will also be reported.+ **-C**[*WHEN*],\ **--color**[=*WHEN*] : For TTY output, enable colors *always*, *never* or *auto*. The default@@ -67,6 +73,7 @@ line (plus `/dev/null`). This option allows following any file the script may `source`. + # FORMATS **tty**@@ -156,6 +163,11 @@ : Overrides the filename included by a `source`/`.` 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 `/dev/null`.++**shell**+: 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 'disable=2039'. # ENVIRONMENT VARIABLES The environment variable `SHELLCHECK_OPTS` can be set with default flags:
shellcheck.hs view
@@ -33,8 +33,10 @@ import Control.Monad.Except import Data.Bits import Data.Char-import Data.Functor import Data.Either+import Data.Functor+import Data.IORef+import Data.List import qualified Data.Map as Map import Data.Maybe import Data.Monoid@@ -74,19 +76,23 @@ usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." options = [- Option "e" ["exclude"]- (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",- Option "f" ["format"]- (ReqArg (Flag "format") "FORMAT") "output format",+ Option "a" ["check-sourced"]+ (NoArg $ Flag "sourced" "false") "Include warnings from sourced files", Option "C" ["color"] (OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN") "Use color (auto, always, never)",+ Option "e" ["exclude"]+ (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",+ Option "f" ["format"]+ (ReqArg (Flag "format") "FORMAT") $+ "Output format (" ++ formatList ++ ")", Option "s" ["shell"]- (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)",- Option "x" ["external-sources"]- (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",+ (ReqArg (Flag "shell") "SHELLNAME")+ "Specify dialect (sh, bash, dash, ksh)", Option "V" ["version"]- (NoArg $ Flag "version" "true") "Print version information"+ (NoArg $ Flag "version" "true") "Print version information",+ Option "x" ["external-sources"]+ (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES" ] printErr = lift . hPutStrLn stderr@@ -107,6 +113,10 @@ ("tty", ShellCheck.Formatter.TTY.format options) ] +formatList = intercalate ", " names+ where+ names = Map.keys $ formats (formatterOptions defaultOptions)+ getOption [] _ = Nothing getOption (Flag var val:_) name | name == var = return val getOption (_:rest) flag = getOption rest flag@@ -129,7 +139,7 @@ in map (Prelude.read . clean) elements :: [Int] -toStatus = liftM (either id id) . runExceptT+toStatus = fmap (either id id) . runExceptT getEnvArgs = do opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv@@ -186,23 +196,27 @@ newStatus <- process file `catch` handler file return $ status `mappend` newStatus handler :: FilePath -> IOException -> IO Status- handler file e = do- onFailure format file (show e)+ handler file e = reportFailure file (show e)+ reportFailure file str = do+ onFailure format file str return RuntimeException process :: FilePath -> IO Status process filename = do- contents <- inputFile filename- let checkspec = (checkSpec options) {- csFilename = filename,- csScript = contents- }- result <- checkScript sys checkspec- onResult format result contents- return $- if null (crComments result)- then NoProblems- else SomeProblems+ input <- (siReadFile sys) filename+ either (reportFailure filename) check input+ where+ check contents = do+ let checkspec = (checkSpec options) {+ csFilename = filename,+ csScript = contents+ }+ result <- checkScript sys checkspec+ onResult format result sys+ return $+ if null (crComments result)+ then NoProblems+ else SomeProblems parseColorOption colorOption = case colorOption of@@ -247,6 +261,13 @@ } } + Flag "sourced" _ ->+ return options {+ checkSpec = (checkSpec options) {+ csCheckSourced = True+ }+ }+ _ -> return options where die s = do@@ -261,14 +282,28 @@ ioInterface options files = do inputs <- mapM normalize files+ cache <- newIORef emptyCache return SystemInterface {- siReadFile = get inputs+ siReadFile = get cache inputs } where- get inputs file = do+ emptyCache :: Map.Map FilePath String+ emptyCache = Map.empty+ get cache inputs file = do+ map <- readIORef cache+ case Map.lookup file map of+ Just x -> return $ Right x+ Nothing -> fetch cache inputs file++ fetch cache inputs file = do ok <- allowable inputs file if ok- then (Right <$> inputFile file) `catch` handler+ then (do+ (contents, shouldCache) <- inputFile file+ when shouldCache $+ modifyIORef cache $ Map.insert file contents+ return $ Right contents+ ) `catch` handler else return $ Left (file ++ " was not specified as input (see shellcheck -x).") where@@ -289,16 +324,19 @@ fallback path _ = return path inputFile file = do- handle <-+ (handle, shouldCache) <- if file == "-"- then return stdin- else openBinaryFile file ReadMode+ then return (stdin, True)+ else do+ h <- openBinaryFile file ReadMode+ reopenable <- hIsSeekable h+ return (h, not reopenable) hSetBinaryMode handle True contents <- decodeString <$> hGetContents handle -- closes handle seq (length contents) $- return contents+ return (contents, shouldCache) -- Decode a char8 string into a utf8 string, with fallback on -- ISO-8859-1. This avoids depending on additional libraries.