65 Commits

Author SHA1 Message Date
Vidar Holen
a21df2d88f Stable version 0.4.3
This is purely a bugfix bump that works on GHC 7.6.3,
as currently found in Debian stable and Ubuntu LTS.
2016-01-13 16:54:54 -08:00
Vidar Holen
d473fb8867 Use system over callCommand to allow linking on ghc 7.6.3. 2016-01-13 14:10:21 -08:00
Vidar Holen
f754363733 Add >= process-1.2.0.0 as dependency due to custom build. 2016-01-11 10:56:40 -08:00
Vidar Holen
ef1f8f535e Stable version 0.4.2
This release is dedicated to Tom Lehrer, who lives
the dream of having quit just to loaf about indefinitely.
2016-01-09 15:30:07 -08:00
Vidar Holen
f9909504dd Make SC2174 only trigger for nested directories. 2016-01-09 14:24:31 -08:00
koalaman
fa4cefda9d Merge pull request #572 from eatnumber1/mkdir
Add a warning when you mkdir with both -p and -m.
2016-01-09 13:58:53 -08:00
Russell Harmon
f2f6c66902 Add a warning when you mkdir with both -p and -m.
When using -p, parent directories will not be created with the mode
specified with -m and will instead be created using the default behavior
controlled by umask.
2016-01-08 14:15:11 -08:00
Vidar Holen
1f4dd85548 Change cabal Build-Type to Custom to actually use Setup.hs. 2016-01-05 14:10:45 -08:00
Vidar Holen
528381796e Allow escaped characters in [..] globs 2015-12-13 10:19:48 -08:00
Vidar Holen
ad7ad28246 Merge branch 'master' of github.com:koalaman/shellcheck 2015-12-12 15:53:18 -08:00
Vidar Holen
33ab998b02 Don't warn about quoting for [ -v var ] 2015-12-12 15:47:35 -08:00
koalaman
9c28237d52 Merge pull request #557 from eatnumber1/ignvars
Add more variables which are assumed to be set.
2015-12-10 20:26:00 -08:00
Russell Harmon
e0e5ba3a90 Add more variables which are assumed to be set.
This commit adds LC_MONETARY, LOGNAME, LD_LIBRARY_PATH, LANGUAGE,
DISPLAY, HOSTNAME, KRB5CCNAME, XAUTHORITY to be ignored when used
without initialization.
2015-12-10 20:05:50 -08:00
Vidar Holen
b4390414ef Fix remaining FIXME for tty error color output. 2015-12-06 12:48:53 -08:00
Vidar Holen
8acd5b13cd Add scripts for running shellcheck/tests interpreted. 2015-12-05 17:47:43 -08:00
Vidar Holen
d00ca0c283 Fix/generalize message for SC2070. 2015-12-05 17:29:06 -08:00
Vidar Holen
8bc98d89a7 Let SC2094 ignore echo/printf/sponge. 2015-12-05 16:50:11 -08:00
Vidar Holen
c7964a7a78 Warn about missing space in 'function foo{'. 2015-12-05 16:09:44 -08:00
Vidar Holen
8ec87d6655 Mention -- as alternative to ./* in SC2035 2015-12-05 13:45:56 -08:00
Vidar Holen
c3df2bf761 Don't warn about deprecated `` when just used for comments. 2015-12-05 13:33:39 -08:00
Vidar Holen
d1df3713ca Document --color in the man page. 2015-12-05 13:16:31 -08:00
Vidar Holen
23496e93b0 Fix compiler warning about missing field 2015-12-05 13:08:02 -08:00
koalaman
437e69fbba Merge pull request #553 from haguenau/add-color-switch
Add --color switch
2015-12-05 12:33:04 -08:00
David Haguenauer
63ad3f99ad Shorten long help line 2015-12-04 11:40:52 -05:00
David Haguenauer
0044c3dd6e Make use of --color with no argument equivalent to --color=always 2015-12-04 10:57:54 -05:00
David Haguenauer
a3d4101d6c Add initial support for --color 2015-12-03 17:55:56 -05:00
David Haguenauer
bd359c5c0f Delete trailing whitespace 2015-12-03 17:55:56 -05:00
Vidar Holen
498de63337 Mention that cabal installs to ~/.cabal/bin 2015-11-28 13:42:31 -08:00
Vidar Holen
52ab7dee2d Updating README.md to take over for shellcheck.net/about.html 2015-11-27 17:32:48 -08:00
Vidar Holen
1a5296659b Properly handle escaped double quotes in quoted backtick expressions. 2015-11-01 12:30:33 -08:00
Vidar Holen
a66ee2967c Trap warnings for kill/stop, non-XSI ints, "sig"-prefix and casing. 2015-10-31 17:36:24 -07:00
Vidar Holen
d985380f48 Consider "echo $(<file)" a bashism, and don't warn about UUOE. 2015-10-31 14:48:08 -07:00
Vidar Holen
6739c4a729 Count declare -p as a variable reference. 2015-10-31 14:16:45 -07:00
Vidar Holen
7415c9dcb7 Warn about non-posix function names like x-y 2015-10-31 13:53:17 -07:00
Vidar Holen
d3fc1f355d Merge branch 'master' of github.com:koalaman/shellcheck 2015-10-31 13:36:52 -07:00
Vidar Holen
48fd793581 Update getFlag function to also return non-flags. 2015-10-27 22:07:29 -07:00
koalaman
e5842e2e2b Merge pull request #516 from Fusl/patch-01
Fix typo "zyper" -> "zypper"
2015-10-22 13:59:50 -07:00
Fusl
cf445c7d20 Fix typo "zyper" -> "zypper" 2015-10-21 11:29:01 +02:00
Vidar Holen
ffb9578a98 Support parsing quoted test operators, and also warn about unicode dashes. 2015-10-17 16:33:21 -07:00
Vidar Holen
630f20e888 Count ~/ as dynamic for resolving source paths. 2015-10-17 10:50:07 -07:00
Vidar Holen
8f5f91f041 Warn about ]] with no corresponding [[. 2015-10-17 10:31:14 -07:00
Vidar Holen
8d9d4533c3 Don't trigger constant checks for -ot/-ef/-nt 2015-10-17 09:28:29 -07:00
koalaman
a4b4954a23 Merge pull request #512 from mimi1vx/opensuse-docu
install instructions for openSUSE
2015-10-17 09:10:12 -07:00
Ondřej Súkup
38cea9201d install instructions for openSUSE 2015-10-17 17:17:32 +02:00
Vidar Holen
4ce916ec1d Include bash builtin arrays in SC2128. 2015-10-14 12:31:09 -07:00
Vidar Holen
b9cb040128 Account for dereferencing for numerical ops in [[ ]]. 2015-10-14 11:11:17 -07:00
Vidar Holen
2488be7298 Don't warn about undefined HOSTNAME if it's being assigned. 2015-10-14 09:21:21 -07:00
Vidar Holen
d01b59a827 Don't warn about empty assignments when at end of command. 2015-10-13 12:16:39 -07:00
Vidar Holen
f77821625c Add dash as a first class supported shell. 2015-10-13 11:37:50 -07:00
Vidar Holen
1eece5b2ee Add warning about local in sh or not in bash functions. 2015-10-10 20:48:52 -07:00
Vidar Holen
58d45e3fa4 Warn about non-posix %q and flags for printf. 2015-10-10 20:14:17 -07:00
Vidar Holen
5aaa1a7d9a Split SC2165 messages into separate codes (2167). 2015-10-10 19:26:14 -07:00
Vidar Holen
3b36c2c820 Document exit codes in man page. 2015-10-10 19:19:38 -07:00
Vidar Holen
55692926b9 Don't consider {} and {a} brace expansions. 2015-10-03 21:12:28 -07:00
Vidar Holen
4172722167 Don't warn about quoted rhs of =~ when not a regex. 2015-10-03 15:21:57 -07:00
Vidar Holen
485593da2c Don't warn about disrespected quotes in ${#var}. 2015-10-01 18:56:45 -07:00
Vidar Holen
1181c6b3af Warn about ${!var} in POSIX mode. 2015-10-01 18:34:09 -07:00
Vidar Holen
ee181cfc43 Don't warn about comparisons when redirecting stderr for test. 2015-10-01 18:19:47 -07:00
Vidar Holen
c72667407b Merge branch 'master' of github.com:koalaman/shellcheck 2015-09-28 18:53:51 -07:00
Vidar Holen
5467a0f1d9 Account for set -o errexit and #!/bin/bash -e for unchecked cd. 2015-09-28 18:52:03 -07:00
Vidar Holen
3fc77d94ec Warn about [ -n foo ] when foo is entirely constant. 2015-09-28 18:46:32 -07:00
koalaman
23e0420cb1 Merge pull request #469 from spartacus06/fedora-readme
Add Fedora install procedure to README.md
2015-09-21 08:29:44 -07:00
Seth Jennings
a898165ac7 Add Fedora install procedure to README.md 2015-09-21 09:12:19 -05:00
Vidar Holen
ba5e3db31a Stable version 0.4.1
This is purely a bugfix bump because I botched the
0.4.0 cabal file and subsequent Hackage release :|
2015-09-05 19:32:37 -07:00
Vidar Holen
56145217fe Fixed the cabal file, which didn't include formatters :| 2015-09-05 19:17:22 -07:00
17 changed files with 795 additions and 196 deletions

270
README.md
View File

@@ -1,26 +1,67 @@
# ShellCheck - A shell script static analysis tool # ShellCheck - A shell script static analysis tool
http://www.shellcheck.net ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
Copyright 2012-2015, Vidar 'koala_man' Holen ![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png).
Licensed under the GNU General Public License, v3
The goals of ShellCheck are: The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues, - To point out and clarify typical beginner's syntax issues
that causes a shell to give cryptic error messages. that causes a shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems, - To point out and clarify typical intermediate level semantic problems
that causes a shell to behave strangely and counter-intuitively. that causes a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls, that may cause an - To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances. 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!
## How to use
There are a variety of ways to use ShellCheck!
#### 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!
#### From your terminal
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
#### In your editor
You can see ShellCheck suggestions directly in a variety of editors.
* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):
![Screenshot of vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* Most other editors, through [GCC error compatibility](blob/master/shellcheck.1.md#user-content-formats).
#### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
Use ShellCheck's exit code, or it's [CheckStyle compatible XML output](blob/master/shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
ShellCheck is written in Haskell, and requires 2 GB of memory to compile.
## Installing ## Installing
On systems with Cabal: The easiest way to install ShellCheck locally is through your package manager.
On systems with Cabal (installs to `~/.cabal/bin`):
cabal update cabal update
cabal install shellcheck cabal install shellcheck
@@ -29,54 +70,60 @@ On Debian based distros:
apt-get install shellcheck apt-get install shellcheck
On Fedora based distros:
dnf install ShellCheck
On OS X with homebrew: On OS X with homebrew:
brew install shellcheck brew install shellcheck
ShellCheck is also available as an online service: On OS X with MacPorts:
http://www.shellcheck.net port install shellcheck
## Building with Cabal On openSUSE:Tumbleweed:
This sections describes how to build ShellCheck from a source directory. zypper in ShellCheck
First, make sure cabal is installed. On Debian based distros: On other openSUSE distributions:
apt-get install cabal-install add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
On Fedora: zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
zypper in ShellCheck
yum install cabal-install or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
On Mac OS X with homebrew (http://brew.sh/):
brew install cabal-install ## Compiling from source
On Mac OS X with MacPorts (http://www.macports.org/): This sections describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
port install hs-cabal-install
On native Windows (https://www.haskell.org/platform/): #### Installing Cabal
Download and install the latest version of the Haskell Platform. ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `yum`, `zypper` or `brew`).
Let cabal update itself, in case your distro version is outdated: 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 $ cabal update
$ cabal install cabal-install
With cabal installed, cd to the ShellCheck source directory and: #### Compiling ShellCheck
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
$ cabal install $ cabal install
This will install ShellCheck to your `~/.cabal/bin` directory. This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add the directory to your `PATH` (for bash, add this to your `~/.bashrc`): Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
export PATH="$HOME/.cabal/bin:$PATH" export PATH="$HOME/.cabal/bin:$PATH"
Verify that your PATH is set up correctly: Log out and in again, and verify that your PATH is set up correctly:
$ which shellcheck $ which shellcheck
~/.cabal/bin/shellcheck ~/.cabal/bin/shellcheck
@@ -93,13 +140,155 @@ In Powershell ISE, you may need to additionally update the output encoding:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 > [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
## Running tests #### Running tests
To run the unit test suite: To run the unit test suite:
cabal configure --enable-tests $ cabal test
cabal build
cabal test
## Gallery of bad code
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
#### 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
#### 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 $(..)
#### 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
#### 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
echo "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
#### 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
#### 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
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
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
#### 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
#### 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
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
> At first you're like "shellcheck is awesome" but then you're like "wtf are we still using bash"
Alexander Tarasikov,
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
## Reporting bugs ## Reporting bugs
@@ -107,4 +296,19 @@ Please use the Github issue tracker for any bugs or feature suggestions:
https://github.com/koalaman/shellcheck/issues https://github.com/koalaman/shellcheck/issues
## Contributing
Please submit patches to code or documentation as Github pull requests!
Contributions must be licensed under the GNU GPLv3.
The contributor retains the copyright.
## Copyright
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
Happy ShellChecking! Happy ShellChecking!

View File

@@ -8,21 +8,13 @@ import Distribution.Simple (
simpleUserHooks ) simpleUserHooks )
import Distribution.Simple.Setup ( SDistFlags ) import Distribution.Simple.Setup ( SDistFlags )
-- | This requires the process package from, import System.Process ( system )
--
-- https://hackage.haskell.org/package/process
--
import System.Process ( callCommand )
-- | This will use almost the default implementation, except we switch
-- out the default pre-sdist hook with our own, 'myPreSDist'.
--
main = defaultMainWithHooks myHooks main = defaultMainWithHooks myHooks
where where
myHooks = simpleUserHooks { preSDist = myPreSDist } myHooks = simpleUserHooks { preSDist = myPreSDist }
-- | This hook will be executed before e.g. @cabal sdist@. It runs -- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc -- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message: -- command is not found, this will fail with an error message:
@@ -35,9 +27,10 @@ main = defaultMainWithHooks myHooks
-- --
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do myPreSDist _ _ = do
putStrLn "Building the man page..." putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd putStrLn pandoc_cmd
callCommand pandoc_cmd result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
return emptyHookedBuildInfo return emptyHookedBuildInfo
where where
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1" pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.4.0 Version: 0.4.3
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -7,7 +7,7 @@ Category: Static Analysis
Author: Vidar Holen Author: Vidar Holen
Maintainer: vidar@vidarholen.net Maintainer: vidar@vidarholen.net
Homepage: http://www.shellcheck.net/ Homepage: http://www.shellcheck.net/
Build-Type: Simple Build-Type: Custom
Cabal-Version: >= 1.8 Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues Bug-reports: https://github.com/koalaman/shellcheck/issues
Description: Description:
@@ -44,7 +44,9 @@ library
mtl >= 2.2.1, mtl >= 2.2.1,
parsec, parsec,
regex-tdfa, regex-tdfa,
QuickCheck >= 2.7.4 QuickCheck >= 2.7.4,
-- When cabal supports it, move this to setup-depends:
process
exposed-modules: exposed-modules:
ShellCheck.AST ShellCheck.AST
ShellCheck.ASTLib ShellCheck.ASTLib
@@ -53,6 +55,10 @@ library
ShellCheck.Checker ShellCheck.Checker
ShellCheck.Data ShellCheck.Data
ShellCheck.Formatter.Format ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle
ShellCheck.Formatter.GCC
ShellCheck.Formatter.JSON
ShellCheck.Formatter.TTY
ShellCheck.Interface ShellCheck.Interface
ShellCheck.Parser ShellCheck.Parser
ShellCheck.Regex ShellCheck.Regex

View File

@@ -93,14 +93,17 @@ oversimplify token =
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar", -- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
-- each in a tuple of (token, stringFlag). -- each in a tuple of (token, stringFlag). Non-flag arguments are added with
-- stringFlag == "".
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) = getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ oversimplify x)) args in let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
concatMap flag textArgs (flagArgs, rest) = break (stopCondition . snd) tokenAndText
in
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
where where
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ] flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args flag (x, '-':args) = map (\v -> (x, [v])) args
flag _ = [] flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)" getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
-- Get all flags in a GNU way, up until -- -- Get all flags in a GNU way, up until --
@@ -224,6 +227,16 @@ isAssignment t =
T_Annotation _ _ w -> isAssignment w T_Annotation _ _ w -> isAssignment w
otherwise -> False otherwise -> False
isOnlyRedirection t =
case t of
T_Pipeline _ _ [x] -> isOnlyRedirection x
T_Annotation _ _ w -> isOnlyRedirection w
T_Redirecting _ (_:_) c -> isOnlyRedirection c
T_SimpleCommand _ [] [] -> True
otherwise -> False
isFunction t = case t of T_Function {} -> True; _ -> False
-- Get the list of commands from tokens that contain them, such as -- Get the list of commands from tokens that contain them, such as
-- the body of while loops and if statements. -- the body of while loops and if statements.
getCommandSequences t = getCommandSequences t =

View File

@@ -74,6 +74,11 @@ checksFor Sh = [
,checkTimeParameters ,checkTimeParameters
,checkForDecimals ,checkForDecimals
] ]
checksFor Dash = [
checkBashisms
,checkForDecimals
,checkLocalScope
]
checksFor Ksh = [ checksFor Ksh = [
checkEchoSed checkEchoSed
] ]
@@ -82,6 +87,7 @@ checksFor Bash = [
,checkBraceExpansionVars ,checkBraceExpansionVars
,checkEchoSed ,checkEchoSed
,checkForDecimals ,checkForDecimals
,checkLocalScope
] ]
runAnalytics :: AnalysisSpec -> AnalysisResult runAnalytics :: AnalysisSpec -> AnalysisResult
@@ -210,9 +216,11 @@ nodeChecks = [
,checkReadWithoutR ,checkReadWithoutR
,checkExportedExpansions ,checkExportedExpansions
,checkLoopVariableReassignment ,checkLoopVariableReassignment
,checkTrailingBracket
,checkNonportableSignals
,checkMkdirDashPM
] ]
filterByAnnotation token = filterByAnnotation token =
filter (not . shouldIgnore) filter (not . shouldIgnore)
where where
@@ -224,6 +232,7 @@ filterByAnnotation token =
any hasNum anns any hasNum anns
where where
hasNum (DisableComment ts) = num == ts hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
shouldIgnoreFor _ _ = False shouldIgnoreFor _ _ = False
parents = getParentTree token parents = getParentTree token
@@ -577,17 +586,39 @@ prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
prop_checkBashisms20= verify checkBashisms "read -ra foo" prop_checkBashisms20= verify checkBashisms "read -ra foo"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]" prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]" prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23= verify checkBashisms "trap mything err int" prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24= verifyNot checkBashisms "trap mything int term" prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123" prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM" prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*" prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2" prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
checkBashisms _ = bashism prop_checkBashisms29= verify checkBashisms "echo ${!var}"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
prop_checkBashisms41= verify checkBashisms "echo `<file`"
prop_checkBashisms42= verify checkBashisms "trap foo int"
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
checkBashisms params = bashism
where where
errMsg id s = err id 2040 $ "In sh, " ++ s ++ " not supported, even when sh is actually bash." isDash = shellType params == Dash
warnMsg id s = warn id 2039 $ "In POSIX sh, " ++ s ++ " not supported." warnMsg id s =
bashism (T_ProcSub id _ _) = errMsg id "process substitution is" if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id "extglob is" bashism (T_Extglob id _ _) = warnMsg id "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is" bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is" bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
@@ -599,8 +630,10 @@ checkBashisms _ = bashism
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is" bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are" bashism (T_HereString id _) = warnMsg id "here-strings are"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef", "\\<", "\\>", "==" ] = | op `elem` [ "-nt", "-ef", "\\<", "\\>"] =
warnMsg id $ op ++ " is" unless isDash $ warnMsg id $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is"
bashism (TC_Unary id _ "-a" _) = bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is" warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
@@ -621,10 +654,11 @@ checkBashisms _ = bashism
warnMsg id $ fromJust str ++ " is" warnMsg id $ fromJust str ++ " is"
where where
str = getLiteralString t str = getLiteralString t
isBashism = isJust str && fromJust str `elem` bashVars isBashism = isJust str && isBashVariable (fromJust str)
bashism t@(T_DollarBraced id token) = do bashism t@(T_DollarBraced id token) = do
mapM_ check expansion mapM_ check expansion
when (var `elem` bashVars) $ warnMsg id $ var ++ " is" when (isBashVariable var) $
warnMsg id $ var ++ " is"
where where
str = bracedString t str = bracedString t
var = getBracedReference str var = getBracedReference str
@@ -640,10 +674,23 @@ checkBashisms _ = bashism
bashism (T_CoProc id _ _) = bashism (T_CoProc id _ _) =
warnMsg id "coproc is" warnMsg id "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && "-" `isPrefixOf` argString = | t `isCommand` "echo" && "-" `isPrefixOf` argString =
unless ("--" `isPrefixOf` argString) $ -- echo "-------" unless ("--" `isPrefixOf` argString) $ -- echo "-----"
warnMsg (getId arg) "echo flags are" if isDash
then
when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n"
else
warnMsg (getId arg) "echo flags are"
where argString = concat $ oversimplify arg where argString = concat $ oversimplify arg
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
@@ -655,31 +702,47 @@ checkBashisms _ = bashism
let name = fromMaybe "" $ getCommandName t let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t flags = getLeadingFlags t
in do in do
when (name `elem` bashCommands) $ warnMsg id $ "'" ++ name ++ "' is" when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is"
potentially $ do potentially $ do
allowed <- Map.lookup name allowedFlags allowed <- Map.lookup name allowedFlags
(word, flag) <- listToMaybe $ filter (\x -> snd x `notElem` allowed) flags (word, flag) <- listToMaybe $
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is" when (name == "source") $ warnMsg id "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = potentially $ do check token = potentially $ do
word <- liftM (map toLower) $ getLiteralString token str <- getLiteralString token
guard $ word `elem` ["err", "debug", "return"] let upper = map toUpper str
return $ warnMsg (getId token) $ "trapping " ++ word ++ " is" return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token)
"prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $
warnMsg (getId token)
"using lower/mixed case for signal names is"
in in
mapM_ check (reverse rest) mapM_ check (drop 1 rest)
when (name == "printf") $ potentially $ do
format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "printf %q is"
where where
bashCommands = [ unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "type", "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset" "typeset"
] ] ++ if not isDash then ["local", "type"] else []
allowedFlags = Map.fromList [ allowedFlags = Map.fromList [
("read", ["r"]), ("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"]), ("ulimit", ["f"]),
("echo", []), ("printf", []),
("exec", []) ("exec", [])
] ]
@@ -687,6 +750,7 @@ checkBashisms _ = bashism
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [ expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
@@ -694,9 +758,19 @@ checkBashisms _ = bashism
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is") (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
] ]
bashVars = [ bashVars = [
"RANDOM", "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SECONDS", "SHLVL", "PIPESTATUS", "SHELLOPTS" "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
] ]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
isBashVariable var =
var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var)
isAssigned var = any f (variableFlow params)
where
f x = case x of
Assignment (_, _, name, _) -> name == var
_ -> False
prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done" prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done" prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done"
@@ -822,6 +896,8 @@ prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/
prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol" prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol"
prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null" prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null"
prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar" prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
checkRedirectToSame params s@(T_Pipeline _ _ list) = checkRedirectToSame params s@(T_Pipeline _ _ list) =
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
where where
@@ -831,7 +907,8 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
when (exceptId /= newId when (exceptId /= newId
&& x == y && x == y
&& not (isOutput t && isOutput u) && not (isOutput t && isOutput u)
&& not (special t)) $ do && not (special t)
&& not (any isHarmlessCommand [t,u])) $ do
addComment $ note newId addComment $ note newId
addComment $ note exceptId addComment $ note exceptId
checkOccurrences _ _ = return () checkOccurrences _ _ = return ()
@@ -854,6 +931,11 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
T_DGREAT _ -> True T_DGREAT _ -> True
_ -> False _ -> False
_ -> False _ -> False
isHarmlessCommand arg = fromMaybe False $ do
cmd <- getClosestCommand (parentMap params) arg
name <- getCommandBasename cmd
return $ name `elem` ["echo", "printf", "sponge"]
checkRedirectToSame _ _ = return () checkRedirectToSame _ _ = return ()
@@ -901,7 +983,7 @@ checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree
err (getId x) 2068 err (getId x) 2068
"Double quote array expansions to avoid re-splitting elements." "Double quote array expansions to avoid re-splitting elements."
where where
-- Fixme: should detect whether the alterantive is quoted -- Fixme: should detect whether the alternative is quoted
isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b
isAlternative _ = False isAlternative _ = False
checkUnquotedDollarAt _ _ = return () checkUnquotedDollarAt _ _ = return ()
@@ -944,24 +1026,26 @@ prop_checkArrayWithoutIndex2 = verifyNotTree checkArrayWithoutIndex "foo='bar ba
prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo" prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo"
prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC" prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC"
prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo $a" prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo $a"
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
checkArrayWithoutIndex params _ = checkArrayWithoutIndex params _ =
concat $ doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) concat $ doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
where where
defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables
readF _ (T_DollarBraced id token) _ = do readF _ (T_DollarBraced id token) _ = do
map <- get map <- get
return . maybeToList $ do return . maybeToList $ do
name <- getLiteralString token name <- getLiteralString token
assignment <- Map.lookup name map assigned <- Map.lookup name map
return [makeComment WarningC id 2128 return [makeComment WarningC id 2128
"Expanding an array without an index only gives the first element."] "Expanding an array without an index only gives the first element."]
readF _ _ _ = return [] readF _ _ _ = return []
writeF _ t name (DataArray _) = do writeF _ t name (DataArray _) = do
modify (Map.insert name t) modify (Map.insert name ())
return [] return []
writeF _ expr name _ = do writeF _ expr name _ = do
if isIndexed expr if isIndexed expr
then modify (Map.insert name expr) then modify (Map.insert name ())
else modify (Map.delete name) else modify (Map.delete name)
return [] return []
@@ -1028,9 +1112,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
commandName = fromMaybe "" $ do commandName = fromMaybe "" $ do
cmd <- getClosestCommand parents t cmd <- getClosestCommand parents t
name <- getCommandBasename cmd name <- getCommandBasename cmd
if name == "find" return $ if name == "find" then getFindCommand cmd else name
then return $ getFindCommand cmd
else return name
isProbablyOk = isProbablyOk =
any isOkAssignment (take 3 $ getPath parents t) any isOkAssignment (take 3 $ getPath parents t)
@@ -1071,8 +1153,9 @@ checkSingleQuotedVariables _ _ = return ()
prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi" prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]" prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow" prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
checkUnquotedN _ (T_Condition _ SingleBracket (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t]))) | willSplit t = prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
err id 2070 "Always true because you failed to quote. Use [[ ]] instead." checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
checkUnquotedN _ _ = return () checkUnquotedN _ _ = return ()
prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]" prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"
@@ -1082,9 +1165,9 @@ prop_checkNumberComparisons4 = verify checkNumberComparisons "[[ $foo > 2.72 ]]"
prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]" prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]"
prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]" prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]"
prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]" prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]"
prop_checkNumberComparisons8 = verify checkNumberComparisons "[[ foo <= bar ]]" prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]"
prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]" prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"
prop_checkNumberComparisons11= verify checkNumberComparisons "[[ $foo -eq 'N' ]]" prop_checkNumberComparisons11= verify checkNumberComparisons "[ $foo -eq 'N' ]"
prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]" prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]"
checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
if isNum lhs && not (isNonNum rhs) if isNum lhs && not (isNonNum rhs)
@@ -1106,15 +1189,15 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do
mapM_ checkDecimals [lhs, rhs] mapM_ checkDecimals [lhs, rhs]
checkStrings [lhs, rhs] when (typ == SingleBracket) $
checkStrings [lhs, rhs]
where where
isLtGt = flip elem ["<", "\\<", ">", "\\>"] isLtGt = flip elem ["<", "\\<", ">", "\\>"]
isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="] isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="]
supportsDecimals = (shellType params) == Ksh
checkDecimals hs = checkDecimals hs =
when (isFraction hs && not supportsDecimals) $ when (isFraction hs && not (hasFloatingPoint params)) $
err (getId hs) 2072 decimalError err (getId hs) 2072 decimalError
decimalError = "Decimals are not supported. " ++ decimalError = "Decimals are not supported. " ++
"Either use integers only, or use bc or awk to compare." "Either use integers only, or use bc or awk to compare."
@@ -1127,8 +1210,8 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
return . not . all numChar $ s return . not . all numChar $ s
numChar x = isDigit x || x `elem` "+-. " numChar x = isDigit x || x `elem` "+-. "
stringError t = err (getId t) 2130 $ stringError t = err (getId t) 2170 $
op ++ " is for integer comparisons. Use " ++ seqv op ++ " instead." "Numerical " ++ op ++ " does not dereference in [..]. Expand or use string operator."
isNum t = isNum t =
case oversimplify t of case oversimplify t of
@@ -1204,16 +1287,26 @@ checkConditionalAndOrs _ t =
otherwise -> return () otherwise -> return ()
prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]" prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]"
prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ 'cow' ]]" prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]"
prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]" prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"
prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"
prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]"
checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) = checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
case rhs of case rhs of
T_NormalWord id [T_DoubleQuoted _ _] -> error id T_NormalWord id [T_DoubleQuoted _ _] -> error rhs
T_NormalWord id [T_SingleQuoted _ _] -> error id T_NormalWord id [T_SingleQuoted _ _] -> error rhs
_ -> return () _ -> return ()
where where
error id = err id 2076 "Don't quote rhs of =~, it'll match literally rather than as a regex." error t =
unless (isConstantNonRe t) $
err (getId t) 2076
"Don't quote rhs of =~, it'll match literally rather than as a regex."
re = mkRegex "[][*.+()]"
hasMetachars s = s `matches` re
isConstantNonRe t = fromMaybe False $ do
s <- getLiteralString t
return . not $ hasMetachars s
checkQuotedCondRegex _ _ = return () checkQuotedCondRegex _ _ = return ()
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]" prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
@@ -1229,16 +1322,23 @@ checkGlobbedRegex _ _ = return ()
prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]" prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]"
prop_checkConstantIfs2 = verify checkConstantIfs "[[ n -le 4 ]]" prop_checkConstantIfs2a= verify checkConstantIfs "[ n -le 4 ]"
prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n -ge 2 ]]" prop_checkConstantIfs2b= verifyNot checkConstantIfs "[[ n -le 4 ]]"
prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n != 2 ]]"
prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]" prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"
prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]" prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"
checkConstantIfs _ (TC_Binary id typ op lhs rhs) prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]"
| op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] = prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"
when (isJust lLit && isJust rLit) $ warn id 2050 "This expression is constant. Did you forget the $ on a variable?" checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
where when (isJust lLit && isJust rLit) $
lLit = getLiteralString lhs warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
rLit = getLiteralString rhs where
lLit = getLiteralString lhs
rLit = getLiteralString rhs
isDynamic =
op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
&& typ == DoubleBracket
|| op `elem` [ "-nt", "-ot", "-ef"]
checkConstantIfs _ _ = return () checkConstantIfs _ _ = return ()
prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]" prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]"
@@ -1249,13 +1349,13 @@ prop_checkLiteralBreakingTest5 = verify checkLiteralBreakingTest "[ -n \"$(true)
prop_checkLiteralBreakingTest6 = verify checkLiteralBreakingTest "[ -z $(true)z ]" prop_checkLiteralBreakingTest6 = verify checkLiteralBreakingTest "[ -z $(true)z ]"
prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]" prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]"
prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]" prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]"
prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"
checkLiteralBreakingTest _ t = potentially $ checkLiteralBreakingTest _ t = potentially $
case t of case t of
(TC_Noary _ _ w@(T_NormalWord _ l)) -> do (TC_Noary _ _ w@(T_NormalWord _ l)) -> do
guard . not $ isConstant w guard . not $ isConstant w -- Covered by SC2078
comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings." comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings."
(TC_Unary _ _ op w@(T_NormalWord _ l)) -> do (TC_Unary _ _ op w@(T_NormalWord _ l)) ->
guard $ not $ isConstant w
case op of case op of
"-n" -> tautologyWarning w "Argument to -n is always true due to literal strings." "-n" -> tautologyWarning w "Argument to -n is always true due to literal strings."
"-z" -> tautologyWarning w "Argument to -z is always false due to literal strings." "-z" -> tautologyWarning w "Argument to -z is always false due to literal strings."
@@ -1312,7 +1412,8 @@ checkBraceExpansionVars _ (T_BraceExpansion id list) = mapM_ check list
checkBraceExpansionVars _ _ = return () checkBraceExpansionVars _ _ = return ()
prop_checkForDecimals = verify checkForDecimals "((3.14*c))" prop_checkForDecimals = verify checkForDecimals "((3.14*c))"
checkForDecimals _ t@(TA_Expansion id _) = potentially $ do checkForDecimals params t@(TA_Expansion id _) = potentially $ do
guard $ not (hasFloatingPoint params)
str <- getLiteralString t str <- getLiteralString t
first <- str !!! 0 first <- str !!! 0
guard $ isDigit first && '.' `elem` str guard $ isDigit first && '.' `elem` str
@@ -1562,17 +1663,21 @@ checkPrintfVar _ = checkUnqualifiedCommand "printf" (const f) where
-- Check whether a word is entirely output from a single command -- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ _] -> True T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ _]] -> True T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
T_NormalWord id [T_Backticked _ _] -> True T_NormalWord id [T_Backticked _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ _]] -> True T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
_ -> False _ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`" prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\"" prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\"" prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\"" prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'." msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token) f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
@@ -1586,6 +1691,7 @@ prop_checkUuoeVar5 = verify checkUuoeVar "foo \"$(echo \"$(date) value:\" $value
prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\"" prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\""
prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005 prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005
prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)" prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)"
prop_checkUuoeVar9 = verify checkUuoeVar "foo $(echo $(<file))"
checkUuoeVar _ p = checkUuoeVar _ p =
case p of case p of
T_Backticked id [cmd] -> check id cmd T_Backticked id [cmd] -> check id cmd
@@ -1723,8 +1829,22 @@ checkTimeParameters _ = checkUnqualifiedCommand "time" f where
prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1" prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"
prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1" prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"
prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo" prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo"
checkTestRedirects _ (T_Redirecting id redirs@(redir:_) cmd) | cmd `isCommand` "test" = prop_checkTestRedirects4 = verifyNot checkTestRedirects "test 1 -eq 2 2> file"
warn (getId redir) 2065 "This is interpretted as a shell file redirection, not a comparison." checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
mapM_ check redirs
where
check t =
when (suspicious t) $
warn (getId t) 2065 "This is interpreted as a shell file redirection, not a comparison."
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
isComparison t =
case t of
T_Greater _ -> True
T_Less _ -> True
otherwise -> False
checkTestRedirects _ _ = return () checkTestRedirects _ _ = return ()
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
@@ -1808,7 +1928,8 @@ checkPS1Assignments _ _ = return ()
prop_checkBackticks1 = verify checkBackticks "echo `foo`" prop_checkBackticks1 = verify checkBackticks "echo `foo`"
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)" prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
checkBackticks _ (T_Backticked id _) = prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
checkBackticks _ (T_Backticked id list) | not (null list) =
style id 2006 "Use $(..) instead of legacy `..`." style id 2006 "Use $(..) instead of legacy `..`."
checkBackticks _ _ = return () checkBackticks _ _ = return ()
@@ -2096,6 +2217,7 @@ leadType shell parents t =
lastCreatesSubshell = lastCreatesSubshell =
case shell of case shell of
Bash -> True Bash -> True
Dash -> True
Sh -> True Sh -> True
Ksh -> False Ksh -> False
@@ -2144,7 +2266,7 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
"export" -> if "f" `elem` flags "export" -> if "f" `elem` flags
then [] then []
else concatMap getReference rest else concatMap getReference rest
"declare" -> if "x" `elem` flags "declare" -> if any (`elem` flags) ["x", "p"]
then concatMap getReference rest then concatMap getReference rest
else [] else []
"readonly" -> concatMap getReference rest "readonly" -> concatMap getReference rest
@@ -2305,6 +2427,10 @@ getReferencedVariables t =
TC_Unary id _ "-v" token -> getIfReference t token TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
then concatMap (getIfReference t) [lhs, rhs]
else []
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op] [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
@@ -2330,6 +2456,8 @@ getReferencedVariables t =
when (isDigit $ head str) $ fail "is a number" when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
-- Try to get referenced variables from a literal string like "$foo" -- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices. -- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 = prop_getVariablesFromLiteral1 =
@@ -2445,6 +2573,8 @@ prop_checkSpacefulness27= verifyNotTree checkSpacefulness "echo ${a:+'foo'}"
prop_checkSpacefulness28= verifyNotTree checkSpacefulness "exec {n}>&1; echo $n" prop_checkSpacefulness28= verifyNotTree checkSpacefulness "exec {n}>&1; echo $n"
prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>&-;" prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>&-;"
prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;" prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"
prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""
prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
checkSpacefulness params t = checkSpacefulness params t =
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params) doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
@@ -2461,7 +2591,7 @@ checkSpacefulness params t =
readF _ token name = do readF _ token name = do
spaced <- hasSpaces name spaced <- hasSpaces name
return [makeComment InfoC (getId token) 2086 warning | return [makeComment InfoC (getId token) 2086 warning |
spaced isExpansion token && spaced
&& not (isArrayExpansion token) -- There's another warning for this && not (isArrayExpansion token) -- There's another warning for this
&& not (isCounting token) && not (isCounting token)
&& not (isQuoteFree parents token) && not (isQuoteFree parents token)
@@ -2483,6 +2613,11 @@ checkSpacefulness params t =
parents = parentMap params parents = parentMap params
isExpansion t =
case t of
(T_DollarBraced _ _ ) -> True
_ -> False
isCounting (T_DollarBraced id token) = isCounting (T_DollarBraced id token) =
case concat $ oversimplify token of case concat $ oversimplify token of
'#':_ -> True '#':_ -> True
@@ -2525,6 +2660,7 @@ prop_checkQuotesInLiterals6 = verifyTree checkQuotesInLiterals "param='my\\ file
prop_checkQuotesInLiterals6a= verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd" prop_checkQuotesInLiterals6a= verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd"
prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param" prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param"
prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param" prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param"
prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm ${#param}"
checkQuotesInLiterals params t = checkQuotesInLiterals params t =
doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)
where where
@@ -2556,12 +2692,18 @@ checkQuotesInLiterals params t =
then return $ getId t then return $ getId t
else Nothing else Nothing
squashesQuotes t =
case t of
T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t
otherwise -> False
readF _ expr name = do readF _ expr name = do
assignment <- getQuotes name assignment <- getQuotes name
return return
(if isJust assignment (if isJust assignment
&& not (isParamTo parents "eval" expr) && not (isParamTo parents "eval" expr)
&& not (isQuoteFree parents expr) && not (isQuoteFree parents expr)
&& not (squashesQuotes expr)
then [ then [
makeComment WarningC (fromJust assignment) 2089 makeComment WarningC (fromJust assignment) 2089
"Quotes/backslashes will be treated literally. Use an array.", "Quotes/backslashes will be treated literally. Use an array.",
@@ -2643,6 +2785,9 @@ prop_checkUnused23= verifyNotTree checkUnusedAssignments "a=1; [ -R a ]"
prop_checkUnused24= verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}" prop_checkUnused24= verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}"
prop_checkUnused25= verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}" prop_checkUnused25= verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}"
prop_checkUnused26= verifyNotTree checkUnusedAssignments "declare -F foo" prop_checkUnused26= verifyNotTree checkUnusedAssignments "declare -F foo"
prop_checkUnused27= verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]"
prop_checkUnused28= verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]"
prop_checkUnused29= verifyNotTree checkUnusedAssignments "var=(a b); declare -p var"
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
where where
flow = variableFlow params flow = variableFlow params
@@ -2753,7 +2898,7 @@ checkUnassignedReferences params t = warnings
isGuarded _ = False isGuarded _ = False
match var candidate = match var candidate =
if var /= candidate && (map toLower var) == (map toLower candidate) if var /= candidate && map toLower var == map toLower candidate
then 1 then 1
else dist var candidate else dist var candidate
@@ -2765,9 +2910,7 @@ checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =
mapM_ check $ takeWhile (not . isEndOfArgs) args mapM_ check $ takeWhile (not . isEndOfArgs) args
where where
check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" = check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" =
info id 2035 $ info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options."
"Use ./" ++ concat (oversimplify v)
++ " so names with dashes won't become options."
check _ = return () check _ = return ()
isEndOfArgs t = isEndOfArgs t =
@@ -2923,11 +3066,18 @@ checkLoopKeywordScope params t |
subshellType t = case leadType (shellType params) (parentMap params) t of subshellType t = case leadType (shellType params) (parentMap params) t of
NoneScope -> Nothing NoneScope -> Nothing
SubshellScope str -> return str SubshellScope str -> return str
isFunction t = case t of T_Function {} -> True; _ -> False
relevant t = isLoop t || isFunction t || isJust (subshellType t) relevant t = isLoop t || isFunction t || isJust (subshellType t)
checkLoopKeywordScope _ _ = return () checkLoopKeywordScope _ _ = return ()
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope params t | t `isCommand` "local" && not (isInFunction t) =
err (getId t) 2168 "'local' is only valid in functions."
where
isInFunction t = any isFunction $ getPath (parentMap params) t
checkLocalScope _ _ = return ()
prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }" prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }"
prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }" prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }"
prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }" prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }"
@@ -2938,7 +3088,11 @@ checkFunctionDeclarations params
Ksh -> Ksh ->
when (hasKeyword && hasParens) $ when (hasKeyword && hasParens) $
err id 2111 "ksh does not allow 'function' keyword and '()' at the same time." err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."
Sh -> do Dash -> forSh
Sh -> forSh
where
forSh = do
when (hasKeyword && hasParens) $ when (hasKeyword && hasParens) $
warn id 2112 "'function' keyword is non-standard. Delete it." warn id 2112 "'function' keyword is non-standard. Delete it."
when (hasKeyword && not hasParens) $ when (hasKeyword && not hasParens) $
@@ -3417,6 +3571,8 @@ prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCd "set -e; cd ~/src; rm -r
prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi" prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi"
prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi" prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi"
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .." 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 = checkUncheckedCd params root =
if hasSetE then [] else execWriter $ doAnalysis checkElement root if hasSetE then [] else execWriter $ doAnalysis checkElement root
where where
@@ -3430,9 +3586,12 @@ checkUncheckedCd params root =
hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root
isSetE t = isSetE t =
case t of case t of
T_SimpleCommand {} -> T_Script _ str _ -> str `matches` re
t `isUnqualifiedCommand` "set" && "e" `elem` map snd (getAllFlags t) T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t || "e" `elem` map snd (getAllFlags t))
_ -> False _ -> False
re = mkRegex "[[:space:]]-[^-]*e"
prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done" 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" prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"
@@ -3448,7 +3607,7 @@ checkLoopVariableReassignment params token =
next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path
return $ do return $ do
warn (getId token) 2165 "This nested loop overrides the index variable of its parent." warn (getId token) 2165 "This nested loop overrides the index variable of its parent."
warn (getId next) 2165 "This parent loop has its index variable overridden." warn (getId next) 2167 "This parent loop has its index variable overridden."
path = drop 1 $ getPath (parentMap params) token path = drop 1 $ getPath (parentMap params) token
loopVariable :: Token -> Maybe String loopVariable :: Token -> Maybe String
loopVariable t = loopVariable t =
@@ -3461,5 +3620,90 @@ checkLoopVariableReassignment params token =
_ _ _ -> return var _ _ _ -> return var
_ -> fail "not loop" _ -> fail "not loop"
prop_checkTrailingBracket1 = verify checkTrailingBracket "if -z n ]]; then true; fi "
prop_checkTrailingBracket2 = verifyNot checkTrailingBracket "if [[ -z n ]]; then true; fi "
prop_checkTrailingBracket3 = verify checkTrailingBracket "a || b ] && thing"
prop_checkTrailingBracket4 = verifyNot checkTrailingBracket "run [ foo ]"
prop_checkTrailingBracket5 = verifyNot checkTrailingBracket "run bar ']'"
checkTrailingBracket _ token =
case token of
T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token
otherwise -> return ()
where
check t command =
case t of
T_NormalWord id [T_Literal _ str] -> potentially $ do
guard $ str `elem` [ "]]", "]" ]
let opposite = invert str
parameters = oversimplify command
guard $ opposite `notElem` parameters
return $ warn id 2171 $
"Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?"
otherwise -> return ()
invert s =
case s of
"]]" -> "[["
"]" -> "["
x -> x
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
checkNonportableSignals _ = checkUnqualifiedCommand "trap" (const f)
where
f = mapM_ check
check param = potentially $ do
str <- getLiteralString param
let id = getId param
return $ sequence_ $ mapMaybe (\f -> f id str) [
checkNumeric,
checkUntrappable
]
checkNumeric id str = do
guard $ not (null str)
guard $ all isDigit str
guard $ str /= "0" -- POSIX exit trap
guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
return $ warn id 2172
"Trapping signals by number is not well defined. Prefer signal names."
checkUntrappable id str = do
guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
return $ err id 2173
"SIGKILL/SIGSTOP can not be trapped."
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
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"
checkMkdirDashPM _ t@(T_SimpleCommand _ _ args) = potentially $ do
name <- getCommandName t
guard $ name == "mkdir"
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
guard $ any couldHaveSubdirs (drop 1 args) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
where
flags = getAllFlags t
couldHaveSubdirs t = fromMaybe True $ do
name <- getLiteralString t
return $ '/' `elem` name
checkMkdirDashPM _ _ = return ()
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -147,6 +147,9 @@ prop_canSourceBadSyntax =
prop_cantSourceDynamic = prop_cantSourceDynamic =
[1090] == checkWithIncludes [("lib", "")] ". \"$1\"" [1090] == checkWithIncludes [("lib", "")] ". \"$1\""
prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canSourceDynamicWhenRedirected = prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""

View File

@@ -25,13 +25,14 @@ internalVariables = [
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE", "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS", "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
"TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC", "TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
-- Other -- Other
"USER", "TZ", "TERM" "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
] ]
variablesWithoutSpaces = [ variablesWithoutSpaces = [
@@ -41,6 +42,12 @@ variablesWithoutSpaces = [
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES" "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
] ]
arrayVariables = [
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
]
commonCommands = [ commonCommands = [
"admin", "alias", "ar", "asa", "at", "awk", "basename", "batch", "admin", "alias", "ar", "asa", "at", "awk", "basename", "batch",
"bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp", "bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",
@@ -76,13 +83,12 @@ sampleWords = [
] ]
shellForExecutable :: String -> Maybe Shell shellForExecutable :: String -> Maybe Shell
shellForExecutable "sh" = return Sh shellForExecutable name =
shellForExecutable "ash" = return Sh case name of
shellForExecutable "dash" = return Sh "sh" -> return Sh
"bash" -> return Bash
shellForExecutable "ksh" = return Ksh "dash" -> return Dash
shellForExecutable "ksh88" = return Ksh "ksh" -> return Ksh
shellForExecutable "ksh93" = return Ksh "ksh88" -> return Ksh
"ksh93" -> return Ksh
shellForExecutable "bash" = return Bash otherwise -> Nothing
shellForExecutable _ = Nothing

View File

@@ -27,15 +27,15 @@ import GHC.Exts
import System.Info import System.Info
import System.IO import System.IO
format :: IO Formatter format :: FormatterOptions -> IO Formatter
format = return Formatter { format options = return Formatter {
header = return (), header = return (),
footer = return (), footer = return (),
onFailure = outputError, onFailure = outputError options,
onResult = outputResult onResult = outputResult options
} }
colorForLevel level = colorForLevel level =
case level of case level of
"error" -> 31 -- red "error" -> 31 -- red
"warning" -> 33 -- yellow "warning" -> 33 -- yellow
@@ -44,13 +44,13 @@ colorForLevel level =
"message" -> 1 -- bold "message" -> 1 -- bold
"source" -> 0 -- none "source" -> 0 -- none
otherwise -> 0 -- none otherwise -> 0 -- none
outputError file error = do outputError options file error = do
color <- getColorFunc color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult result contents = do outputResult options result contents = do
color <- getColorFunc color <- getColorFunc $ foColorOption options
let comments = crComments result let comments = crComments result
let fileLines = lines contents let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines let lineCount = fromIntegral $ length fileLines
@@ -75,10 +75,15 @@ cuteIndent comment =
code code = "SC" ++ show code code code = "SC" ++ show code
getColorFunc = do getColorFunc colorOption = do
term <- hIsTerminalDevice stdout term <- hIsTerminalDevice stdout
let windows = "mingw" `isPrefixOf` os let windows = "mingw" `isPrefixOf` os
return $ if term && not windows then colorComment else const id let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return $ if useColor then colorComment else const id
where where
colorComment level comment = colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear ansi (colorForLevel level) ++ comment ++ clear

View File

@@ -72,8 +72,15 @@ data AnalysisResult = AnalysisResult {
arComments :: [TokenComment] arComments :: [TokenComment]
} }
-- Formatter options
data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption
}
-- Supporting data types -- Supporting data types
data Shell = Ksh | Sh | Bash deriving (Show, Eq) data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
@@ -90,6 +97,12 @@ data Comment = Comment Severity Code String deriving (Show, Eq)
data PositionedComment = PositionedComment Position Comment deriving (Show, Eq) data PositionedComment = PositionedComment Position Comment deriving (Show, Eq)
data TokenComment = TokenComment Id Comment deriving (Show, Eq) data TokenComment = TokenComment Id Comment deriving (Show, Eq)
data ColorOption =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing -- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface { mockedSystemInterface files = SystemInterface {

View File

@@ -359,19 +359,36 @@ readConditionContents single =
readCondBinaryOp = try $ do readCondBinaryOp = try $ do
optional guardArithmetic optional guardArithmetic
id <- getNextId id <- getNextId
op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp op <- getOp
spacingOrLf spacingOrLf
return op return op
where where
tryOp s = try $ do flaglessOps = [ "==", "!=", "<=", ">=", "=~", ">", "<", "=" ]
id <- getNextId
string s getOp = do
return $ TC_Binary id typ s
otherOp = try $ do
id <- getNextId id <- getNextId
op <- anyQuotedOp <|> anyEscapedOp <|> 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 s
anyOp = flagOp <|> flaglessOp <|> fail
"Expected comparison operator (don't wrap commands in []/[[]])"
flagOp = try $ do
s <- readOp s <- readOp
when (s == "-a" || s == "-o") $ fail "Unexpected operator" when (s == "-a" || s == "-o") $ fail "Unexpected operator"
return $ TC_Binary id typ s return s
flaglessOp =
choice $ map (try . string) flaglessOps
escaped s = if any (`elem` s) "<>" then '\\':s else s
guardArithmetic = do guardArithmetic = do
try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ") try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")
@@ -394,10 +411,17 @@ readConditionContents single =
return $ TC_Unary id typ s return $ TC_Unary id typ s
readOp = try $ do readOp = try $ do
char '-' char '-' <|> weirdDash
s <- many1 letter s <- many1 letter <|> fail "Expected a test operator"
return ('-':s) return ('-':s)
weirdDash = do
pos <- getPosition
oneOf "\x058A\x05BE\x2010\x2011\x2012\x2013\x2014\x2015\xFE63\xFF0D"
parseProblemAt pos ErrorC 1100
"This is a unicode dash. Delete and retype as ASCII minus."
return '-'
readCondWord = do readCondWord = do
notFollowedBy2 (try (spacing >> string "]")) notFollowedBy2 (try (spacing >> string "]"))
x <- readNormalWord x <- readNormalWord
@@ -429,6 +453,7 @@ readConditionContents single =
return $ TC_Or id typ x return $ TC_Or id typ x
readAndOrOp op requiresSpacing = do readAndOrOp op requiresSpacing = do
optional $ lookAhead weirdDash
x <- string op x <- string op
condSpacing requiresSpacing condSpacing requiresSpacing
return x return x
@@ -598,7 +623,7 @@ readArithmeticContents =
readDoubleQuoted, readDoubleQuoted,
readNormalDollar, readNormalDollar,
readBraced, readBraced,
readBackTicked, readUnquotedBackTicked,
readNormalLiteral "+-*/=%^,]?:" readNormalLiteral "+-*/=%^,]?:"
] ]
spacing spacing
@@ -706,6 +731,10 @@ prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"
prop_readCondition11= isOk readCondition "[[ a == b ||\n c == d ]]" prop_readCondition11= isOk readCondition "[[ a == b ||\n c == d ]]"
prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]" prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]"
prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]" prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
prop_readCondition14= isOk readCondition "[ foo '>' bar ]"
prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]"
prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
readCondition = called "test expression" $ do readCondition = called "test expression" $ do
opos <- getPosition opos <- getPosition
id <- getNextId id <- getNextId
@@ -725,7 +754,7 @@ readCondition = called "test expression" $ do
condition <- readConditionContents single condition <- readConditionContents single
cpos <- getPosition cpos <- getPosition
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here" close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?" when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?" when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
spacing spacing
@@ -805,7 +834,7 @@ readNormalWordPart end = do
readGlob, readGlob,
readNormalDollar, readNormalDollar,
readBraced, readBraced,
readBackTicked, readUnquotedBackTicked,
readProcSub, readProcSub,
readNormalLiteral end, readNormalLiteral end,
readLiteralCurlyBraces readLiteralCurlyBraces
@@ -841,7 +870,7 @@ readDollarBracedWord = do
list <- many readDollarBracedPart list <- many readDollarBracedPart
return $ T_NormalWord id list return $ T_NormalWord id list
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readBackTicked <|> readDollarBracedLiteral readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readUnquotedBackTicked <|> readDollarBracedLiteral
readDollarBracedLiteral = do readDollarBracedLiteral = do
id <- getNextId id <- getNextId
@@ -899,15 +928,18 @@ readSingleQuotedPart =
readSingleEscaped readSingleEscaped
<|> many1 (noneOf "'\\\x2018\x2019") <|> many1 (noneOf "'\\\x2018\x2019")
prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"
prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`" prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`"
prop_readBackTicked3 = isWarning readBackTicked "´grep \"\\\"\"´" prop_readBackTicked2 = isOk (readBackTicked False) "`grep \"\\\"\"`"
prop_readBackTicked4 = isOk readBackTicked "`echo foo\necho bar`" prop_readBackTicked3 = isWarning (readBackTicked False) "´grep \"\\\"\"´"
prop_readBackTicked4 = isOk readSimpleCommand "`echo foo\necho bar`"
prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar" prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar"
prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar" prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar"
prop_readBackTicked7 = isOk readSimpleCommand "`#inline comment`" prop_readBackTicked7 = isOk readSimpleCommand "`#inline comment`"
prop_readBackTicked8 = isOk readSimpleCommand "echo `#comment` \\\nbar baz" prop_readBackTicked8 = isOk readSimpleCommand "echo `#comment` \\\nbar baz"
readBackTicked = called "backtick expansion" $ do readQuotedBackTicked = readBackTicked True
readUnquotedBackTicked = readBackTicked False
readBackTicked quoted = called "backtick expansion" $ do
id <- getNextId id <- getNextId
startPos <- getPosition startPos <- getPosition
backtick backtick
@@ -926,6 +958,7 @@ readBackTicked = called "backtick expansion" $ do
return $ T_Backticked id result return $ T_Backticked id result
where where
unEscape [] = [] unEscape [] = []
unEscape ('\\':'"':rest) | quoted = '"' : unEscape rest
unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest
unEscape ('\\':'\n':rest) = unEscape rest unEscape ('\\':'\n':rest) = unEscape rest
unEscape (c:rest) = c : unEscape rest unEscape (c:rest) = c : unEscape rest
@@ -993,7 +1026,7 @@ suggestForgotClosingQuote startPos endPos name = do
parseProblemAt endPos InfoC 1079 parseProblemAt endPos InfoC 1079
"This is actually an end quote, but due to next char it looks suspect." "This is actually an end quote, but due to next char it looks suspect."
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked
readDoubleQuotedLiteral = do readDoubleQuotedLiteral = do
doubleQuote doubleQuote
@@ -1020,6 +1053,9 @@ prop_readGlob2 = isOk readGlob "[^0-9]"
prop_readGlob3 = isOk readGlob "[a[:alpha:]]" prop_readGlob3 = isOk readGlob "[a[:alpha:]]"
prop_readGlob4 = isOk readGlob "[[:alnum:]]" prop_readGlob4 = isOk readGlob "[[:alnum:]]"
prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]" prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"
prop_readGlob6 = isOk readGlob "[\\|]"
prop_readGlob7 = isOk readGlob "[^[]"
prop_readGlob8 = isOk readGlob "[*?]"
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
where where
readSimple = do readSimple = do
@@ -1030,11 +1066,11 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
readClass = try $ do readClass = try $ do
id <- getNextId id <- getNextId
char '[' char '['
s <- many1 (predefined <|> liftM return (letter <|> digit <|> oneOf globchars)) s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars)
char ']' char ']'
return $ T_Glob id $ "[" ++ concat s ++ "]" return $ T_Glob id $ "[" ++ concat s ++ "]"
where where
globchars = "^-_:?*.,!~@#$%=+{}/~" globchars = liftM return . oneOf $ "!$[" ++ extglobStartChars
predefined = do predefined = do
try $ string "[:" try $ string "[:"
s <- many1 letter s <- many1 letter
@@ -1152,12 +1188,19 @@ prop_readBraced3 = isOk readBraced "{1,\\},2}"
prop_readBraced4 = isOk readBraced "{1,{2,3}}" prop_readBraced4 = isOk readBraced "{1,{2,3}}"
prop_readBraced5 = isOk readBraced "{JP{,E}G,jp{,e}g}" prop_readBraced5 = isOk readBraced "{JP{,E}G,jp{,e}g}"
prop_readBraced6 = isOk readBraced "{foo,bar,$((${var}))}" prop_readBraced6 = isOk readBraced "{foo,bar,$((${var}))}"
prop_readBraced7 = isNotOk readBraced "{}"
prop_readBraced8 = isNotOk readBraced "{foo}"
readBraced = try braceExpansion readBraced = try braceExpansion
where where
braceExpansion = braceExpansion =
T_BraceExpansion `withParser` do T_BraceExpansion `withParser` do
char '{' char '{'
elements <- bracedElement `sepBy1` char ',' elements <- bracedElement `sepBy1` char ','
guard $
case elements of
(_:_:_) -> True
[t] -> ".." `isInfixOf` onlyLiteralString t
[] -> False
char '}' char '}'
return elements return elements
bracedElement = bracedElement =
@@ -1529,7 +1572,11 @@ readSimpleCommand = called "simple command" $ do
readSource :: Monad m => SourcePos -> Token -> SCParser m Token readSource :: Monad m => SourcePos -> Token -> SCParser m Token
readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do
override <- getSourceOverride override <- getSourceOverride
let literalFile = override `mplus` getLiteralString file let literalFile = do
name <- override `mplus` getLiteralString file
-- Hack to avoid 'source ~/foo' trying to read from literal tilde
guard . not $ "~/" `isPrefixOf` name
return name
case literalFile of case literalFile of
Nothing -> do Nothing -> do
parseNoteAt pos WarningC 1090 parseNoteAt pos WarningC 1090
@@ -1933,6 +1980,9 @@ prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}"
prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }" prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }" prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)" prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"
prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"
prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"
readFunctionDefinition = called "function" $ do readFunctionDefinition = called "function" $ do
functionSignature <- try readFunctionSignature functionSignature <- try readFunctionSignature
allspacing allspacing
@@ -1950,8 +2000,11 @@ readFunctionDefinition = called "function" $ do
whitespace whitespace
spacing spacing
name <- readFunctionName name <- readFunctionName
spacing spaces <- spacing
hasParens <- wasIncluded readParens hasParens <- wasIncluded readParens
when (not hasParens && null spaces) $
acceptButWarn (lookAhead (oneOf "{("))
ErrorC 1095 "You need a space or linefeed between the function name and body."
return $ T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name return $ T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name
readWithoutFunction = try $ do readWithoutFunction = try $ do
@@ -2064,6 +2117,8 @@ prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= " prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
prop_readAssignmentWord9a= isOk readAssignmentWord "foo=" prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42" prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
@@ -2082,7 +2137,7 @@ readAssignmentWord = try $ do
isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof)) isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof))
if not hasLeftSpace && (hasRightSpace || isEndOfCommand) if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
then do then do
when (variable /= "IFS" && hasRightSpace) $ when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
parseNoteAt pos WarningC 1007 parseNoteAt pos WarningC 1007
"Remove space after = if trying to assign a value (for empty string, use var='' ... )." "Remove space after = if trying to assign a value (for empty string, use var='' ... )."
value <- readEmptyLiteral value <- readEmptyLiteral
@@ -2334,6 +2389,7 @@ readScript = do
isWarning p s = parsesCleanly p s == Just False isWarning p s = parsesCleanly p s == Just False
isOk p s = parsesCleanly p s == Just True isOk p s = parsesCleanly p s == Just True
isNotOk p s = parsesCleanly p s == Nothing
testParse string = runIdentity $ do testParse string = runIdentity $ do
(res, _) <- runParser (mockedSystemInterface []) readScript "-" string (res, _) <- runParser (mockedSystemInterface []) readScript "-" string

BIN
doc/emacs-flycheck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
doc/terminal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
doc/vim-syntastic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

5
quickrun Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
# quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling.
runghc -idist/build/autogen shellcheck.hs "$@"

15
quicktest Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# quicktest runs the ShellCheck unit tests in an interpreted mode.
# This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth.
(
var=$(echo 'liftM and $ sequence [ShellCheck.Analytics.runTests, ShellCheck.Parser.runTests, ShellCheck.Checker.runTests]' | cabal repl | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then
exit 0
else
grep -C 3 "Fail" <<< "$var"
exit 1
fi
) 2>&1

View File

@@ -16,7 +16,7 @@ errors and pitfalls where the shell just gives a cryptic error message or
strange behavior, but it also reports on a few more advanced issues where strange behavior, but it also reports on a few more advanced issues where
corner cases can cause delayed failures. corner cases can cause delayed failures.
ShellCheck gives shell specific advice. Consider the line: ShellCheck gives shell specific advice. Consider this line:
(( area = 3.14*r*r )) (( area = 3.14*r*r ))
@@ -32,6 +32,12 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS # OPTIONS
**-C**[*WHEN*],\ **--color**[=*WHEN*]
: For TTY outut, enable colors *always*, *never* or *auto*. The default
is *auto*. **--color** without an argument is equivalent to
**--color=always**.
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...] **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
: Explicitly exclude the specified codes from the report. Subsequent **-e** : Explicitly exclude the specified codes from the report. Subsequent **-e**
@@ -46,7 +52,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-s**\ *shell*,\ **--shell=***shell* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash* and *ksh*. : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
The default is to use the file's shebang, or *bash* if the target shell The default is to use the file's shebang, or *bash* if the target shell
can't be determined. can't be determined.
@@ -54,7 +60,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Print version information and exit. : Print version information and exit.
**-x**,\ **-external-sources** **-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input. : Follow 'source' statements even when the file is not specified as input.
By default, `shellcheck` will only follow files specified on the command By default, `shellcheck` will only follow files specified on the command
@@ -159,6 +165,16 @@ The environment variable `SHELLCHECK_OPTS` can be set with default flags:
Its value will be split on spaces and prepended to the command line on each Its value will be split on spaces and prepended to the command line on each
invocation. invocation.
# RETURN VALUES
ShellCheck uses the follow exit codes:
+ 0: All files successfully scanned with no issues.
+ 1: All files successfully scanned with some issues.
+ 2: Some files could not be processed (e.g. file not found).
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# AUTHOR # AUTHOR
ShellCheck is written and maintained by Vidar Holen. ShellCheck is written and maintained by Vidar Holen.

View File

@@ -48,7 +48,6 @@ data Flag = Flag String String
data Status = data Status =
NoProblems NoProblems
| SomeProblems | SomeProblems
| BadInput
| SupportFailure | SupportFailure
| SyntaxFailure | SyntaxFailure
| RuntimeException | RuntimeException
@@ -60,12 +59,16 @@ instance Monoid Status where
data Options = Options { data Options = Options {
checkSpec :: CheckSpec, checkSpec :: CheckSpec,
externalSources :: Bool externalSources :: Bool,
formatterOptions :: FormatterOptions
} }
defaultOptions = Options { defaultOptions = Options {
checkSpec = emptyCheckSpec, checkSpec = emptyCheckSpec,
externalSources = False externalSources = False,
formatterOptions = FormatterOptions {
foColorOption = ColorAuto
}
} }
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@@ -74,8 +77,11 @@ options = [
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings", (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
Option "f" ["format"] Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") "output format", (ReqArg (Flag "format") "FORMAT") "output format",
Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)",
Option "s" ["shell"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)", (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)",
Option "x" ["external-sources"] Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.", (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
Option "V" ["version"] Option "V" ["version"]
@@ -92,12 +98,12 @@ parseArguments argv =
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
throwError SyntaxFailure throwError SyntaxFailure
formats :: Map.Map String (IO Formatter) formats :: FormatterOptions -> Map.Map String (IO Formatter)
formats = Map.fromList [ formats options = Map.fromList [
("checkstyle", ShellCheck.Formatter.CheckStyle.format), ("checkstyle", ShellCheck.Formatter.CheckStyle.format),
("gcc", ShellCheck.Formatter.GCC.format), ("gcc", ShellCheck.Formatter.GCC.format),
("json", ShellCheck.Formatter.JSON.format), ("json", ShellCheck.Formatter.JSON.format),
("tty", ShellCheck.Formatter.TTY.format) ("tty", ShellCheck.Formatter.TTY.format options)
] ]
getOption [] _ = Nothing getOption [] _ = Nothing
@@ -144,7 +150,6 @@ statusToCode status =
case status of case status of
NoProblems -> ExitSuccess NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1 SomeProblems -> ExitFailure 1
BadInput -> ExitFailure 5
SyntaxFailure -> ExitFailure 3 SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4 SupportFailure -> ExitFailure 4
RuntimeException -> ExitFailure 2 RuntimeException -> ExitFailure 2
@@ -154,12 +159,13 @@ process flags files = do
options <- foldM (flip parseOption) defaultOptions flags options <- foldM (flip parseOption) defaultOptions flags
verifyFiles files verifyFiles files
let format = fromMaybe "tty" $ getOption flags "format" let format = fromMaybe "tty" $ getOption flags "format"
let formatters = formats $ formatterOptions options
formatter <- formatter <-
case Map.lookup format formats of case Map.lookup format formatters of
Nothing -> do Nothing -> do
printErr $ "Unknown format " ++ format printErr $ "Unknown format " ++ format
printErr "Supported formats:" printErr "Supported formats:"
mapM_ (printErr . write) $ Map.keys formats mapM_ (printErr . write) $ Map.keys formatters
throwError SupportFailure throwError SupportFailure
where write s = " " ++ s where write s = " " ++ s
Just f -> ExceptT $ fmap Right f Just f -> ExceptT $ fmap Right f
@@ -197,6 +203,13 @@ runFormatter sys format options files = do
then NoProblems then NoProblems
else SomeProblems else SomeProblems
parseColorOption colorOption =
case colorOption of
"auto" -> ColorAuto
"always" -> ColorAlways
"never" -> ColorNever
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
parseOption flag options = parseOption flag options =
case flag of case flag of
Flag "shell" str -> Flag "shell" str ->
@@ -221,11 +234,18 @@ parseOption flag options =
liftIO printVersion liftIO printVersion
throwError NoProblems throwError NoProblems
Flag "externals" _ -> do Flag "externals" _ ->
return options { return options {
externalSources = True externalSources = True
} }
Flag "color" color ->
return options {
formatterOptions = (formatterOptions options) {
foColorOption = parseColorOption color
}
}
_ -> return options _ -> return options
where where
die s = do die s = do