98 Commits
v0.4.0 ... bats

Author SHA1 Message Date
Vidar Holen
ad1a0da954 Attempted bats @test support 2016-05-22 18:53:35 -07:00
Vidar Holen
47fd16b8e8 Stable version 0.4.4
This release is dedicated to AlphaGo.
The second golden age of AI is upon us!
2016-05-15 13:53:37 -07:00
Vidar Holen
1d04754b37 Don't warn about a && b || c in if/while/until. 2016-05-14 17:14:32 -07:00
Vidar Holen
13ff0a7432 Warn when arrays are appended/assigned scalars. 2016-05-14 16:24:18 -07:00
Vidar Holen
40136fe249 Fix parsing of [[ a =~ {$var} ]] 2016-05-08 12:19:25 -07:00
Vidar Holen
86999ded1f Improve 'let' parsing, trigger unused var for ((a=1)) 2016-04-30 13:45:39 -07:00
Vidar Holen
7551a241ad Add missing warnings for {$i..10} similar to {1..$i} 2016-04-23 16:00:56 -07:00
Vidar Holen
2f0ae44de4 Fix parsing of here documents 2016-04-16 19:14:02 -07:00
Vidar Holen
51d8caf2c9 Add missing import 2016-04-16 09:53:15 -07:00
Vidar Holen
f835c2d4c1 Fix handling of spaces in shebangs. 2016-04-16 09:42:07 -07:00
Vidar Holen
db0c8c2dc9 Separate out command specific checks.
The checks use a better interface and give
an overall speed boost of 10%.
2016-04-10 17:01:40 -07:00
Vidar Holen
9911470d67 Don't warn about LINENO in dash 2016-04-09 09:56:07 -07:00
Vidar Holen
a5821c3a4d s/sh/bash/ in SC2176 because of posix guarantees. 2016-03-19 16:47:55 -07:00
Vidar Holen
c91083354f Warn about timing pipelines and compound commands in sh/dash. 2016-03-19 16:14:55 -07:00
Vidar Holen
2957fb64c9 Allow parsing 'time ( foo )' 2016-03-19 16:13:54 -07:00
koalaman
459e30804f Merge pull request #622 from Arguggi/master
Add stack support
2016-03-19 14:50:48 -07:00
koalaman
49569e10e6 Merge pull request #628 from benmwebb/patch-1
Fix a handful of typos.
2016-03-11 09:02:02 -08:00
Ben Webb
ba0221a1da Fix a handful of typos. 2016-03-10 21:27:07 -08:00
Vidar Holen
944313c6ba Directives after the shebang now apply to the entire script.
Also adds support for the shell= directive.
2016-03-08 20:16:16 -08:00
Vidar Holen
6af1aeb259 Add warning for multi-digit FDs in posix/dash. 2016-03-08 17:37:12 -08:00
Arguggi
b7c9d23452 Add stack support 2016-03-02 15:38:02 +01:00
koalaman
e792d69293 Merge pull request #608 from cs-shadow/master
Fix broken links in README
2016-02-17 09:25:56 -08:00
Chandan Singh
4d8f2eb707 Fix broken links in README
Presently, the links pointed to by "GCC error compatibility" and
"CheckStyle compatible XML output" are broken due to improper
way of referring files. This change fixes those links.

Also, this commit removes some trailing whitespaces in README.
2016-02-16 18:09:15 +00:00
Vidar Holen
8a3bd25f7c Improve error for missing final ) in $((foo) 2016-02-07 14:16:40 -08:00
Vidar Holen
825c1b5d22 Support parsing $((( as $( ((, with warning. 2016-02-06 22:19:29 -08:00
Vidar Holen
92473b512a Add warning for trailing spaces after \ breaks. 2016-02-01 20:20:34 -08:00
Vidar Holen
7e75d12ce1 Improve expansion in single quote false positives. 2016-01-26 19:57:18 -08:00
Vidar Holen
7d278c3ca1 Ignore SC2055 if rhs of either != is a glob. 2016-01-26 19:12:47 -08:00
Vidar Holen
5f1175fb58 Add special case warning to quote in eval echo {1..$n} 2016-01-25 18:46:55 -08:00
koalaman
257b794322 Merge pull request #578 from jwilk/master
Fix typo
2016-01-19 17:08:32 -08:00
Jakub Wilk
89572d3a96 Fix typo 2016-01-20 01:00:22 +01:00
koalaman
15edcbd4d5 Merge pull request #577 from neil-greenwood/readme-sed-quote
Fix missing quote in README.md
2016-01-18 09:00:53 -08:00
Neil Greenwood
736febaa3c Update README.md
Fix missing quote in `sed` example.
2016-01-18 14:53:15 +00:00
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
24 changed files with 2333 additions and 1188 deletions

2
.gitignore vendored
View File

@@ -12,4 +12,4 @@ cabal-dev
.cabal-sandbox/
cabal.sandbox.config
cabal.config
.stack-work

276
README.md
View File

@@ -1,26 +1,67 @@
# 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
Licensed under the GNU General Public License, v3
![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png).
The goals of ShellCheck are:
The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues,
that causes a shell to give cryptic error messages.
- To point out and clarify typical beginner's syntax issues
that cause a shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems,
that causes a shell to behave strangely and counter-intuitively.
- To point out and clarify typical intermediate level semantic problems
that cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls, that may cause an
- To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances.
ShellCheck is written in Haskell, and requires 2 GB of memory to compile.
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](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 its [CheckStyle compatible XML output](shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
## 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 install shellcheck
@@ -29,54 +70,60 @@ On Debian based distros:
apt-get install shellcheck
On Fedora based distros:
dnf install ShellCheck
On OS X with homebrew:
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 section 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 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
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"
Verify that your PATH is set up correctly:
Log out and in again, and verify that your PATH is set up correctly:
$ which shellcheck
~/.cabal/bin/shellcheck
@@ -93,18 +140,175 @@ In Powershell ISE, you may need to additionally update the output encoding:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
## Running tests
#### Running tests
To run the unit test suite:
cabal configure --enable-tests
cabal build
cabal test
$ 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
Please use the Github issue tracker for any bugs or feature suggestions:
Please use the GitHub issue tracker for any bugs or feature suggestions:
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!

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
data Token =
TA_Binary Id String Token Token
| TA_Assignment Id String Token Token
| TA_Expansion Id [Token]
| TA_Index Id Token
| TA_Sequence Id [Token]
@@ -127,9 +128,14 @@ data Token =
| T_CoProc Id (Maybe String) Token
| T_CoProcBody Id Token
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
| T_BatsTest Id Token Token
deriving (Show)
data Annotation = DisableComment Integer | SourceOverride String deriving (Show, Eq)
data Annotation =
DisableComment Integer
| SourceOverride String
| ShellOverride String
deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
-- This is an abomination.
@@ -162,7 +168,7 @@ analyze f g i =
dll l m v = do
x <- roundAll l
y <- roundAll m
return $ v x m
return $ v x y
d1 t v = do
x <- round t
return $ v x
@@ -246,6 +252,7 @@ analyze f g i =
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
delve (TA_Sequence id l) = dl l $ TA_Sequence id
delve (TA_Trinary id t1 t2 t3) = do
@@ -259,6 +266,7 @@ analyze f g i =
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
delve (T_Include id includer script) = d2 includer script $ T_Include id
delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id
delve t = return t
getId t = case t of
@@ -340,6 +348,7 @@ getId t = case t of
TC_Unary id _ _ _ -> id
TC_Noary id _ _ -> id
TA_Binary id _ _ _ -> id
TA_Assignment id _ _ _ -> id
TA_Unary id _ _ -> id
TA_Sequence id _ -> id
TA_Trinary id _ _ _ -> id
@@ -356,6 +365,7 @@ getId t = case t of
T_CoProc id _ _ -> id
T_CoProcBody id _ -> id
T_Include id _ _ -> id
T_BatsTest id _ _ -> id
blank :: Monad m => Token -> m ()
blank = const $ return ()

View File

@@ -93,14 +93,17 @@ oversimplify token =
-- 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)) =
let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ oversimplify x)) args in
concatMap flag textArgs
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
in
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
where
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args
flag _ = []
flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
-- Get all flags in a GNU way, up until --
@@ -224,6 +227,16 @@ isAssignment t =
T_Annotation _ _ w -> isAssignment w
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
-- the body of while loops and if statements.
getCommandSequences t =

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,18 @@
-}
module ShellCheck.Analyzer (analyzeScript) where
import ShellCheck.Interface
import ShellCheck.Analytics
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import Data.List
import qualified ShellCheck.Checks.Commands
-- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript = runAnalytics
analyzeScript spec = AnalysisResult {
arComments =
filterByAnnotation (asScript spec) . nub $
runAnalytics spec
++ ShellCheck.Checks.Commands.runChecks spec
}

646
ShellCheck/AnalyzerLib.hs Normal file
View File

@@ -0,0 +1,646 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Arrow (first)
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
type Analysis = ReaderT Parameters (Writer [TokenComment]) ()
data Parameters = Parameters {
variableFlow :: [StackData],
parentMap :: Map.Map Id Token,
shellType :: Shell,
shellTypeSpecified :: Bool
}
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
data StackData =
StackScope Scope
| StackScopeEnd
-- (Base expression, specific position, var name, assigned values)
| Assignment (Token, Token, String, DataType)
| Reference (Token, Token, String)
deriving (Show)
data DataType = DataString DataSource | DataArray DataSource
deriving (Show)
data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
deriving (Show)
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec {
asScript = root,
asShellType = Nothing,
asExecutionMode = Executed
}
pScript s =
let
pSpec = ParseSpec {
psFilename = "script",
psScript = s
}
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
TokenComment id $ Comment severity code note
addComment note = tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str
err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str
makeParameters spec =
let params = Parameters {
shellType = fromMaybe (determineShell root) $ asShellType spec,
shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root,
variableFlow =
getVariableFlow (shellType params) (parentMap params) root
} in params
where root = asScript spec
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
prop_determineShell4 = determineShell (fromJust $ pScript
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
prop_determineShell5 = determineShell (fromJust $ pScript
"#shellcheck shell=sh\nfoo") == Sh
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
determineShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString
where
forAnnotation t =
case t of
(ShellOverride s) -> return s
_ -> fail ""
getCandidates :: Token -> [Maybe String]
getCandidates t@(T_Script {}) = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++
[Just $ fromShebang s]
fromShebang (T_Script _ s t) = shellFor s
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
--- Context seeking
getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
where
pre t = modify (first ((:) t))
post t = do
(_:rest, map) <- get
case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
getTokenMap t =
execState (doAnalysis f t) Map.empty
where
f t = modify (Map.insert (getId t) t)
-- Is this node self quoting for a regular element?
isQuoteFree = isQuoteFreeNode False
-- Is this node striclty self quoting, for array expansions
isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment {} -> return True
T_FdRedirect {} -> return True
_ -> Nothing
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
case t of
TC_Noary _ DoubleBracket _ -> return True
TC_Unary _ DoubleBracket _ _ -> return True
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Redirecting {} -> return $
if strict then False else
-- Not true, just a hack to prevent warning about non-expansion refs
any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
T_CaseExpression {} -> return True
T_HereDoc {} -> return True
T_DollarBraced {} -> return True
-- When non-strict, pragmatically assume it's desirable to split here
T_ForIn {} -> return (not strict)
T_SelectIn {} -> return (not strict)
_ -> Nothing
isParamTo tree cmd =
go
where
go x = case Map.lookup (getId x) tree of
Nothing -> False
Just parent -> check parent
check t =
case t of
T_SingleQuoted _ _ -> go t
T_DoubleQuoted _ _ -> go t
T_NormalWord _ _ -> go t
T_SimpleCommand {} -> isCommand t cmd
T_Redirecting {} -> isCommand t cmd
_ -> False
getClosestCommand tree t =
msum . map getCommand $ getPath tree t
where
getCommand t@(T_Redirecting {}) = return t
getCommand _ = Nothing
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where
go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_)
| currentId == getId word = True
go _ _ = False
-- A list of the element and all its parents
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child
parents params = getPath (parentMap params)
pathTo t = do
parents <- reader parentMap
return $ getPath parents t
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
T_NormalWord id [T_Backticked _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
_ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
-- TODO: Replace this with a proper Control Flow Graph
getVariableFlow shell parents t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack
where
startScope t =
let scopeType = leadType shell parents t
in do
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
when (assignFirst t) $ setWritten t
endScope t =
let scopeType = leadType shell parents t
in do
setRead t
unless (assignFirst t) $ setWritten t
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
assignFirst (T_ForIn {}) = True
assignFirst (T_SelectIn {}) = True
assignFirst (T_BatsTest {}) = True
assignFirst _ = False
setRead t =
let read = getReferencedVariables parents t
in mapM_ (\v -> modify (Reference v:)) read
setWritten t =
let written = getModifiedVariables t
in mapM_ (\v -> modify (Assignment v:)) written
leadType shell parents t =
case t of
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
T_Backticked _ _ -> SubshellScope "`..` expansion"
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
T_Subshell _ _ -> SubshellScope "(..) group"
T_BatsTest {} -> SubshellScope "@bats test"
T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} ->
if fromMaybe False causesSubshell
then SubshellScope "pipeline"
else NoneScope
_ -> NoneScope
where
parentPipeline = do
parent <- Map.lookup (getId t) parents
case parent of
T_Pipeline {} -> return parent
_ -> Nothing
causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline
if length list <= 1
then return False
else if lastCreatesSubshell
then return True
else return . not $ (getId . head $ reverse list) == getId t
lastCreatesSubshell =
case shell of
Bash -> True
Dash -> True
Sh -> True
Ksh -> False
getModifiedVariables t =
case t of
T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of
T_Assignment id _ name _ w ->
[(x, x, name, dataTypeFrom DataString w)]
_ -> []
) vars
c@(T_SimpleCommand {}) ->
getModifiedVariableCommand c
TA_Unary _ "++|" var -> maybeToList $ do
name <- getLiteralString var
return (t, t, name, DataString $ SourceFrom [t])
TA_Unary _ "|++" var -> maybeToList $ do
name <- getLiteralString var
return (t, t, name, DataString $ SourceFrom [t])
TA_Assignment _ op lhs rhs -> maybeToList $ do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
name <- getLiteralString lhs
return (t, t, name, DataString $ SourceFrom [rhs])
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal)
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> []
isClosingFileOp op =
case op of
T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if any (`elem` flags) ["x", "p"]
then concatMap getReference rest
else []
"readonly" -> concatMap getReference rest
"trap" ->
case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
_ -> []
_ -> []
where
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = []
flags = map snd $ getAllFlags base
getReferencedVariableCommand _ = []
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"read" ->
let params = map getLiteral rest in
catMaybes . takeWhile isJust . reverse $ params
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
_ -> []
"let" -> concatMap letParamToLiteral rest
"export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> declaredVars
"local" -> concatMap getModifierParamString rest
"readonly" -> concatMap getModifierParamString rest
"set" -> maybeToList $ do
params <- getSetParams rest
return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest
_ -> []
where
flags = map snd $ getAllFlags base
stripEquals s = let rest = dropWhile (/= '=') s in
if rest == "" then "" else tail rest
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t
declaredVars = concatMap (getModifierParam defaultType) rest
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, DataString SourceExternal)
getModifierParamString = getModifierParam DataString
getModifierParam def t@(T_Assignment _ _ name _ value) =
[(base, t, name, dataTypeFrom def value)]
getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
name <- getLiteralString t
guard $ isVariableName name
return (base, t, name, def SourceDeclaration)
getModifierParam _ _ = []
letParamToLiteral token =
if var == ""
then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
getSetParams (t:rest) =
let s = getLiteralString t in
case s of
Just "--" -> return rest
Just ('-':_) -> getSetParams rest
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
f (_:rest) = f rest
f [] = fail "not found"
-- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. Here we cheat and
-- just get the last one, if it's a variable name.
getMapfileArray base arguments = do
lastArg <- listToMaybe (reverse arguments)
name <- getLiteralString lastArg
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
getReferencedVariables parents t =
case t of
T_DollarBraced id l -> let str = bracedString t in
(t, t, getBracedReference str) :
map (\x -> (l, l, x)) (getIndexReferences str)
TA_Expansion id _ ->
if isArithmeticAssignment t
then []
else getIfReference t t
T_Assignment id mode str _ word ->
[(t, t, str) | mode == Append] ++ specialReferences str t word
TC_Unary id _ "-v" 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_BatsTest {} -> [ -- pretend @test references vars to avoid warnings
(t, t, "lines"),
(t, t, "status"),
(t, t, "output")
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x
where
-- Try to reduce false positives for unused vars only referenced from evaluated vars
specialReferences name base word =
if name `elem` [
"PS1", "PS2", "PS3", "PS4",
"PROMPT_COMMAND"
]
then
map (\x -> (base, base, x)) $
getVariablesFromLiteralToken word
else []
literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x
literalizer _ = Nothing
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
guard . not $ null str
when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" _ _ :_ -> True
_ -> False
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
--- Command specific checks
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ do
cmd <- getCommandName token
return $ matcher cmd
isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) =
if c `elem` "*@#?-$!" then return [c] else fail "not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest
guard $ suffix /= rest -- e.g. ${!@}
first <- suffix !!! 0
guard $ first `elem` "*?"
return ""
nameExpansion _ = Nothing
-- Useful generic functions
potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ())
headOrDefault _ (a:_) = a
headOrDefault def _ = def
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
filterByAnnotation token =
filter (not . shouldIgnore)
where
idFor (TokenComment id _) = id
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ idFor note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
shouldIgnoreFor _ _ = False
parents = getParentTree token
getCode (TokenComment _ (Comment _ c _)) = c
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

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

View File

@@ -0,0 +1,560 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (runChecks
, ShellCheck.Checks.Commands.runTests
) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data CommandName = Exactly String | Basename String
deriving (Eq, Ord)
data CommandCheck =
CommandCheck CommandName (Token -> Analysis)
nullCheck :: Token -> Analysis
nullCheck _ = return ()
verify :: CommandCheck -> String -> Bool
verify f s = producesComments f s == Just True
verifyNot f s = producesComments f s == Just False
producesComments :: CommandCheck -> String -> Maybe Bool
producesComments f s = do
root <- pScript s
return . not . null $ runList (defaultSpec root) [f]
composeChecks f g t = do
f t
g t
arguments (T_SimpleCommand _ _ (cmd:args)) = args
commandChecks :: [CommandCheck]
commandChecks = [
checkTr
,checkFindNameGlob
,checkNeedlessExpr
,checkGrepRe
,checkTrapQuotes
,checkReturn
,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes
,checkInjectableFindSh
,checkFindActionPrecedence
,checkMkdirDashPM
,checkNonportableSignals
,checkInteractiveSu
,checkSshCommandString
,checkPrintfVar
,checkUuoeCmd
,checkSetAssignment
,checkExportedExpansions
,checkAliasesUsesArgs
,checkAliasesExpandEarly
]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty
where
addCheck map (CommandCheck name function) =
Map.insertWith' composeChecks name function map
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
name <- getLiteralString cmd
return $
if '/' `elem` name
then
Map.findWithDefault nullCheck (Basename $ basename name) map t
else do
Map.findWithDefault nullCheck (Exactly name) map t
Map.findWithDefault nullCheck (Basename name) map t
where
basename = reverse . takeWhile (/= '/') . reverse
checkCommand _ _ = return ()
runList spec list = notes
where
root = asScript spec
params = makeParameters spec
notes = execWriter $ runReaderT (doAnalysis (checkCommand map) root) params
map = buildCommandMap list
runChecks spec = runList spec commandChecks
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
prop_checkTr5 = verify checkTr "tr foo bar"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion."
f word =
case getLiteralString word of
Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."
Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
unless ("[:" `isPrefixOf` s) $
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets."
Nothing -> return ()
duplicated s =
let relevant = filter isAlpha s
in relevant /= nub relevant
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
acceptsGlob _ = False
f [] = return ()
f [x] = return ()
f (a:b:r) = do
when (acceptsGlob (getLiteralString a) && isGlob b) $ do
let (Just s) = getLiteralString a
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
f (b:r)
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t =
when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ]
words = mapMaybe getLiteralString
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where
-- --regex=*(extglob) doesn't work. Fixme?
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
skippable _ = False
f [] = return ()
f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
f (re:_) = do
when (isGlob re) $
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
let string = concat $ oversimplify re
if isConfusedGlobRegex string then
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
else potentially $ do
char <- getSuspiciousRegexWildcard string
return $ info (getId re) 2022 $
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
wordStartingWith c =
head . filter ([c] `isPrefixOf`) $ candidates
where
candidates =
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
getSuspiciousRegexWildcard str =
if not $ str `matches` contra
then do
match <- matchRegex suspicious str
str <- match !!! 0
str !!! 0
else
fail "looks good"
where
suspicious = mkRegex "([A-Za-z1-9])\\*"
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
f (x:_) = checkTrap x
f _ = return ()
checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs
checkTrap _ = return ()
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
checkExpansions (T_DollarExpansion id _) = warning id
checkExpansions (T_Backticked id _) = warning id
checkExpansions (T_DollarBraced id _) = warning id
checkExpansions (T_DollarArithmetic id _) = warning id
checkExpansions _ = return ()
prop_checkReturn1 = verifyNot checkReturn "return"
prop_checkReturn2 = verifyNot checkReturn "return 1"
prop_checkReturn3 = verifyNot checkReturn "return $var"
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments)
where
f (first:second:_) =
err (getId second) 2151
"Only one integer 0-255 can be returned. Use stdout for other data."
f [value] =
when (isInvalid $ literal value) $
err (getId value) 2152
"Can only return 0-255. Other data should be written to stdout."
f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|| let value = (read s :: Integer) in value > 255
literal token = fromJust $ getLiteralStringExt lit token
lit (T_DollarBraced {}) = return "0"
lit (T_DollarArithmetic {}) = return "0"
lit (T_DollarExpansion {}) = return "0"
lit (T_Backticked {}) = return "0"
lit _ = return "WTF"
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
where
f = void . sequence . mapMaybe check . tails
check (exec:arg:term:_) = do
execS <- getLiteralString exec
termS <- getLiteralString term
cmdS <- getLiteralStringExt (const $ return " ") arg
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
guard $ cmdS `matches` commandRegex
return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
check _ = Nothing
commandRegex = mkRegex "[ |;]"
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
return ()
where allButLast = reverse . drop 1 . reverse $ args
f args = mapM_ checkEscapes args
checkEscapes (T_NormalWord _ args) =
mapM_ checkEscapes args
checkEscapes (T_DoubleQuoted id args) =
mapM_ checkEscapes args
checkEscapes (T_Literal id str) = examine id str
checkEscapes (T_SingleQuoted id str) = examine id str
checkEscapes _ = return ()
examine id str =
when (str `matches` hasEscapes) $
info id 2028 "echo won't expand escape sequences. Consider printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
where
check args = do
let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
match pattern idStrings
match _ [] = return ()
match [] (next:_) = action next
match (p:tests) ((id, arg):args) = do
when (p arg) $ match tests args
match (p:tests) args
pattern = [
(`elem` ["-exec", "-execdir"]),
(`elem` ["sh", "bash", "ksh"]),
(== "-c")
]
action (id, arg) =
when ("{}" `isInfixOf` arg) $
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
where
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
f list | length list < length pattern = return ()
f list@(_:rest) =
if and (zipWith ($) pattern list)
then warnFor (list !! (length pattern - 1))
else f rest
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
isParam strs t = fromMaybe False $ do
param <- getLiteralString t
return $ param `elem` strs
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
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 = CommandCheck (Basename "mkdir") check
where
check t = potentially $ do
let flags = getAllFlags t
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
couldHaveSubdirs t = fromMaybe True $ do
name <- getLiteralString t
return $ '/' `elem` name
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 = CommandCheck (Exactly "trap") (f . arguments)
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_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
checkInteractiveSu = CommandCheck (Basename "su") f
where
f cmd = when (length (arguments cmd) <= 1) $ do
path <- pathTo cmd
when (all undirected path) $
info (getId cmd) 2117
"To run commands as another user, use su -c or sudo."
undirected (T_Pipeline _ _ l) = length l <= 1
-- This should really just be modifications to stdin, but meh
undirected (T_Redirecting _ list _) = null list
undirected _ = True
-- This is hard to get right without properly parsing ssh args
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
where
nonOptions =
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
f args =
case nonOptions args of
(hostport:r@(_:_)) -> checkArg $ last r
_ -> return ()
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
case filter (not . isConstant) parts of
[] -> return ()
(x:_) -> info (getId x) 2029
"Note that, unescaped, this expands on the client side."
checkArg _ = return ()
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
f (format:params) = check format
f _ = return ()
check format =
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
warn (getId format) 2059
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
f _ = return ()
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where
f (var:value:rest) =
let str = literal var in
when (isVariableName str || isAssignment str) $
msg (getId var)
f (var:_) =
when (isAssignment $ literal var) $
msg (getId var)
f _ = return ()
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
isAssignment str = '=' `elem` str
literal (T_NormalWord _ l) = concatMap literal l
literal (T_Literal _ str) = str
literal _ = "*"
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
where
check = mapM_ checkForVariables
checkForVariables f =
case getWordParts f of
[t@(T_DollarBraced {})] ->
warn (getId t) 2163 "Exporting an expansion rather than a variable."
_ -> return ()
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
where
re = mkRegex "\\$\\{?[0-9*@]"
f = mapM_ checkArg
checkArg arg =
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
when ('=' `elem` string && string `matches` re) $
err (getId arg) 2142
"Aliases can't use positional parameters. Use a function."
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
where
f = mapM_ checkArg
checkArg arg | '=' `elem` concat (oversimplify arg) =
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
checkArg _ = return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

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

View File

@@ -27,15 +27,15 @@ import GHC.Exts
import System.Info
import System.IO
format :: IO Formatter
format = return Formatter {
format :: FormatterOptions -> IO Formatter
format options = return Formatter {
header = return (),
footer = return (),
onFailure = outputError,
onResult = outputResult
onFailure = outputError options,
onResult = outputResult options
}
colorForLevel level =
colorForLevel level =
case level of
"error" -> 31 -- red
"warning" -> 33 -- yellow
@@ -44,13 +44,13 @@ colorForLevel level =
"message" -> 1 -- bold
"source" -> 0 -- none
otherwise -> 0 -- none
outputError file error = do
color <- getColorFunc
outputError options file error = do
color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult result contents = do
color <- getColorFunc
outputResult options result contents = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines
@@ -75,10 +75,15 @@ cuteIndent comment =
code code = "SC" ++ show code
getColorFunc = do
getColorFunc colorOption = do
term <- hIsTerminalDevice stdout
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
colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear

View File

@@ -72,8 +72,15 @@ data AnalysisResult = AnalysisResult {
arComments :: [TokenComment]
}
-- Formatter options
data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption
}
-- 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)
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 TokenComment = TokenComment Id Comment deriving (Show, Eq)
data ColorOption =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {

View File

@@ -52,20 +52,24 @@ type SCParser m v = ParsecT String UserState (SCBase m) v
backslash :: Monad m => SCParser m Char
backslash = char '\\'
linefeed = optional carriageReturn >> char '\n'
linefeed :: Monad m => SCParser m Char
linefeed = do
optional carriageReturn
c <- char '\n'
readPendingHereDocs
return c
singleQuote = char '\'' <|> unicodeSingleQuote
doubleQuote = char '"' <|> unicodeDoubleQuote
variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_"
functionChars = variableChars <|> oneOf ":+-.?"
specialVariable = oneOf "@*#?-$!"
tokenDelimiter = oneOf "&|;<> \t\n\r" <|> almostSpace
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
quotable = almostSpace <|> unicodeDoubleQuote <|> oneOf quotableChars
bracedQuotable = oneOf "}\"$`'"
doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars
doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars
whitespace = oneOf " \t\n" <|> carriageReturn <|> almostSpace
whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed
linewhitespace = oneOf " \t" <|> almostSpace
suspectCharAfterQuotes = variableChars <|> char '%'
@@ -138,15 +142,24 @@ data Context =
| ContextSource String
deriving (Show)
data HereDocContext =
HereDocPending Token -- on linefeed, read this T_HereDoc
| HereDocBoundary -- but don't consider heredocs before this
deriving (Show)
data UserState = UserState {
lastId :: Id,
positionMap :: Map.Map Id SourcePos,
parseNotes :: [ParseNote]
parseNotes :: [ParseNote],
hereDocMap :: Map.Map Id [Token],
pendingHereDocs :: [HereDocContext]
}
initialUserState = UserState {
lastId = Id $ -1,
positionMap = Map.empty,
parseNotes = []
parseNotes = [],
hereDocMap = Map.empty,
pendingHereDocs = []
}
codeForParseNote (ParseNote _ _ code _) = code
@@ -155,7 +168,6 @@ noteToParseNote map (Note id severity code message) =
where
pos = fromJust $ Map.lookup id map
getLastId = lastId <$> getState
getNextIdAt sourcepos = do
@@ -173,6 +185,58 @@ getNextId = do
pos <- getPosition
getNextIdAt pos
addToHereDocMap id list = do
state <- getState
let map = hereDocMap state
putState $ state {
hereDocMap = Map.insert id list map
}
withHereDocBoundary p = do
pushBoundary
do
v <- p
popBoundary
return v
<|> do
popBoundary
fail ""
where
pushBoundary = do
state <- getState
let docs = pendingHereDocs state
putState $ state {
pendingHereDocs = HereDocBoundary : docs
}
popBoundary = do
state <- getState
let docs = tail $ dropWhile (not . isHereDocBoundary) $
pendingHereDocs state
putState $ state {
pendingHereDocs = docs
}
addPendingHereDoc t = do
state <- getState
let docs = pendingHereDocs state
putState $ state {
pendingHereDocs = HereDocPending t : docs
}
popPendingHereDocs = do
state <- getState
let (pending, boundary) = break isHereDocBoundary $ pendingHereDocs state
putState $ state {
pendingHereDocs = boundary
}
return . map extract . reverse $ pendingHereDocs state
where
extract (HereDocPending t) = t
isHereDocBoundary x = case x of
HereDocBoundary -> True
otherwise -> False
getMap = positionMap <$> getState
getParseNotes = parseNotes <$> getState
@@ -359,19 +423,36 @@ readConditionContents single =
readCondBinaryOp = try $ do
optional guardArithmetic
id <- getNextId
op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp
op <- getOp
spacingOrLf
return op
where
tryOp s = try $ do
id <- getNextId
string s
return $ TC_Binary id typ s
otherOp = try $ do
flaglessOps = [ "==", "!=", "<=", ">=", "=~", ">", "<", "=" ]
getOp = do
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
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
try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")
@@ -394,10 +475,17 @@ readConditionContents single =
return $ TC_Unary id typ s
readOp = try $ do
char '-'
s <- many1 letter
char '-' <|> weirdDash
s <- many1 letter <|> fail "Expected a test operator"
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
notFollowedBy2 (try (spacing >> string "]"))
x <- readNormalWord
@@ -429,6 +517,7 @@ readConditionContents single =
return $ TC_Or id typ x
readAndOrOp op requiresSpacing = do
optional $ lookAhead weirdDash
x <- string op
condSpacing requiresSpacing
return x
@@ -497,8 +586,8 @@ readConditionContents single =
where
readGlobLiteral = do
id <- getNextId
s <- many1 (extglobStart <|> oneOf "{}[]$")
return $ T_Literal id s
s <- extglobStart <|> oneOf "{}[]$"
return $ T_Literal id [s]
readGroup = called "regex grouping" $ do
id <- getNextId
char '('
@@ -598,7 +687,7 @@ readArithmeticContents =
readDoubleQuoted,
readNormalDollar,
readBraced,
readBackTicked,
readUnquotedBackTicked,
readNormalLiteral "+-*/=%^,]?:"
]
spacing
@@ -619,7 +708,9 @@ readArithmeticContents =
l <- readAssignment `sepBy` (char ',' >> spacing)
return $ TA_Sequence id l
readAssignment = readTrinary `splitBy` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
readAssignment = chainr1 readTrinary readAssignmentOp
readAssignmentOp = readComboOp ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] TA_Assignment
readTrinary = do
x <- readLogicalOr
do
@@ -706,6 +797,10 @@ prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"
prop_readCondition11= isOk readCondition "[[ a == b ||\n c == d ]]"
prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]"
prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
prop_readCondition14= isOk readCondition "[ foo '>' bar ]"
prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]"
prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
readCondition = called "test expression" $ do
opos <- getPosition
id <- getNextId
@@ -725,7 +820,7 @@ readCondition = called "test expression" $ do
condition <- readConditionContents single
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 opos ErrorC 1034 "Did you mean [[ ?"
spacing
@@ -743,7 +838,7 @@ prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/
readAnnotation = called "shellcheck annotation" $ do
try readAnnotationPrefix
many1 linewhitespace
values <- many1 (readDisable <|> readSourceOverride)
values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride)
linefeed
many linewhitespace
return $ concat values
@@ -760,6 +855,14 @@ readAnnotation = called "shellcheck annotation" $ do
filename <- many1 $ noneOf " \n"
return [SourceOverride filename]
readShellOverride = forKey "shell" $ do
pos <- getPosition
shell <- many1 $ noneOf " \n"
when (isNothing $ shellForExecutable shell) $
parseNoteAt pos ErrorC 1103
"This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell]
forKey s p = do
try $ string s
char '='
@@ -782,6 +885,9 @@ prop_readNormalWord3 = isOk readNormalWord "foo#"
prop_readNormalWord4 = isOk readNormalWord "$\"foo\"$'foo\nbar'"
prop_readNormalWord5 = isWarning readNormalWord "${foo}}"
prop_readNormalWord6 = isOk readNormalWord "foo/{}"
prop_readNormalWord7 = isOk readNormalWord "foo\\\nbar"
prop_readNormalWord8 = isWarning readSubshell "(foo\\ \nbar)"
prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)"
readNormalWord = readNormalishWord ""
readNormalishWord end = do
@@ -805,7 +911,7 @@ readNormalWordPart end = do
readGlob,
readNormalDollar,
readBraced,
readBackTicked,
readUnquotedBackTicked,
readProcSub,
readNormalLiteral end,
readLiteralCurlyBraces
@@ -841,7 +947,7 @@ readDollarBracedWord = do
list <- many readDollarBracedPart
return $ T_NormalWord id list
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readBackTicked <|> readDollarBracedLiteral
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readUnquotedBackTicked <|> readDollarBracedLiteral
readDollarBracedLiteral = do
id <- getNextId
@@ -899,15 +1005,18 @@ readSingleQuotedPart =
readSingleEscaped
<|> many1 (noneOf "'\\\x2018\x2019")
prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"
prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"
prop_readBackTicked3 = isWarning readBackTicked "´grep \"\\\"\"´"
prop_readBackTicked4 = isOk readBackTicked "`echo foo\necho bar`"
prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`"
prop_readBackTicked2 = isOk (readBackTicked False) "`grep \"\\\"\"`"
prop_readBackTicked3 = isWarning (readBackTicked False) "´grep \"\\\"\"´"
prop_readBackTicked4 = isOk readSimpleCommand "`echo foo\necho bar`"
prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar"
prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar"
prop_readBackTicked7 = isOk readSimpleCommand "`#inline comment`"
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
startPos <- getPosition
backtick
@@ -926,6 +1035,7 @@ readBackTicked = called "backtick expansion" $ do
return $ T_Backticked id result
where
unEscape [] = []
unEscape ('\\':'"':rest) | quoted = '"' : unEscape rest
unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest
unEscape ('\\':'\n':rest) = unEscape rest
unEscape (c:rest) = c : unEscape rest
@@ -993,7 +1103,7 @@ suggestForgotClosingQuote startPos endPos name = do
parseProblemAt endPos InfoC 1079
"This is actually an end quote, but due to next char it looks suspect."
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked
readDoubleQuotedLiteral = do
doubleQuote
@@ -1020,6 +1130,9 @@ prop_readGlob2 = isOk readGlob "[^0-9]"
prop_readGlob3 = isOk readGlob "[a[:alpha:]]"
prop_readGlob4 = isOk readGlob "[[:alnum:]]"
prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"
prop_readGlob6 = isOk readGlob "[\\|]"
prop_readGlob7 = isOk readGlob "[^[]"
prop_readGlob8 = isOk readGlob "[*?]"
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
where
readSimple = do
@@ -1030,11 +1143,11 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
readClass = try $ do
id <- getNextId
char '['
s <- many1 (predefined <|> liftM return (letter <|> digit <|> oneOf globchars))
s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars)
char ']'
return $ T_Glob id $ "[" ++ concat s ++ "]"
where
globchars = "^-_:?*.,!~@#$%=+{}/~"
globchars = liftM return . oneOf $ "!$[" ++ extglobStartChars
predefined = do
try $ string "[:"
s <- many1 letter
@@ -1054,6 +1167,7 @@ readNormalEscaped = called "escaped char" $ do
backslash
do
next <- quotable <|> oneOf "?*@!+[]{}.,~#"
when (next == ' ') $ checkTrailingSpaces pos <|> return ()
return $ if next == '\n' then "" else [next]
<|>
do
@@ -1070,6 +1184,11 @@ readNormalEscaped = called "escaped char" $ do
escapedChar 'r' = Just "carriage return"
escapedChar _ = Nothing
checkTrailingSpaces pos = lookAhead . try $ do
many linewhitespace
void linefeed <|> eof
parseProblemAt pos ErrorC 1101 "Delete trailing spaces after \\ to break line (or use quotes for literal space)."
prop_readExtglob1 = isOk readExtglob "!(*.mp3)"
prop_readExtglob2 = isOk readExtglob "!(*.mp3|*.wmv)"
@@ -1152,12 +1271,19 @@ prop_readBraced3 = isOk readBraced "{1,\\},2}"
prop_readBraced4 = isOk readBraced "{1,{2,3}}"
prop_readBraced5 = isOk readBraced "{JP{,E}G,jp{,e}g}"
prop_readBraced6 = isOk readBraced "{foo,bar,$((${var}))}"
prop_readBraced7 = isNotOk readBraced "{}"
prop_readBraced8 = isNotOk readBraced "{foo}"
readBraced = try braceExpansion
where
braceExpansion =
T_BraceExpansion `withParser` do
char '{'
elements <- bracedElement `sepBy1` char ','
guard $
case elements of
(_:_:_) -> True
[t] -> ".." `isInfixOf` onlyLiteralString t
[] -> False
char '}'
return elements
bracedElement =
@@ -1174,7 +1300,10 @@ readBraced = try braceExpansion
readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
readDollarExpression = readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable
prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))"
prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)"
readDollarExpression = readTripleParenthesis "$" readDollarArithmetic readDollarExpansion <|> readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable
prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
readDollarSingleQuote = called "$'..' expression" $ do
@@ -1200,7 +1329,9 @@ readDollarArithmetic = called "$((..)) expression" $ do
id <- getNextId
try (string "$((")
c <- readArithmeticContents
string "))"
pos <- getPosition
char ')'
char ')' <|> fail "Expected a double )) to end the $((..))"
return (T_DollarArithmetic id c)
readDollarBracket = called "$[..] expression" $ do
@@ -1218,6 +1349,25 @@ readArithmeticExpression = called "((..)) command" $ do
string "))"
return (T_Arithmetic id c)
-- Check if maybe ((( was intended as ( (( rather than (( (
readTripleParenthesis prefix expected alternative = do
pos <- try . lookAhead $ do
string prefix
p <- getPosition
string "(((" -- should optimally be "((" but it's noisy and rarely helpful
return p
-- If the expected parser fails, try the alt.
-- If the alt fails, run the expected one again for the errors.
try expected <|> tryAlt pos <|> expected
where
tryAlt pos = do
t <- try alternative
parseNoteAt pos WarningC 1102 $
"Shells differ in parsing ambiguous " ++ prefix ++ "(((. Use spaces: " ++ prefix ++ "( (( ."
return t
prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }"
prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}"
readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
@@ -1300,14 +1450,17 @@ readDollarLonely = do
n <- lookAhead (anyChar <|> (eof >> return '_'))
return $ T_Literal id "$"
prop_readHereDoc = isOk readHereDoc "<< foo\nlol\ncow\nfoo"
prop_readHereDoc2 = isWarning readHereDoc "<<- EOF\n cow\n EOF"
prop_readHereDoc3 = isOk readHereDoc "<< foo\n$\"\nfoo"
prop_readHereDoc4 = isOk readHereDoc "<< foo\n`\nfoo"
prop_readHereDoc5 = isOk readHereDoc "<<- !foo\nbar\n!foo"
prop_readHereDoc6 = isOk readHereDoc "<< foo\\ bar\ncow\nfoo bar"
prop_readHereDoc7 = isOk readHereDoc "<< foo\n\\$(f ())\nfoo"
prop_readHereDoc8 = isOk readHereDoc "<<foo>>bar\netc\nfoo"
prop_readHereDoc = isOk readScript "cat << foo\nlol\ncow\nfoo"
prop_readHereDoc2 = isWarning readScript "cat <<- EOF\n cow\n EOF"
prop_readHereDoc3 = isOk readScript "cat << foo\n$\"\nfoo"
prop_readHereDoc4 = isOk readScript "cat << foo\n`\nfoo"
prop_readHereDoc5 = isOk readScript "cat <<- !foo\nbar\n!foo"
prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
readHereDoc = called "here document" $ do
fid <- getNextId
pos <- getPosition
@@ -1324,24 +1477,11 @@ readHereDoc = called "here document" $ do
liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
<|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
<|> (readToken >>= (\x -> return (Unquoted, x)))
spacing
startPos <- getPosition
hereData <- anyChar `reluctantlyTill` do
linefeed
spacing
string endToken
disregard linefeed <|> eof
do
linefeed
spaces <- spacing
verifyHereDoc dashed quoted spaces hereData
string endToken
parsedData <- parseHereData quoted startPos hereData
return $ T_FdRedirect fid "" $ T_HereDoc hid dashed quoted endToken parsedData
`attempting` (eof >> debugHereDoc tokenPosition endToken hereData)
-- add empty tokens for now, read the rest in readPendingHereDocs
let doc = T_HereDoc hid dashed quoted endToken []
addPendingHereDoc doc
return $ T_FdRedirect fid "" doc
where
stripLiteral (T_Literal _ x) = x
stripLiteral (T_SingleQuoted _ x) = x
@@ -1356,6 +1496,27 @@ readHereDoc = called "here document" $ do
c <- anyChar
return [c]
readPendingHereDocs = do
docs <- popPendingHereDocs
mapM_ readDoc docs
where
readDoc (T_HereDoc id dashed quoted endToken _) = do
pos <- getPosition
hereData <- anyChar `reluctantlyTill` do
spacing
string endToken
disregard (char '\n') <|> eof
do
spaces <- spacing
verifyHereDoc dashed quoted spaces hereData
string endToken
parsedData <- parseHereData quoted pos hereData
list <- parseHereData quoted pos hereData
addToHereDocMap id list
`attempting` (eof >> debugHereDoc pos endToken hereData)
parseHereData Quoted startPos hereData = do
id <- getNextIdAt startPos
return [T_Literal id hereData]
@@ -1440,7 +1601,7 @@ readHereString = called "here string" $ do
word <- readNormalWord
return $ T_FdRedirect id "" $ T_HereString id2 word
readNewlineList = many1 ((newline <|> carriageReturn) `thenSkip` spacing)
readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing)
readLineBreak = optional readNewlineList
prop_readSeparator1 = isWarning readScript "a &; b"
@@ -1529,7 +1690,11 @@ readSimpleCommand = called "simple command" $ do
readSource :: Monad m => SourcePos -> Token -> SCParser m Token
readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do
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
Nothing -> do
parseNoteAt pos WarningC 1090
@@ -1610,6 +1775,7 @@ readTermOrNone = do
eof
return []
prop_readTerm = isOk readTerm "time ( foo; bar; )"
readTerm = do
allspacing
m <- readAndOr
@@ -1768,6 +1934,16 @@ readBraceGroup = called "brace group" $ do
fail "Missing '}'"
return $ T_BraceGroup id list
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
readBatsTest = called "bats @test" $ do
id <- getNextId
try $ string "@test"
spacing
name <- readNormalWord
spacing
test <- readBraceGroup
return $ T_BatsTest id name test
prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"
readWhileClause = called "while loop" $ do
pos <- getPosition
@@ -1933,6 +2109,9 @@ prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}"
prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
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
functionSignature <- try readFunctionSignature
allspacing
@@ -1950,13 +2129,17 @@ readFunctionDefinition = called "function" $ do
whitespace
spacing
name <- readFunctionName
spacing
spaces <- spacing
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
readWithoutFunction = try $ do
id <- getNextId
name <- readFunctionName
guard $ name /= "time" -- Interfers with time ( foo )
spacing
readParens
return $ T_Function id (FunctionKeyword False) (FunctionParentheses True) name
@@ -2001,7 +2184,21 @@ readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip`
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
readCompoundCommand = do
id <- getNextId
cmd <- choice [ readBraceGroup, readArithmeticExpression, readSubshell, readCondition, readWhileClause, readUntilClause, readIfClause, readForClause, readSelectClause, readCaseClause, readFunctionDefinition]
cmd <- choice [
readBraceGroup,
readTripleParenthesis "" readArithmeticExpression readSubshell,
readArithmeticExpression,
readSubshell,
readCondition,
readWhileClause,
readUntilClause,
readIfClause,
readForClause,
readSelectClause,
readCaseClause,
readBatsTest,
readFunctionDefinition
]
spacing
redirs <- many readIoRedirect
unless (null redirs) $ optional $ do
@@ -2030,13 +2227,29 @@ readTimeSuffix = do
lookAhead $ char '-'
readCmdWord
-- Fixme: this is a hack that doesn't handle let '++c' or let a\>b
-- Fixme: this is a hack that doesn't handle let c='4'"5" or let a\>b
readLetSuffix :: Monad m => SCParser m [Token]
readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
where
readLetExpression :: Monad m => SCParser m Token
readLetExpression = do
startPos <- getPosition
expression <- readStringForParser readCmdWord
subParse startPos readArithmeticContents expression
let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
subParse newPos readArithmeticContents unQuoted
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p =
case s of
first:rest@(_:_) ->
let (last:backwards) = reverse rest
middle = reverse backwards
in
if first `elem` "'\"" && first == last
then (middle, updatePosChar p first)
else (s, p)
x -> (s, p)
-- bash allows a=(b), ksh allows $a=(b). dash allows neither. Let's warn.
readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
@@ -2064,6 +2277,8 @@ prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
@@ -2082,7 +2297,7 @@ readAssignmentWord = try $ do
isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof))
if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
then do
when (variable /= "IFS" && hasRightSpace) $
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
parseNoteAt pos WarningC 1007
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
value <- readEmptyLiteral
@@ -2233,8 +2448,11 @@ ifParse p t f =
prop_readShebang1 = isOk readShebang "#!/bin/sh\n"
prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n"
prop_readShebang4 = isWarning readShebang "! /bin/sh"
readShebang = do
try readCorrect <|> try readSwapped
try readCorrect <|> try readSwapped <|> try readMissingHash
many linewhitespace
str <- many $ noneOf "\r\n"
optional carriageReturn
optional linefeed
@@ -2247,6 +2465,15 @@ readShebang = do
parseProblemAt pos ErrorC 1084
"Use #!, not !#, for the shebang."
readMissingHash = do
pos <- getPosition
char '!'
lookAhead $ do
many linewhitespace
char '/'
parseProblemAt pos ErrorC 1104
"Use #!, not just !, for the shebang."
verifyEof = eof <|> choice [
ifParsable g_Lparen $
parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
@@ -2277,9 +2504,11 @@ readScript = do
verifyShell pos (getShell sb)
if isValidShell (getShell sb) /= Just False
then do
commands <- readCompoundListOrEmpty
annotationId <- getNextId
annotations <- readAnnotations
commands <- withAnnotations annotations readCompoundListOrEmpty
verifyEof
return $ T_Script id sb commands
return $ T_Annotation annotationId annotations $ T_Script id sb commands
else do
many anyChar
return $ T_Script id sb []
@@ -2316,6 +2545,7 @@ readScript = do
"ash",
"dash",
"bash",
"bats",
"ksh"
]
badShells = [
@@ -2334,6 +2564,7 @@ readScript = do
isWarning p s = parsesCleanly p s == Just False
isOk p s = parsesCleanly p s == Just True
isNotOk p s = parsesCleanly p s == Nothing
testParse string = runIdentity $ do
(res, _) <- runParser (mockedSystemInterface []) readScript "-" string
@@ -2349,9 +2580,8 @@ parsesCleanly parser string = runIdentity $ do
parseWithNotes parser = do
item <- parser
map <- getMap
parseNotes <- getParseNotes
return (item, map, nub . sortNotes $ parseNotes)
state <- getState
return (item, state)
compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2)
sortNotes = sortBy compareNotes
@@ -2391,11 +2621,12 @@ system = lift . lift . lift
parseShell sys name contents = do
(result, state) <- runParser sys (parseWithNotes readScript) name contents
case result of
Right (script, tokenMap, notes) ->
Right (script, userstate) ->
return ParseResult {
prComments = map toPositionedComment $ nub $ notes ++ parseProblems state,
prTokenPositions = Map.map posToPos tokenMap,
prRoot = Just script
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
prTokenPositions = Map.map posToPos (positionMap userstate),
prRoot = Just $
reattachHereDocs script (hereDocMap userstate)
}
Left err ->
return ParseResult {
@@ -2416,6 +2647,13 @@ parseShell sys name contents = do
second (ContextName pos str) = ParseNote pos InfoC 1009 $
"The mentioned parser error was in this " ++ str ++ "."
reattachHereDocs root map =
doTransform f root
where
f t@(T_HereDoc id dash quote string []) = fromMaybe t $ do
list <- Map.lookup id map
return $ T_HereDoc id dash quote string list
f t = t
toPositionedComment :: ParseNote -> PositionedComment
toPositionedComment (ParseNote pos severity code message) =

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 "$@"

21
quicktest Executable file
View File

@@ -0,0 +1,21 @@
#!/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
,ShellCheck.Checks.Commands.runTests
,ShellCheck.AnalyzerLib.runTests
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then
exit 0
else
grep -C 3 -e "Fail" -e "Tracing" <<< "$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
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 ))
@@ -32,6 +32,12 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS
**-C**[*WHEN*],\ **--color**[=*WHEN*]
: For TTY output, 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*...]
: 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*
: 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
can't be determined.
@@ -54,7 +60,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Print version information and exit.
**-x**,\ **-external-sources**
**-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input.
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
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
ShellCheck is written and maintained by Vidar Holen.

View File

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

35
stack.yaml Normal file
View File

@@ -0,0 +1,35 @@
# This file was automatically generated by stack init
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-5.5
# Local packages, usually specified by relative directory name
packages:
- '.'
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
extra-deps: []
# Override default flag values for local packages and extra-deps
flags: {}
# Extra package databases containing global packages
extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: >= 1.0.0
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

View File

@@ -4,13 +4,17 @@ import Control.Monad
import System.Exit
import qualified ShellCheck.Checker
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Parser
import qualified ShellCheck.Checks.Commands
main = do
putStrLn "Running ShellCheck tests..."
results <- sequence [
ShellCheck.Checker.runTests,
ShellCheck.Checks.Commands.runTests,
ShellCheck.Analytics.runTests,
ShellCheck.AnalyzerLib.runTests,
ShellCheck.Parser.runTests
]
if and results