103 Commits

Author SHA1 Message Date
Vidar Holen
c36f6d89ba Stable version 0.4.7
This release is dedicated to net neutrality. Remember when the Internet
was a meritocracy? [Please drink a verification can to continue.]
2017-12-08 20:29:12 -08:00
Vidar Holen
e801da0621 Add a changelog 2017-12-07 17:49:43 -08:00
koalaman
51e6bf809f Merge pull request #1041 from LukeShu/fix-isClosingFileOp
Fix isClosingFileOp (fixes issue #862)
2017-11-08 10:15:21 -08:00
Vidar Holen
3413a076ff Cabal: Don't make executables depend on library 2017-11-05 20:51:39 -08:00
Vidar Holen
53f63b85bb Use Data.Map.Strict instead of Map.insertWith' 2017-11-05 20:11:04 -08:00
Luke Shumaker
df068bc8ed Fix isClosingFileOp (fixes issue #862)
The isClosingFileOp function expected closing file ops to use T_IoFile, but
they actually use T_IoDuplicate; so it effectively always returned False.
2017-11-05 18:53:01 -05:00
Vidar Holen
102683ab04 Try to warn when using directives after commands (#981) 2017-11-04 15:22:17 -07:00
Vidar Holen
acead72c93 Improve directive parsing 2017-10-29 17:39:13 -07:00
Vidar Holen
0c1e2bbd4d Warn when using directives in front of elif and case items (#1036) 2017-10-29 16:31:46 -07:00
koalaman
5d9cb81008 Merge pull request #1026 from PeterDaveHello/patch-1
Enable syntax highlight in README.md code block
2017-10-15 19:09:32 -07:00
koalaman
1491402dcb Merge pull request #1027 from PeterDaveHello/README.md-Table-of-Contents
Add Table of Contents in README.md
2017-10-15 19:08:16 -07:00
Vidar Holen
436a46ebab Improve automated docker builds and tagging 2017-10-15 15:43:06 -07:00
Vidar Holen
db1e24d140 Dockerfile renamed "shellcheck" to "bin". Unbreak it. 2017-10-10 10:02:11 -07:00
Peter Dave Hello
35daf7534b Add Table of Contents in README.md 2017-10-10 20:25:04 +08:00
Peter Dave Hello
76ad5dbb9f Enable syntax highlight in README.md code block 2017-10-10 20:20:51 +08:00
Vidar Holen
f73736e5c9 Add Alpine-based docker image 2017-10-07 15:19:35 -07:00
Vidar Holen
3785a08906 Don't suggest $@ in [[ $* = "" ]] (#976) 2017-10-01 10:27:21 -07:00
Vidar Holen
74c199b51a Warn when one case pattern overrides another. 2017-09-16 15:23:51 -07:00
Vidar Holen
371dcdda3a Warn about missing default case for getopts. 2017-09-16 10:26:28 -07:00
Vidar Holen
38044e3f75 Fix 2062 for grep -e -foo bar* and --regex -foo bar* 2017-09-09 17:03:29 -07:00
Vidar Holen
b0f6f935f3 Don't suggset quoting in grep -- -foo bar* (#517) 2017-09-09 16:57:06 -07:00
Vidar Holen
bd2facb245 Suggest (( expr )) over let expr (#813) 2017-09-09 16:07:38 -07:00
Vidar Holen
895ba31337 Add ^@![]/ to allowed function characters (#909) 2017-09-09 15:34:08 -07:00
koalaman
ccc037d458 Merge pull request #988 from Nightfirecat/953-local-A-fix
SC2154: Fix false positive on `local`
2017-09-09 09:37:04 -07:00
koalaman
a1b370efbc Merge pull request #983 from Dynamic-Gravity/master
Updated readme installation instructions
2017-09-09 09:35:59 -07:00
Jordan Atwood
7f36c369f3 SC2154: Fix false positive on local 2017-09-06 15:40:31 -07:00
Unknown
7b55e73e03 Updated readme installation instructions
Added installation instructions for Solus distro.
2017-08-29 13:38:12 -04:00
Vidar Holen
6c068e7d29 Merge branch 'master' of github.com:koalaman/shellcheck 2017-08-13 19:45:31 -07:00
Vidar Holen
8dd40efb44 Add support for -a: emit for sourced files. 2017-08-13 19:34:45 -07:00
koalaman
751aebf984 Merge pull request #968 from ssbarnea/patch-1
Documented binary cabal install for MacOS
2017-08-13 10:52:03 -07:00
Sorin Sbarnea
3bf6913a15 Documented binary cabal install for MacOS
Installing haskell binaries is 50x faster than compiling it from source.
2017-08-11 12:01:28 +01:00
Vidar Holen
73d06c4f47 Autogenerate list of formats for --help 2017-08-06 15:48:59 -07:00
koalaman
72ed234291 Merge pull request #964 from blueyed/help-document-output-formats
List available output formats in --help output
2017-08-06 15:35:26 -07:00
koalaman
b94c03e5a1 Merge pull request #957 from martin-schwenke/issue-950
Fix incorrect detection of bash-style substring expansion (issue #950)
2017-08-06 15:34:23 -07:00
Daniel Hahler
226bc4409c Use spaces with list of dialects in --help for consistency 2017-08-06 16:25:31 +02:00
Daniel Hahler
4a6acb6ff0 List available output formats in --help output 2017-08-06 16:24:43 +02:00
koalaman
1d76abc439 Add storage bucket listing to readme 2017-07-30 10:52:06 -07:00
Martin Schwenke
807d899f3b Fix incorrect detection of bash-style substring expansion
Substring expansion detection only considers ':' as a separator..  It
needs to avoid triggering for ":-", ":=", ":+" and ":?", since they
mean other things.

This is a regression introduced by commit
a90b6d14b3

Signed-off-by: Martin Schwenke <martin@meltin.net>
2017-07-20 15:59:05 +10:00
koalaman
d6803ffa24 Merge pull request #955 from tsoernes/patch-1
Add Stack as install method
2017-07-18 20:39:39 -07:00
tsoernes
4ec8d73a14 Add Stack as install method 2017-07-19 04:17:09 +02:00
Vidar Holen
81388cefd2 Warn when calling functions before defining them. 2017-07-10 22:53:26 -07:00
Vidar Holen
43bb6a20ad Improve message for SC1052-54 about 'then;' 2017-07-08 17:25:54 -07:00
Vidar Holen
8f99d2b008 Don't warn about missing path for find -O3 . (#942) 2017-07-08 15:46:02 -07:00
Vidar Holen
79ae89076a Swap SC1041 and SC1042 for better sort order. 2017-07-08 15:21:58 -07:00
Vidar Holen
aa33280cb0 Improve here doc diagnosis 2017-07-08 14:00:02 -07:00
Vidar Holen
bd13224907 Use standard Haskell 'void' instead of custom 2017-07-08 10:23:51 -07:00
Vidar Holen
b064cf3038 Fix parsing here docs like << '#foo' (#947) 2017-07-07 22:26:12 -07:00
koalaman
79d6066450 Mention docker release tags in readme 2017-07-05 10:30:30 -07:00
Vidar Holen
1463cf773a Suggest explicit escape "\\n" for "\n" 2017-07-04 11:06:52 -07:00
Vidar Holen
31bb02d6b7 Ignore leading \ for commands (#927) 2017-07-03 16:40:11 -07:00
Vidar Holen
5bd33dbf92 Warn when piping/redirecting to mv/cp/echo/etc (#921) 2017-07-03 16:02:58 -07:00
Vidar Holen
a3c6aff0fb Improve parsing of line breaks in for statements (#926) 2017-07-03 13:58:10 -07:00
Vidar Holen
8184ef1e8b Don't complain about missing space in {( (#937) 2017-07-03 12:22:19 -07:00
Vidar Holen
a839a6657b Warn when commands start with dashes (#938) 2017-07-03 12:06:59 -07:00
Vidar Holen
a10b924570 Mention correct operator when warning about spaces around += (#944) 2017-07-03 10:44:09 -07:00
Vidar Holen
8f31ae913b Skip command argument when checking trap signal specs (#946) 2017-07-03 10:36:51 -07:00
Vidar Holen
a06ad41bfa Add Linux binaries to readme 2017-06-24 23:00:59 -07:00
Vidar Holen
21f5bf01eb Make TravisCI auto-build Linux executables. 2017-06-24 22:29:35 -07:00
koalaman
2ded4df6fa Merge pull request #935 from blueyed/README-fixes
README: style fixes, add Neomake
2017-06-14 10:47:28 -07:00
Daniel Hahler
90da31f226 README: style fixes, add Neomake 2017-06-13 21:22:50 +02:00
koalaman
b1486ec1e9 Merge pull request #903 from mrshu/mrshu/pushd-popd-like-cd
SC2164: Make SC2164 apply to `pushd` and `popd`
2017-06-12 09:54:05 -07:00
mr.Shu
954aa99b11 Analytics.hs: Refactor cd, popd and pushd checks
* Refactor the check of unchecked `cd`, `pushd` and `popd` into one
  function.

Signed-off-by: mr.Shu <mr@shu.io>
2017-06-12 12:16:01 +02:00
mr.Shu
79872f92f8 Merge branch 'master' of https://github.com/koalaman/shellcheck into mrshu/pushd-popd-like-cd
Signed-off-by: mr.Shu <mr@shu.io>
2017-06-12 11:29:19 +02:00
koalaman
bf9b841b07 Link to Windows executables in the Readme 2017-06-10 10:10:09 -07:00
Vidar Holen
5fad708df5 Zip compiled Windows executables. 2017-06-10 09:26:08 -07:00
Vidar Holen
5cece759cc Autobuild Windows .exe files 2017-06-09 21:40:59 -07:00
Vidar Holen
50c8172de4 Allow escaping ( with quotes in [ .. ] (#925) 2017-06-03 11:45:25 -07:00
Vidar Holen
ce950edbfd Don't trigger SC2026 when followed by empty literals (#923) 2017-06-03 09:38:47 -07:00
Royce Remer
f8e75d3e89 add compilation documentation for test runners 2017-05-28 16:06:55 -07:00
Vidar Holen
6f4e06d83c Avoid rescanning tree for lastpipe on every node. 2017-05-28 16:04:42 -07:00
Vladimir Panteleev
505ff7832f Recognize bash's shopt -s lastpipe
Fixes #732.
2017-05-28 14:56:49 -07:00
Vidar Holen
ac3f0b3360 SC2114 about rm -rf /usr is no longer silenced by -- 2017-05-28 14:44:58 -07:00
Vidar Holen
070a465b64 Recognize missing and superfluous cases in getopts loops. 2017-05-28 13:38:04 -07:00
Vidar Holen
4243c6a0bf Treat + like :+ to squash SC2068 2017-05-24 19:20:28 -07:00
Vidar Holen
8bc89bc451 Mention DevGuide in the README 2017-05-21 17:15:51 -07:00
Vidar Holen
5099ebf9b9 Allow comments after shellcheck directives. 2017-05-21 13:56:22 -07:00
Vidar Holen
d943ef6f77 Update Docker instructions. 2017-05-20 21:13:53 -07:00
mr.Shu
5e4c288cf4 SC2174: Do not warn at mkdir -pm 0700 ../foo
* Do not warn when `mkdir -pm 0700` is used with combination of paths
  like `..` and `.`

* Fixes #854

Signed-off-by: mr.Shu <mr@shu.io>
2017-05-16 11:45:04 -07:00
mr.Shu
9e35aa7ce8 SC2164: Make SC2164 apply to pushd and popd
* Since `pushd` and `popd` have the same failure cases, make the check
  for SC2164 apply to them as well.

* This commit also refactors the code a bit as `hasSetE` is now used in
  multiple places.

* Fixes #863.

Signed-off-by: mr.Shu <mr@shu.io>
2017-05-14 14:00:10 +02:00
Vincent van der Weele
21d7068bc8 Add VSCode integration to editor list 2017-05-09 19:03:26 -07:00
Vidar Holen
324aa3cc88 Improve and deduplicate string comparison warnings. 2017-04-22 21:09:42 -07:00
Dan Kegel
9c4f651e6b Document shell directive added by 944313c6 2017-04-18 09:57:38 -07:00
Vidar Holen
3cf8b9ceab Parse ksh nested arrays and warn about var=(( 2017-04-17 21:01:16 -07:00
Vidar Holen
5c01b6c7f5 Parse empty [ ] conditionals 2017-04-16 18:11:00 -07:00
Vidar Holen
7604e5eb58 Warn when using a glob as a command name. 2017-04-15 19:53:09 -07:00
Vidar Holen
4fb1080809 Warn when redirecting to a literal integer. 2017-04-15 17:20:33 -07:00
Vidar Holen
4f9a80db15 Remove leftover debug trace 2017-04-15 13:44:01 -07:00
Vidar Holen
3a38c50b8e Fix shellcheck warnings :P 2017-04-15 13:24:41 -07:00
Vidar Holen
fd79e80e78 Fix SC2120 triggering on sourced files and ${!var*} 2017-04-15 11:26:47 -07:00
Vidar Holen
1fd9b474ba Don't warn about quoting variables in [ -v 'bar[$foo]' ] 2017-04-15 10:57:10 -07:00
Vidar Holen
faafc99704 Don't trigger SC2037 when quoting (PAGER="cat" foo) 2017-04-15 10:33:56 -07:00
Vidar Holen
bc882fd85a Recognize more invalid shebangs 2017-04-08 16:34:00 -07:00
Vidar Holen
41b6e3d5eb Don't warn about [ -v foo ] being unassigned. 2017-04-08 15:19:47 -07:00
Vidar Holen
da1691912b Replace _otherwise with _ in cases 2017-04-08 14:00:52 -07:00
Vaibhav Sagar
0feb95b337 Implement fixes suggested by HLint 2017-04-08 11:07:32 -07:00
Vidar Holen
f0e0d9ffdb Don't suggest \[\] in PS1 for non-bash 2017-04-02 14:50:15 -07:00
Vidar Holen
3c75674b50 Warn about unquoted expansions in arrays. 2017-04-02 14:28:12 -07:00
Vidar Holen
8e5e77ad76 Don't suggest removing $ for (( $! + ${!var} )) 2017-04-02 09:49:47 -07:00
Vidar Holen
66c7cf19e2 Fix missing backslash in SC1003 about '\'' 2017-04-01 22:01:05 -07:00
xenopeek
36573b5b26 Update README.md
Command to install on Arch Linux based distros
2017-03-27 21:59:31 -07:00
Robert de Bock
9e4a9c8c6c Update README.md
Fixed a typo: scipts -> scripts.
2017-03-27 21:58:51 -07:00
Robert de Bock
c2fcb742db Update README.md
Adding a usage example for Docker.
2017-03-27 21:58:51 -07:00
Vaibhav Sagar
e8b4a79b65 Update resolver 2017-03-26 13:18:20 -07:00
26 changed files with 1896 additions and 691 deletions

42
.prepare_deploy Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
# This script packages up Travis compiled binaries
set -ex
shopt -s nullglob
cd deploy
cp ../LICENSE LICENSE.txt
sed -e $'s/$/\r/' > README.txt << END
This is a precompiled ShellCheck binary.
http://www.shellcheck.net/
ShellCheck is a static analysis tool for shell scripts.
It's licensed under the GNU General Public License v3.0.
Information and source code is available on the website.
This binary was compiled on $(date -u).
====== Latest commits ======
$(git log -n 3)
END
for file in ./*.exe
do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
done
for file in *.linux
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in ./*
do
sha512sum "$file" > "$file.sha512sum"
done

View File

@@ -6,16 +6,67 @@ services:
- docker - docker
before_install: before_install:
- export DOCKER_REPO=koalaman/shellcheck - DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
- |- - DOCKER_BUILDS=""
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH") - TAGS=""
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
- test -n "$TRAVIS_TAG" && TAGS="$TAGS $TRAVIS_TAG" || true
- test "$TRAVIS_BRANCH" = master && test -n "$TRAVIS_TAG" && TAGS="$TAGS stable" || true
script: script:
- docker build -t builder -f Dockerfile_builder . - mkdir deploy
- docker run --rm -it -v $(pwd):/mnt builder # Windows .exe
- docker build -t $DOCKER_REPO:$TAG . - docker pull koalaman/winghc
- docker run --user="$UID" --rm -v "$PWD:/appdata" koalaman/winghc cuib
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
- rm -rf dist || true
# Linux static executable
- docker pull koalaman/scbuilder
- docker run --user="$UID" --rm -v "$PWD:/mnt" koalaman/scbuilder
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux"; done
- ./shellcheck --version
- rm -rf dist || true
# Linux Docker image
- name="$DOCKER_BASE"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- docker build -t "$name:current" .
- docker run "$name:current" --version
# Linux Alpine based Docker image
- name="$DOCKER_BASE-alpine"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- sed 's/^FROM .*/FROM alpine:latest/' Dockerfile > Dockerfile.alpine
- docker build -f Dockerfile.alpine -t "$name:current" .
- docker run "$name:current" --version
# Misc packaging
- ./.prepare_deploy
after_success: after_success:
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- |- - for repo in $DOCKER_BUILDS;
([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG" do
for tag in $TAGS;
do
echo "Deploying $repo:current as $repo:$tag...";
docker tag "$repo:current" "$repo:$tag";
docker push "$repo:$tag";
done;
done;
after_failure:
- id
- pwd
- df -h
- find . -name '*.log' -type f -exec grep "" /dev/null {} +
- find . -ls
deploy:
provider: gcs
skip_cleanup: true
access_key_id: GOOG7MDN7WEH6IIGBDCA
secret_access_key:
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
bucket: shellcheck
local-dir: deploy
on:
repo: koalaman/shellcheck
all_branches: true

309
CHANGELOG.md Normal file
View File

@@ -0,0 +1,309 @@
## v0.4.7 - 2017-12-08
### Added
- Statically linked binaries for Linux and Windows (see README.md)!
- `-a` flag to also include warnings in `source`d files
- SC2221/SC2222: Warn about overridden case branches
- SC2220: Warn about unhandled error cases in getopt loops
- SC2218: Warn when using functions before they're defined
- SC2216/SC2217: Warn when piping/redirecting to mv/cp and other non-readers
- SC2215: Warn about commands starting with leading dash
- SC2214: Warn about superfluous getopt flags
- SC2213: Warn about unhandled getopt flags
- SC2212: Suggest `false` over `[ ]`
- SC2211: Warn when using a glob as a command name
- SC2210: Warn when redirecting to an integer, e.g. `foo 1>2`
- SC2206/SC2207: Suggest alternatives when using word splitting in arrays
- SC1117: Warn about double quoted, undefined backslash sequences
- SC1113/SC1114/SC1115: Recognized more malformed shebangs
### Fixed
- `[ -v foo ]` no longer warns if `foo` is undefined
- SC2037 is now suppressed by quotes, e.g. `PAGER="cat" man foo`
- Ksh nested array declarations now parse correctly
- Parameter Expansion without colons are now recognized, e.g. `${foo+bar}`
- The `lastpipe` option is now respected with regard to subshell warnings
- `\(` is now respected for grouping in `[`
- Leading `\` is now ignored for commands, to allow alias suppression
- Comments are now allowed after directives to e.g. explain 'disable'
## v0.4.6 - 2017-03-26
### Added
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays in [/[[
- SC2196/SC2197: Warn about deprected egrep/fgrep
- SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
- SC2188/SC2189: Warn about redirections without commands
- SC2186: Warn about deprecated `tempfile`
- SC1109: Warn when finding `&amp;`/`&gt;`/`&lt;` unquoted
- SC1108: Warn about missing spaces in `[ var= foo ]`
### Changed
- All files are now read as UTF-8 with lenient latin1 fallback, ignoring locale
- Unicode quotes are no longer considered syntactic quotes
- `ash` scripts will now be checked as `dash` with a warning
### Fixed
- `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives
- Here doc delimters with esoteric quoting like `foo""` are now handled
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
- `grep -F` now suppresses regex related suggestions
- Command name checks now recognize busybox applet names
## v0.4.5 - 2016-10-21
### Added
- A Docker build (thanks, kpankonen!)
- SC2185: Suggest explicitly adding path for `find`
- SC2184: Warn about unsetting globs (e.g. `unset foo[1]`)
- SC2183: Warn about `printf` with more formatters than variables
- SC2182: Warn about ignored arguments with `printf`
- SC2181: Suggest using command directly instead of `if [ $? -eq 0 ]`
- SC1106: Warn when using `test` operators in `(( 1 -eq 2 ))`
### Changed
- Unrecognized directives now causes a warning rather than parse failure.
### Fixed
- Indices in associative arrays are now parsed correctly
- Missing shebang warning squashed when specifying with a directive
- Ksh multidimensional arrays are now supported
- Variables in substring ${a:x:y} expansions now count as referenced
- SC1102 now also handles ambiguous `$((`
- Using `$(seq ..)` will no longer suggest quoting
- SC2148 (missing shebang) is now suppressed when using shell directives
- `[ a '>' b ]` is now recognized as being correctly escaped
## v0.4.4 - 2016-05-15
### Added
- Haskell Stack support (thanks, Arguggi!)
- SC2179/SC2178: Warn when assigning/appending strings to arrays
- SC1102: Warn about ambiguous `$(((`
- SC1101: Warn when \\ linebreaks have trailing spaces
### Changed
- Directives directly after the shebang now apply to the entire file
### Fixed
- `{$i..10}` is now flagged similar to `{1..$i}`
## v0.4.3 - 2016-01-13
### Fixed
- Build now works on GHC 7.6.3 as found on Debian Stable/Ubuntu LTS
## v0.4.2 - 2016-01-09
### Added
- First class support for the `dash` shell
- The `--color` flag similar to ls/grep's (thanks, haguenau!)
- SC2174: Warn about unexpected behavior of `mkdir -pm` (thanks, eatnumber1!)
- SC2172: Warn about non-portable use of signal numbers in `trap`
- SC2171: Warn about `]]` without leading `[[`
- SC2168: Warn about `local` outside functions
### Fixed
- Warnings about unchecked `cd` will no longer trigger with `set -e`
- `[ a -nt/-ot/-ef b ]` no longer warns about being constant
- Quoted test operators like `[ foo "<" bar ]` now parse
- Escaped quotes in backticks now parse correctly
## v0.4.1 - 2015-09-05
### Fixed
- Added missing files to Cabal, fixing the build
## v0.4.0 - 2015-09-05
### Added
- Support for following `source`d files
- Support for setting default flags in `SHELLCHECK_OPTS`
- An `--external-sources` flag for following arbitrary `source`d files
- A `source` directive to override the filename to `source`
- SC2166: Suggest using `[ p ] && [ q ]` over `[ p -a q ]`
- SC2165: Warn when nested `for` loops use the same variable name
- SC2164: Warn when using `cd` without checking that it succeeds
- SC2163: Warn about `export $var`
- SC2162: Warn when using `read` without `-r`
- SC2157: Warn about `[ "$var " ]` and similar never-empty string matches
### Fixed
- `cat -vnE file` and similar will no longer flag as UUOC
- Nested trinary operators in `(( ))` now parse correctly
- Ksh `${ ..; }` command expansions now parse
## v0.3.8 - 2015-06-20
### Changed
- ShellCheck's license has changed from AGPLv3 to GPLv3.
### Added
- SC2156: Warn about injecting filenames in `find -exec sh -c "{}" \;`
### Fixed
- Variables and command substitutions in brace expansions are now parsed
- ANSI colors are now disabled on Windows
- Empty scripts now parse
## v0.3.7 - 2015-04-16
### Fixed
- Build now works on GHC 7.10
- Use `regex-tdfa` over `regex-compat` since the latter crashes on OS X.
## v0.3.6 - 2015-03-28
### Added
- SC2155: Warn about masked return values in `export foo=$(exit 1)`
- SC2154: Warn when a lowercase variable is referenced but not assigned
- SC2152/SC2151: Warn about bad `return` values like `1234` and `"foo"`
- SC2150: Warn about `find -exec "shell command" \;`
### Fixed
- `coproc` is now supported
- Trinary operator now recognized in `((..))`
### Removed
- Zsh support has been removed
## v0.3.5 - 2014-11-09
### Added
- SC2148: Warn when not including a shebang
- SC2147: Warn about literal ~ in PATH
- SC1086: Warn about `$` in for loop variables, e.g. `for $i in ..`
- SC1084: Warn when the shebang uses `!#` instead of `#!`
### Fixed
- Empty and comment-only backtick expansions now parse
- Variables used in PS1/PROMPT\_COMMAND/trap now count as referenced
- ShellCheck now skips unreadable files and directories
- `-f gcc` on empty files no longer crashes
- Variables in $".." are now considered quoted
- Warnings about expansions in single quotes now include backticks
## v0.3.4 - 2014-07-08
### Added
- SC2146: Warn about precedence when combining `find -o` with actions
- SC2145: Warn when concatenating arrays and strings
### Fixed
- Case statements now support `;&` and `;;&`
- Indices in array declarations now parse correctly
- `let` expressions now parsed as arithmetic expressions
- Escaping is now respected in here documents
### Changed
- Completely drop Makefile in favor of Cabal (thanks rodrigosetti!)
## v0.3.3 - 2014-05-29
### Added
- SC2144: Warn when using globs in `[/[[`
- SC2143: Suggesting using `grep -q` over `[ "$(.. | grep)" ]`
- SC2142: Warn when referencing positional parameters in aliases
- SC2141: Warn about suspicious IFS assignments like `IFS="\n"`
- SC2140: Warn about bad embedded quotes like `echo "var="value""`
- SC2130: Warn when using `-eq` on strings
- SC2139: Warn about define time expansions in alias definitions
- SC2129: Suggest command grouping over `a >> log; b >> log; c >> log`
- SC2128: Warn when expanding arrays without an index
- SC2126: Suggest `grep -c` over `grep|wc`
- SC2123: Warn about accidentally overriding `$PATH`, e.g. `PATH=/my/dir`
- SC1083: Warn about literal `{/}` outside of quotes
- SC1082: Warn about UTF-8 BOMs
### Fixed
- SC2051 no longer triggers for `{1,$n}`, only `{1..$n}`
- Improved detection of single quoted `sed` variables, e.g. `sed '$s///'`
- Stop warning about single quoted variables in `PS1` and similar
- Support for Zsh short form loops, `=(..)`
### Removed
- SC1000 about unescaped lonely `$`, e.g. `grep "^foo$"`
## v0.3.2 - 2014-03-22
### Added
- SC2121: Warn about trying to `set` variables, e.g. `set var = value`
- SC2120/SC2119: Warn when a function uses `$1..` if none are ever passed
- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami`
- SC2116: Detect useless use of echo, e.g. `for i in $(echo $var)`
- SC2115/SC2114: Detect some catastrophic `rm -r "$empty/"` mistakes
- SC1081: Warn when capitalizing keywords like `While`
- SC1077: Warn when using acute accents instead of backticks
### Fixed
- Shells are now properly recognized in shebangs containing flags
- Stop warning about math on decimals in ksh/zsh
- Stop warning about decimal comparisons with `=`, e.g. `[ $version = 1.2 ]`
- Parsing of `|&`
- `${a[x]}` not counting as a reference of `x`
- `(( x[0] ))` not counting as a reference of `x`
## v0.3.1 - 2014-02-03
### Added
- The `-s` flag to specify shell dialect
- SC2105/SC2104: Warn about `break/continue` outside loops
- SC1076: Detect invalid `[/[[` arithmetic like `[ 1 + 2 = 3 ]`
- SC1075: Suggest using `elif` over `else if`
### Fixed
- Don't warn when comma separating elements in brace expansions
- Improved detection of single quoted `sed` variables, e.g. `sed '$d'`
- Parsing of arithmetic for loops using `{..}` instead of `do..done`
- Don't treat the last pipeline stage as a subshell in ksh/zsh
## v0.3.0 - 2014-01-19
### Added
- A man page (thanks Dridi!)
- GCC compatible error reporting (`shellcheck -f gcc`)
- CheckStyle compatible XML error reporting (`shellcheck -f checkstyle`)
- Error codes for each warning, e.g. SC1234
- Allow disabling warnings with `# shellcheck disable=SC1234`
- Allow disabling warnings with `--exclude`
- SC2103: Suggest using subshells over `cd foo; bar; cd ..`
- SC2102: Warn about duplicates in char ranges, e.g. `[10-15]`
- SC2101: Warn about named classes not inside a char range, e.g. `[:digit:]`
- SC2100/SC2099: Warn about bad math expressions like `i=i+5`
- SC2098/SC2097: Warn about `foo=bar echo $foo`
- SC2095: Warn when using `ssh`/`ffmpeg` in `while read` loops
- Better warnings for missing here doc tokens
### Fixed
- Don't warn when single quoting variables with `ssh/perl/eval`
- `${!var}` is now counted as a variable reference
### Removed
- Suggestions about using parameter expansion over basename
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
## v0.2.0 - 2013-10-27
### Added
- Suggest `./*` instead of `*` when passing globs to commands
- Suggest `pgrep` over `ps | grep`
- Warn about unicode quotes
- Warn about assigned but unused variables
- Inform about client side expansion when using `ssh`
### Fixed
- CLI tool now uses exit codes and stderr canonically
- Parsing of extglobs containing empty patterns
- Parsing of bash style `eval foo=(bar)`
- Parsing of expansions in here documents
- Parsing of function names containing :+-
- Don't warn about `find|xargs` when using `-print0`
## v0.1.0 - 2013-07-23
### Added
- First release

View File

@@ -1,11 +1,10 @@
FROM alpine:latest FROM scratch
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com> LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
COPY package/bin/shellcheck /usr/local/bin/ # This file assumes ShellCheck has already been built.
COPY package/lib/ /usr/local/lib/ # See https://github.com/koalaman/scbuilder
COPY shellcheck /bin/shellcheck
RUN ldconfig /usr/local/lib
WORKDIR /mnt WORKDIR /mnt
ENTRYPOINT ["shellcheck"] ENTRYPOINT ["/bin/shellcheck"]

View File

@@ -1,54 +0,0 @@
FROM mitchty/alpine-ghc:latest
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
RUN apk add --no-cache build-base
RUN mkdir -p /usr/src/shellcheck
WORKDIR /usr/src/shellcheck
# # ------------------------------------------------------------
# # Build & Test
# # ------------------------------------------------------------
# Obtain the dependencies first, which are less likely to change, in order to reduce
# subsequent build time by leveraging image cache. This benefits developers when they
# build their code with this image locally. In case of Travis CI, this doesn't help
# reduce building time because Travis CI doesn't use cache.
COPY ShellCheck.cabal .
RUN cabal update && cabal install --only-dependencies
# Copy the rest of the source files, including ShellCheck.cabal again but doesn't matter
COPY . .
# Build
RUN cabal install
# Test
RUN cabal test
# # ------------------------------------------------------------
# # Set PATH
# # ------------------------------------------------------------
# Add runtime path to easily reach the executable file. This only exists during build.
ENV PATH "/root/.cabal/bin:$PATH"
# Make it permanent for someone who login to the container of this image
RUN echo "export PATH=${PATH}" >> /etc/profile
# # ------------------------------------------------------------
# # Extract Binaries
# # ------------------------------------------------------------
# Get shellcheck binary
RUN mkdir -p /package/bin/
RUN cp $(which shellcheck) /package/bin/
# Get shared libraries using magic
RUN mkdir -p /package/lib/
RUN ldd $(which shellcheck) | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /package/lib/
# Copy shellcheck package out to mounted directory
CMD ["cp", "-avr", "/package", "/mnt/"]

367
README.md
View File

@@ -6,37 +6,65 @@ ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell
The goals of ShellCheck are The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues - To point out and clarify typical beginner's syntax issues that cause a shell
that cause a shell to give cryptic error messages. to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems - To point out and clarify typical intermediate level semantic problems that
that cause a shell to behave strangely and counter-intuitively. 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. advanced user's otherwise working script to fail under future circumstances.
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify! See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
## Table of Contents
- [How to use](#how-to-use)
- [On the web](#on-the-web)
- [From your terminal](#from-your-terminal)
- [In your editor](#in-your-editor)
- [In your build or test suites](#in-your-build-or-test-suites)
- [Installing](#installing)
- [Travis CI Setup](#travis-ci-setup)
- [Compiling from source](#compiling-from-source)
- [Installing Cabal](#installing-cabal)
- [Compiling ShellCheck](#compiling-shellcheck)
- [Running tests](#running-tests)
- [Gallery of bad code](#gallery-of-bad-code)
- [Quoting](#quoting)
- [Conditionals](#conditionals)
- [Frequently misused commands](#frequently-misused-commands)
- [Common beginner's mistakes](#common-beginners-mistakes)
- [Style](#style)
- [Data and typing errors](#data-and-typing-errors)
- [Robustness](#robustness)
- [Portability](#portability)
- [Miscellaneous](#miscellaneous)
- [Testimonials](#testimonials)
- [Ignoring issues](#ignoring-issues)
- [Reporting bugs](#reporting-bugs)
- [Contributing](#contributing)
- [Copyright](#copyright)
## How to use ## How to use
There are a variety of ways to use ShellCheck!
There are a number of ways to use ShellCheck!
### On the web
#### On the web
Paste a shell script on http://www.shellcheck.net for instant feedback. Paste a shell script on http://www.shellcheck.net for instant feedback.
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends! [ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
### From your terminal
#### From your terminal
Run `shellcheck yourscript` in your terminal for instant output, as seen above. Run `shellcheck yourscript` in your terminal for instant output, as seen above.
### In your editor
#### In your editor
You can see ShellCheck suggestions directly in a variety of editors. You can see ShellCheck suggestions directly in a variety of editors.
* Vim, through [ALE](https://github.com/w0rp/ale) or [Syntastic](https://github.com/scrooloose/syntastic): * Vim, through [ALE](https://github.com/w0rp/ale), [Neomake](https://github.com/neomake/neomake), or [Syntastic](https://github.com/scrooloose/syntastic):
![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png). ![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
@@ -48,28 +76,16 @@ You can see ShellCheck suggestions directly in a variety of editors.
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck). * Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats). * Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
### In your build or test suites
#### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites. While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation. ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
## Travis CI Setup
If you want to use ShellCheck in Travis CI, setting it up is simple :tada:.
```yml
language: bash
addons:
apt:
sources:
- debian-sid # Grab ShellCheck from the Debian repo
packages:
- shellcheck
```
## Installing ## Installing
The easiest way to install ShellCheck locally is through your package manager. The easiest way to install ShellCheck locally is through your package manager.
@@ -79,10 +95,19 @@ On systems with Cabal (installs to `~/.cabal/bin`):
cabal update cabal update
cabal install ShellCheck cabal install ShellCheck
On systems with Stack (installs to `~/.local/bin`):
stack update
stack install ShellCheck
On Debian based distros: On Debian based distros:
apt-get install shellcheck apt-get install shellcheck
On Arch Linux based distros:
pacman -S shellcheck
On Gentoo based distros: On Gentoo based distros:
emerge --ask shellcheck emerge --ask shellcheck
@@ -110,48 +135,92 @@ On openSUSE:Tumbleweed:
On other openSUSE distributions: On other openSUSE distributions:
add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell Add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
zypper in ShellCheck zypper in ShellCheck
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
On Solus:
eopkg install shellcheck
From Docker Hub: From Docker Hub:
docker pull koalaman/shellcheck ```sh
docker pull koalaman/shellcheck:latest # Or :v0.4.6 for a release version
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
```
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend.
Alternatively, get freshly built binaries for the latest commit here:
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-latest.linux.x86_64.tar.xz) (statically linked)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-latest.zip)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums and release builds.
## Travis CI Setup
If you want to use ShellCheck in Travis CI, you can most easily install it via `apt`:
```yml
language: bash
addons:
apt:
sources:
- debian-sid # Grab ShellCheck from the Debian repo
packages:
- shellcheck
```
## Compiling from source ## Compiling from source
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile. This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
### Installing Cabal
#### Installing Cabal
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`). ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
brew install cask
brew cask install haskell-platform
cabal install cabal-install
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/ 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 Verify that `cabal` is installed and update its dependency list with
$ cabal update $ cabal update
#### Compiling ShellCheck ### Compiling ShellCheck
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install: `git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
$ cabal install $ cabal install
Or if you intend to run the tests:
$ cabal install --enable-tests
This will compile ShellCheck and install it to your `~/.cabal/bin` directory. This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
export PATH="$HOME/.cabal/bin:$PATH" ```sh
export PATH="$HOME/.cabal/bin:$PATH"
```
Log out and in again, and verify that your PATH is set up correctly: Log out and in again, and verify that your PATH is set up correctly:
$ which shellcheck ```sh
~/.cabal/bin/shellcheck $ which shellcheck
~/.cabal/bin/shellcheck
```
On native Windows, the `PATH` should already be set up, but the system On native Windows, the `PATH` should already be set up, but the system
may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE, may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,
@@ -165,155 +234,161 @@ In Powershell ISE, you may need to additionally update the output encoding:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 > [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
#### Running tests ### Running tests
To run the unit test suite: To run the unit test suite:
$ cabal test $ cabal test
## Gallery of bad code ## Gallery of bad code
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues. So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
#### Quoting ### Quoting
ShellCheck can recognize several types of incorrect quoting: ShellCheck can recognize several types of incorrect quoting:
echo $1 # Unquoted variables ```sh
find . -name *.ogg # Unquoted find/grep patterns echo $1 # Unquoted variables
rm "~/my file.txt" # Quoted tilde expansion find . -name *.ogg # Unquoted find/grep patterns
v='--verbose="true"'; cmd $v # Literal quotes in variables rm "~/my file.txt" # Quoted tilde expansion
for f in "*.ogg" # Incorrectly quoted 'for' loops v='--verbose="true"'; cmd $v # Literal quotes in variables
touch $@ # Unquoted $@ for f in "*.ogg" # Incorrectly quoted 'for' loops
echo 'Don't forget to restart!' # Singlequote closed by apostrophe touch $@ # Unquoted $@
echo 'Don\'t try this at home' # Attempting to escape ' in '' echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Path is $PATH' # Variables in single quotes echo 'Don\'t try this at home' # Attempting to escape ' in ''
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
```
### Conditionals
#### Conditionals
ShellCheck can recognize many types of incorrect test statements. ShellCheck can recognize many types of incorrect test statements.
[[ n != 0 ]] # Constant test expressions ```sh
[[ -e *.mpg ]] # Existence checks of globs [[ n != 0 ]] # Constant test expressions
[[ $foo==0 ]] # Always true due to missing spaces [[ -e *.mpg ]] # Existence checks of globs
[[ -n "$foo " ]] # Always true due to literals [[ $foo==0 ]] # Always true due to missing spaces
[[ $foo =~ "fo+" ]] # Quoted regex in =~ [[ -n "$foo " ]] # Always true due to literals
[ foo =~ re ] # Unsupported [ ] operators [[ $foo =~ "fo+" ]] # Quoted regex in =~
[ $1 -eq "shellcheck" ] # Numerical comparison of strings [ foo =~ re ] # Unsupported [ ] operators
[ $n && $m ] # && in [ .. ] [ $1 -eq "shellcheck" ] # Numerical comparison of strings
[ grep -q foo file ] # Command without $(..) [ $n && $m ] # && in [ .. ]
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed [ grep -q foo file ] # Command without $(..)
(( 1 -lt 2 )) # Using test operators in ((..)) [[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..))
```
### Frequently misused commands
#### Frequently misused commands
ShellCheck can recognize instances where commands are used incorrectly: ShellCheck can recognize instances where commands are used incorrectly:
grep '*foo*' file # Globs in regex contexts ```sh
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec grep '*foo*' file # Globs in regex contexts
sudo echo 'Var=42' > /etc/profile # Redirecting sudo find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
time --format=%s sleep 10 # Passing time(1) flags to time builtin sudo echo 'Var=42' > /etc/profile # Redirecting sudo
while read h; do ssh "$h" uptime # Commands eating while loop input time --format=%s sleep 10 # Passing time(1) flags to time builtin
alias archive='mv $1 /backup' # Defining aliases with arguments while read h; do ssh "$h" uptime # Commands eating while loop input
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr alias archive='mv $1 /backup' # Defining aliases with arguments
exec foo; echo "Done!" # Misused 'exec' tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find exec foo; echo "Done!" # Misused 'exec'
f() { whoami; }; sudo f # External use of internal functions find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
f() { whoami; }; sudo f # External use of internal functions
```
### Common beginner's mistakes
#### Common beginner's mistakes
ShellCheck recognizes many common beginner's syntax errors: ShellCheck recognizes many common beginner's syntax errors:
var = 42 # Spaces around = in assignments ```sh
$foo=42 # $ in assignments var = 42 # Spaces around = in assignments
for $var in *; do ... # $ in for loop variables $foo=42 # $ in assignments
var$n="Hello" # Wrong indirect assignment for $var in *; do ... # $ in for loop variables
echo ${var$n} # Wrong indirect reference var$n="Hello" # Wrong indirect assignment
var=(1, 2, 3) # Comma separated arrays echo ${var$n} # Wrong indirect reference
array=( [index] = value ) # Incorrect index initialization var=(1, 2, 3) # Comma separated arrays
echo "Argument 10 is $10" # Positional parameter misreference array=( [index] = value ) # Incorrect index initialization
if $(myfunction); then ..; fi # Wrapping commands in $() echo "Argument 10 is $10" # Positional parameter misreference
else if othercondition; then .. # Using 'else if' if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
```
### Style
#### Style
ShellCheck can make suggestions to improve style: ShellCheck can make suggestions to improve style:
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead ```sh
a >> log; b >> log; c >> log # Use a redirection block instead [[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
echo "The time is `date`" # Use $() instead a >> log; b >> log; c >> log # Use a redirection block instead
cd dir; process *; cd ..; # Use subshells instead echo "The time is `date`" # Use $() instead
echo $[1+2] # Use standard $((..)) instead of old $[] cd dir; process *; cd ..; # Use subshells instead
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..)) echo $[1+2] # Use standard $((..)) instead of old $[]
echo "$(date)" # Useless use of echo echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
cat file | grep foo # Useless use of cat echo "$(date)" # Useless use of echo
cat file | grep foo # Useless use of cat
```
### Data and typing errors
#### Data and typing errors
ShellCheck can recognize issues related to data and typing: ShellCheck can recognize issues related to data and typing:
args="$@" # Assigning arrays to strings ```sh
files=(foo bar); echo "$files" # Referencing arrays as strings args="$@" # Assigning arrays to strings
declare -A arr=(foo bar) # Associative arrays without index files=(foo bar); echo "$files" # Referencing arrays as strings
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays declare -A arr=(foo bar) # Associative arrays without index
[[ $# > 2 ]] # Comparing numbers as strings printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
var=World; echo "Hello " var # Unused lowercase variables [[ $# > 2 ]] # Comparing numbers as strings
echo "Hello $name" # Unassigned lowercase variables var=World; echo "Hello " var # Unused lowercase variables
cmd | read bar; echo $bar # Assignments in subshells echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
```
### Robustness
#### Robustness
ShellCheck can make suggestions for improving the robustness of a script: ShellCheck can make suggestions for improving the robustness of a script:
rm -rf "$STEAMROOT/"* # Catastrophic rm ```sh
touch ./-l; ls * # Globs that could become options rm -rf "$STEAMROOT/"* # Catastrophic rm
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection touch ./-l; ls * # Globs that could become options
printf "Hello $name" # Variables in printf format find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
for f in $(ls *.txt); do # Iterating over ls output printf "Hello $name" # Variables in printf format
export MYVAR=$(cmd) # Masked exit codes for f in $(ls *.txt); do # Iterating over ls output
export MYVAR=$(cmd) # Masked exit codes
```
### Portability
#### Portability
ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`: 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`:
```sh
echo {1..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh
time sleep 1 | sleep 5 # Undefined uses of 'time'
```
echo {1..$n} # Works in ksh, but not bash/dash/sh ### Miscellaneous
echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh
time sleep 1 | sleep 5 # Undefined uses of 'time'
#### Miscellaneous
ShellCheck recognizes a menagerie of other issues: ShellCheck recognizes a menagerie of other issues:
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\] ```sh
PATH="$PATH:~/bin" # Literal tilde in $PATH PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
rm “file” # Unicode quotes PATH="$PATH:~/bin" # Literal tilde in $PATH
echo "Hello world" # Carriage return / DOS line endings rm “file” # Unicode quotes
echo hello \ # Trailing spaces after \ echo "Hello world" # Carriage return / DOS line endings
var=42 echo $var # Expansion of inlined environment echo hello \ # Trailing spaces after \
#!/bin/bash -x -e # Common shebang errors var=42 echo $var # Expansion of inlined environment
echo $((n/180*100)) # Unnecessary loss of precision #!/bin/bash -x -e # Common shebang errors
ls *[:digit:].txt # Bad character class globs echo $((n/180*100)) # Unnecessary loss of precision
sed 's/foo/bar/' file > file # Redirecting to input ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input
```
## Testimonials ## Testimonials
@@ -334,15 +409,15 @@ Please use the GitHub issue tracker for any bugs or feature suggestions:
https://github.com/koalaman/shellcheck/issues https://github.com/koalaman/shellcheck/issues
## Contributing ## Contributing
Please submit patches to code or documentation as GitHub pull requests! Please submit patches to code or documentation as GitHub pull requests! Check
out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the
ShellCheck Wiki.
Contributions must be licensed under the GNU GPLv3. Contributions must be licensed under the GNU GPLv3.
The contributor retains the copyright. The contributor retains the copyright.
## Copyright ## Copyright
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE). ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.4.6 Version: 0.4.7
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -70,7 +70,6 @@ library
executable shellcheck executable shellcheck
build-depends: build-depends:
ShellCheck,
base >= 4 && < 5, base >= 4 && < 5,
containers, containers,
directory, directory,
@@ -84,7 +83,6 @@ executable shellcheck
test-suite test-shellcheck test-suite test-shellcheck
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
build-depends: build-depends:
ShellCheck,
base >= 4 && < 5, base >= 4 && < 5,
containers, containers,
directory, directory,

View File

@@ -19,21 +19,21 @@
-} -}
module ShellCheck.AST where module ShellCheck.AST where
import Control.Monad
import Control.Monad.Identity import Control.Monad.Identity
import Text.Parsec import Text.Parsec
import qualified ShellCheck.Regex as Re import qualified ShellCheck.Regex as Re
import Prelude hiding (id)
data Id = Id Int deriving (Show, Eq, Ord) newtype Id = Id Int deriving (Show, Eq, Ord)
data Quoted = Quoted | Unquoted deriving (Show, Eq) data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq) data Dashed = Dashed | Undashed deriving (Show, Eq)
data AssignmentMode = Assign | Append deriving (Show, Eq) data AssignmentMode = Assign | Append deriving (Show, Eq)
data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq) newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq) newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq) data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
data Root = Root Token newtype Root = Root Token
data Token = data Token =
TA_Binary Id String Token Token TA_Binary Id String Token Token
| TA_Assignment Id String Token Token | TA_Assignment Id String Token Token
@@ -48,8 +48,9 @@ data Token =
| TC_Nullary Id ConditionType Token | TC_Nullary Id ConditionType Token
| TC_Or Id ConditionType String Token Token | TC_Or Id ConditionType String Token Token
| TC_Unary Id ConditionType String Token | TC_Unary Id ConditionType String Token
| TC_Empty Id ConditionType
| T_AND_IF Id | T_AND_IF Id
| T_AndIf Id (Token) (Token) | T_AndIf Id Token Token
| T_Arithmetic Id Token | T_Arithmetic Id Token
| T_Array Id [Token] | T_Array Id [Token]
| T_IndexedElement Id [Token] Token | T_IndexedElement Id [Token] Token
@@ -110,7 +111,7 @@ data Token =
| T_NEWLINE Id | T_NEWLINE Id
| T_NormalWord Id [Token] | T_NormalWord Id [Token]
| T_OR_IF Id | T_OR_IF Id
| T_OrIf Id (Token) (Token) | T_OrIf Id Token Token
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz} | T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands] | T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token] | T_ProcSub Id String [Token]
@@ -162,11 +163,6 @@ analyze f g i =
i newT i newT
roundAll = mapM round roundAll = mapM round
roundMaybe Nothing = return Nothing
roundMaybe (Just v) = do
s <- round v
return (Just s)
dl l v = do dl l v = do
x <- roundAll l x <- roundAll l
return $ v x return $ v x
@@ -277,6 +273,7 @@ analyze f g i =
delve (T_Include id includer script) = d2 includer script $ T_Include id delve (T_Include id includer script) = d2 includer script $ T_Include id
delve t = return t delve t = return t
getId :: Token -> Id
getId t = case t of getId t = case t of
T_AND_IF id -> id T_AND_IF id -> id
T_OR_IF id -> id T_OR_IF id -> id
@@ -376,10 +373,14 @@ getId t = case t of
T_CoProcBody id _ -> id T_CoProcBody id _ -> id
T_Include id _ _ -> id T_Include id _ _ -> id
T_UnparsedIndex id _ _ -> id T_UnparsedIndex id _ _ -> id
TC_Empty id _ -> id
blank :: Monad m => Token -> m () blank :: Monad m => Token -> m ()
blank = const $ return () blank = const $ return ()
doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token
doAnalysis f = analyze f blank return doAnalysis f = analyze f blank return
doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token
doStackAnalysis startToken endToken = analyze startToken endToken return doStackAnalysis startToken endToken = analyze startToken endToken return
doTransform :: (Token -> Token) -> Token -> Token
doTransform i = runIdentity . analyze blank blank (return . i) doTransform i = runIdentity . analyze blank blank (return . i)

View File

@@ -48,8 +48,8 @@ willSplit x =
T_NormalWord _ l -> any willSplit l T_NormalWord _ l -> any willSplit l
_ -> False _ -> False
isGlob (T_Extglob {}) = True isGlob T_Extglob {} = True
isGlob (T_Glob {}) = True isGlob T_Glob {} = True
isGlob (T_NormalWord _ l) = any isGlob l isGlob (T_NormalWord _ l) = any isGlob l
isGlob _ = False isGlob _ = False
@@ -119,6 +119,16 @@ getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
-- Check if a command has a flag. -- Check if a command has a flag.
hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd) hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd)
-- Is this token a word that starts with a dash?
isFlag token =
case getWordParts token of
T_Literal _ ('-':_) : _ -> True
_ -> False
-- Is this token a flag where the - is unquoted?
isUnquotedFlag token = fromMaybe False $ do
str <- getLeadingUnquotedString token
return $ "-" `isPrefixOf` str
-- Given a T_DollarBraced, return a simplified version of the string contents. -- Given a T_DollarBraced, return a simplified version of the string contents.
bracedString (T_DollarBraced _ l) = concat $ oversimplify l bracedString (T_DollarBraced _ l) = concat $ oversimplify l
@@ -144,9 +154,9 @@ mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
-- Is it certain that this word will becomes multiple words? -- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t willBecomeMultipleArgs t = willConcatInAssignment t || f t
where where
f (T_Extglob {}) = True f T_Extglob {} = True
f (T_Glob {}) = True f T_Glob {} = True
f (T_BraceExpansion {}) = True f T_BraceExpansion {} = True
f (T_DoubleQuoted _ parts) = any f parts f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts f (T_NormalWord _ parts) = any f parts
f _ = False f _ = False
@@ -154,7 +164,7 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
-- This does token cause implicit concatenation in assignments? -- This does token cause implicit concatenation in assignments?
willConcatInAssignment token = willConcatInAssignment token =
case token of case token of
t@(T_DollarBraced {}) -> isArrayExpansion t t@T_DollarBraced {} -> isArrayExpansion t
(T_DoubleQuoted _ parts) -> any willConcatInAssignment parts (T_DoubleQuoted _ parts) -> any willConcatInAssignment parts
(T_NormalWord _ parts) -> any willConcatInAssignment parts (T_NormalWord _ parts) -> any willConcatInAssignment parts
_ -> False _ -> False
@@ -169,7 +179,7 @@ onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
-- Maybe get a literal string, but only if it's an unquoted argument. -- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) = getUnquotedLiteral (T_NormalWord _ list) =
liftM concat $ mapM str list concat <$> mapM str list
where where
str (T_Literal _ s) = return s str (T_Literal _ s) = return s
str _ = Nothing str _ = Nothing
@@ -186,9 +196,16 @@ getTrailingUnquotedLiteral t =
where where
from t = from t =
case t of case t of
(T_Literal {}) -> return t T_Literal {} -> return t
_ -> Nothing _ -> Nothing
-- Get the leading, unquoted, literal string of a token (if any).
getLeadingUnquotedString :: Token -> Maybe String
getLeadingUnquotedString t =
case t of
T_NormalWord _ ((T_Literal _ s) : _) -> return s
_ -> Nothing
-- Maybe get the literal string of this token and any globs in it. -- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f getGlobOrLiteralString = getLiteralStringExt f
where where
@@ -200,7 +217,7 @@ getGlobOrLiteralString = getLiteralStringExt f
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
getLiteralStringExt more = g getLiteralStringExt more = g
where where
allInList = liftM concat . mapM g allInList = fmap concat . mapM g
g (T_DoubleQuoted _ l) = allInList l g (T_DoubleQuoted _ l) = allInList l
g (T_DollarDoubleQuoted _ l) = allInList l g (T_DollarDoubleQuoted _ l) = allInList l
g (T_NormalWord _ l) = allInList l g (T_NormalWord _ l) = allInList l
@@ -237,7 +254,7 @@ getCommand t =
T_Redirecting _ _ w -> getCommand w T_Redirecting _ _ w -> getCommand w
T_SimpleCommand _ _ (w:_) -> return t T_SimpleCommand _ _ (w:_) -> return t
T_Annotation _ _ t -> getCommand t T_Annotation _ _ t -> getCommand t
otherwise -> Nothing _ -> Nothing
-- Maybe get the command name of a token representing a command -- Maybe get the command name of a token representing a command
getCommandName t = do getCommandName t = do
@@ -259,13 +276,13 @@ getCommandNameFromExpansion t =
T_DollarExpansion _ [c] -> extract c T_DollarExpansion _ [c] -> extract c
T_Backticked _ [c] -> extract c T_Backticked _ [c] -> extract c
T_DollarBraceCommandExpansion _ [c] -> extract c T_DollarBraceCommandExpansion _ [c] -> extract c
otherwise -> Nothing _ -> Nothing
where where
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
extract _ = Nothing extract _ = Nothing
-- Get the basename of a token representing a command -- Get the basename of a token representing a command
getCommandBasename = liftM basename . getCommandName getCommandBasename = fmap basename . getCommandName
where where
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
@@ -275,7 +292,7 @@ isAssignment t =
T_SimpleCommand _ (w:_) [] -> True T_SimpleCommand _ (w:_) [] -> True
T_Assignment {} -> True T_Assignment {} -> True
T_Annotation _ _ w -> isAssignment w T_Annotation _ _ w -> isAssignment w
otherwise -> False _ -> False
isOnlyRedirection t = isOnlyRedirection t =
case t of case t of
@@ -283,7 +300,7 @@ isOnlyRedirection t =
T_Annotation _ _ w -> isOnlyRedirection w T_Annotation _ _ w -> isOnlyRedirection w
T_Redirecting _ (_:_) c -> isOnlyRedirection c T_Redirecting _ (_:_) c -> isOnlyRedirection c
T_SimpleCommand _ [] [] -> True T_SimpleCommand _ [] [] -> True
otherwise -> False _ -> False
isFunction t = case t of T_Function {} -> True; _ -> False isFunction t = case t of T_Function {} -> True; _ -> False
@@ -291,6 +308,7 @@ isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as -- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements. -- the body of while loops or branches of if statements.
getCommandSequences :: Token -> [[Token]]
getCommandSequences t = getCommandSequences t =
case t of case t of
T_Script _ _ cmds -> [cmds] T_Script _ _ cmds -> [cmds]
@@ -301,16 +319,18 @@ getCommandSequences t =
T_ForIn _ _ _ cmds -> [cmds] T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds] T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses] T_IfExpression _ thens elses -> map snd thens ++ [elses]
otherwise -> [] T_Annotation _ _ t -> getCommandSequences t
_ -> []
-- Get a list of names of associative arrays -- Get a list of names of associative arrays
getAssociativeArrays t = getAssociativeArrays t =
nub . execWriter $ doAnalysis f t nub . execWriter $ doAnalysis f t
where where
f :: Token -> Writer [String] () f :: Token -> Writer [String] ()
f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
name <- getCommandName t name <- getCommandName t
guard $ name == "declare" || name == "typeset" let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames
let flags = getAllFlags t let flags = getAllFlags t
guard $ elem "A" $ map snd flags guard $ elem "A" $ map snd flags
let args = map fst . filter ((==) "" . snd) $ flags let args = map fst . filter ((==) "" . snd) $ flags
@@ -321,7 +341,7 @@ getAssociativeArrays t =
nameAssignments t = nameAssignments t =
case t of case t of
T_Assignment _ _ name _ _ -> return name T_Assignment _ _ name _ _ -> return name
otherwise -> Nothing _ -> Nothing
-- A Pseudoglob is a wildcard pattern used for checking if a match can succeed. -- A Pseudoglob is a wildcard pattern used for checking if a match can succeed.
-- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which -- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which
@@ -333,7 +353,7 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
-- PGMany. -- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob] wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToPseudoGlob word = wordToPseudoGlob word =
simplifyPseudoGlob <$> concat <$> mapM f (getWordParts word) simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where where
f x = case x of f x = case x of
T_Literal _ s -> return $ map PGChar s T_Literal _ s -> return $ map PGChar s
@@ -351,6 +371,19 @@ wordToPseudoGlob word =
_ -> return [PGMany] _ -> return [PGMany]
-- Turn a word into a PG pattern, but only if we can preserve
-- exact semantics.
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToExactPseudoGlob word =
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_Glob _ "?" -> return [PGAny]
T_Glob _ "*" -> return [PGMany]
_ -> fail "Unknown token type"
-- Reorder a PseudoGlob for more efficient matching, e.g. -- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g -- f?*?**g -> f??*g
simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob] simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
@@ -382,5 +415,22 @@ pseudoGlobsCanOverlap = matchable
matchable (_:_) [] = False matchable (_:_) [] = False
matchable [] r = matchable r [] matchable [] r = matchable r []
-- Check whether the first pattern always overlaps the second.
pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool
pseudoGlobIsSuperSetof = matchable
where
matchable x@(xf:xs) y@(yf:ys) =
case (xf, yf) of
(PGMany, PGMany) -> matchable x ys
(PGMany, _) -> matchable x ys || matchable xs y
(_, PGMany) -> False
(PGAny, _) -> matchable xs ys
(_, PGAny) -> False
(_, _) -> xf == yf && matchable xs ys
matchable [] [] = True
matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False
wordsCanBeEqual x y = fromMaybe True $ wordsCanBeEqual x y = fromMaybe True $
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y) liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)

View File

@@ -41,7 +41,7 @@ import Data.List
import Data.Maybe import Data.Maybe
import Data.Ord import Data.Ord
import Debug.Trace import Debug.Trace
import qualified Data.Map as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
@@ -61,8 +61,9 @@ treeChecks = [
,checkArrayWithoutIndex ,checkArrayWithoutIndex
,checkShebang ,checkShebang
,checkUnassignedReferences ,checkUnassignedReferences
,checkUncheckedCd ,checkUncheckedCdPushdPopd
,checkArrayAssignmentIndices ,checkArrayAssignmentIndices
,checkUseBeforeDefinition
] ]
runAnalytics :: AnalysisSpec -> [TokenComment] runAnalytics :: AnalysisSpec -> [TokenComment]
@@ -141,7 +142,6 @@ nodeChecks = [
,checkWrongArithmeticAssignment ,checkWrongArithmeticAssignment
,checkConditionalAndOrs ,checkConditionalAndOrs
,checkFunctionDeclarations ,checkFunctionDeclarations
,checkCatastrophicRm
,checkStderrPipe ,checkStderrPipe
,checkOverridingPath ,checkOverridingPath
,checkArrayAsString ,checkArrayAsString
@@ -160,6 +160,12 @@ nodeChecks = [
,checkRedirectedNowhere ,checkRedirectedNowhere
,checkUnmatchableCases ,checkUnmatchableCases
,checkSubshellAsTest ,checkSubshellAsTest
,checkSplittingInArrays
,checkRedirectionToNumber
,checkGlobAsCommand
,checkFlagAsCommand
,checkEmptyCondition
,checkPipeToNowhere
] ]
@@ -255,15 +261,25 @@ prop_checkAssignAteCommand1 = verify checkAssignAteCommand "A=ls -l"
prop_checkAssignAteCommand2 = verify checkAssignAteCommand "A=ls --sort=$foo" prop_checkAssignAteCommand2 = verify checkAssignAteCommand "A=ls --sort=$foo"
prop_checkAssignAteCommand3 = verify checkAssignAteCommand "A=cat foo | grep bar" prop_checkAssignAteCommand3 = verify checkAssignAteCommand "A=cat foo | grep bar"
prop_checkAssignAteCommand4 = verifyNot checkAssignAteCommand "A=foo ls -l" prop_checkAssignAteCommand4 = verifyNot checkAssignAteCommand "A=foo ls -l"
prop_checkAssignAteCommand5 = verifyNot checkAssignAteCommand "PAGER=cat grep bar" prop_checkAssignAteCommand5 = verify checkAssignAteCommand "PAGER=cat grep bar"
checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) (firstWord:_)) = prop_checkAssignAteCommand6 = verifyNot checkAssignAteCommand "PAGER=\"cat\" grep bar"
when ("-" `isPrefixOf` concat (oversimplify firstWord) || prop_checkAssignAteCommand7 = verify checkAssignAteCommand "here=pwd"
isCommonCommand (getLiteralString assignmentTerm) checkAssignAteCommand _ (T_SimpleCommand id (T_Assignment _ _ _ _ assignmentTerm:[]) list) =
&& not (isCommonCommand (getLiteralString firstWord))) $ -- Check if first word is intended as an argument (flag or glob).
warn id 2037 "To assign the output of a command, use var=$(cmd) ." if firstWordIsArg list
then
err id 2037 "To assign the output of a command, use var=$(cmd) ."
else
-- Check if it's a known, unquoted command name.
when (isCommonCommand $ getUnquotedLiteral assignmentTerm) $
warn id 2209 "Use var=$(command) to assign output (or quote to assign string)."
where where
isCommonCommand (Just s) = s `elem` commonCommands isCommonCommand (Just s) = s `elem` commonCommands
isCommonCommand _ = False isCommonCommand _ = False
firstWordIsArg list = fromMaybe False $ do
head <- list !!! 0
return $ isGlob head || isUnquotedFlag head
checkAssignAteCommand _ _ = return () checkAssignAteCommand _ _ = return ()
prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1" prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1"
@@ -349,21 +365,18 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
]) $ warn (getId find) 2038 ]) $ warn (getId find) 2038
"Use -print0/-0 or -exec + to allow for non-alphanumeric filenames." "Use -print0/-0 or -exec + to allow for non-alphanumeric filenames."
for ["?", "echo"] $
\(_:echo:_) -> info (getId echo) 2008 "echo doesn't read from stdin, are you sure you should be piping to it?"
for' ["ps", "grep"] $ for' ["ps", "grep"] $
\x -> info x 2009 "Consider using pgrep instead of grepping ps output." \x -> info x 2009 "Consider using pgrep instead of grepping ps output."
for ["grep", "wc"] $ for ["grep", "wc"] $
\(grep:wc:_) -> \(grep:wc:_) ->
let flagsGrep = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep let flagsGrep = fromMaybe [] $ map snd . getAllFlags <$> getCommand grep
flagsWc = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand wc flagsWc = fromMaybe [] $ map snd . getAllFlags <$> getCommand wc
in in
unless ((any (`elem` ["o", "only-matching", "r", "R", "recursive"]) flagsGrep) || (any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc) || ((length flagsWc) == 0)) $ unless (any (`elem` ["o", "only-matching", "r", "R", "recursive"]) flagsGrep || any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc || null flagsWc) $
style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l." style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l."
didLs <- liftM or . sequence $ [ didLs <- fmap or . sequence $ [
for' ["ls", "grep"] $ for' ["ls", "grep"] $
\x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.", \x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.",
for' ["ls", "xargs"] $ for' ["ls", "xargs"] $
@@ -439,7 +452,7 @@ prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done"
prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done" prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done"
checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) = checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =
when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list
|| (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $ || (fmap wouldHaveBeenGlob (getLiteralString word) == Just True)) $
err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once." err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once."
checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) = checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) =
warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . " warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . "
@@ -622,13 +635,11 @@ checkShorthandIf _ _ = return ()
prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done" prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done"
prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*" prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"
prop_checkDollarStar3 = verifyNot checkDollarStar "[[ $* = 'a b' ]]"
checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _)]) checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _)])
| bracedString b == "*" = | bracedString b == "*" =
unless isAssigned $ unless (isStrictlyQuoteFree (parentMap p) t) $
warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems." warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."
where
path = getPath (parentMap p) t
isAssigned = any isAssignment . take 2 $ path
checkDollarStar _ _ = return () checkDollarStar _ _ = return ()
@@ -642,15 +653,12 @@ prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@"
prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done" prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done"
prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\"" prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\""
prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}" prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}"
prop_checkUnquotedDollarAt10 = verifyNot checkUnquotedDollarAt "echo ${@+\"$@\"}"
checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word = checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word =
forM_ (take 1 $ filter isArrayExpansion parts) $ \x -> forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->
unless (isAlternative x) $ unless (isQuotedAlternativeReference x) $
err (getId x) 2068 err (getId x) 2068
"Double quote array expansions to avoid re-splitting elements." "Double quote array expansions to avoid re-splitting elements."
where
-- Fixme: should detect whether the alternative is quoted
isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b
isAlternative _ = False
checkUnquotedDollarAt _ _ = return () checkUnquotedDollarAt _ _ = return ()
prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\"" prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\""
@@ -658,7 +666,7 @@ prop_checkConcatenatedDollarAt2 = verify checkConcatenatedDollarAt "echo ${arr[@
prop_checkConcatenatedDollarAt3 = verify checkConcatenatedDollarAt "echo $a$@" prop_checkConcatenatedDollarAt3 = verify checkConcatenatedDollarAt "echo $a$@"
prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@" prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@"
prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\"" prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\""
checkConcatenatedDollarAt p word@(T_NormalWord {}) checkConcatenatedDollarAt p word@T_NormalWord {}
| not $ isQuoteFree (parentMap p) word = | not $ isQuoteFree (parentMap p) word =
unless (null $ drop 1 parts) $ unless (null $ drop 1 parts) $
mapM_ for array mapM_ for array
@@ -777,6 +785,7 @@ prop_checkSingleQuotedVariables10= verify checkSingleQuotedVariables "echo '`pwd
prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'" prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'"
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'" prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'" prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"
checkSingleQuotedVariables params t@(T_SingleQuoted id s) = checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
when (s `matches` re) $ when (s `matches` re) $
if "sed" == commandName if "sed" == commandName
@@ -814,6 +823,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
isOkAssignment t = isOkAssignment t =
case t of case t of
T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted
TC_Unary _ _ "-v" _ -> True
_ -> False _ -> False
re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`" re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`"
@@ -848,25 +858,33 @@ prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]
prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]" prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]"
prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]" prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]"
prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]" prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"
prop_checkNumberComparisons11= verify checkNumberComparisons "[ $foo -eq 'N' ]" prop_checkNumberComparisons11 = verify checkNumberComparisons "[ $foo -eq 'N' ]"
prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]" prop_checkNumberComparisons12 = verify checkNumberComparisons "[ x$foo -gt x${N} ]"
prop_checkNumberComparisons13 = verify checkNumberComparisons "[ $foo > $bar ]"
prop_checkNumberComparisons14 = verifyNot checkNumberComparisons "[[ foo < bar ]]"
prop_checkNumberComparisons15 = verifyNot checkNumberComparisons "[ $foo '>' $bar ]"
checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
if isNum lhs && not (isNonNum rhs) if isNum lhs || isNum rhs
|| isNum rhs && not (isNonNum lhs)
then do then do
when (isLtGt op) $ when (isLtGt op) $
err id 2071 $ err id 2071 $
op ++ " is for string comparisons. Use " ++ eqv op ++ " instead." op ++ " is for string comparisons. Use " ++ eqv op ++ " instead."
when (isLeGe op) $ when (isLeGe op && hasStringComparison) $
err id 2071 $ op ++ " is not a valid operator. " ++ err id 2071 $ op ++ " is not a valid operator. " ++
"Use " ++ eqv op ++ " ." "Use " ++ eqv op ++ " ."
else do else do
when (isLeGe op || isLtGt op) $ when (isLeGe op || isLtGt op) $
mapM_ checkDecimals [lhs, rhs] mapM_ checkDecimals [lhs, rhs]
when (isLeGe op) $ when (isLeGe op && hasStringComparison) $
err id 2122 $ op ++ " is not a valid operator. " ++ err id 2122 $ op ++ " is not a valid operator. " ++
"Use '! a " ++ invert op ++ " b' instead." "Use '! a " ++ esc ++ invert op ++ " b' instead."
when (typ == SingleBracket && op `elem` ["<", ">"]) $
case shellType params of
Sh -> return () -- These are unsupported and will be caught by bashism checks.
Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting."
_ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])."
when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do
mapM_ checkDecimals [lhs, rhs] mapM_ checkDecimals [lhs, rhs]
@@ -874,6 +892,7 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
checkStrings [lhs, rhs] checkStrings [lhs, rhs]
where where
hasStringComparison = shellType params /= Sh
isLtGt = flip elem ["<", "\\<", ">", "\\>"] isLtGt = flip elem ["<", "\\<", ">", "\\>"]
isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="] isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="]
@@ -883,8 +902,8 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
decimalError = "Decimals are not supported. " ++ decimalError = "Decimals are not supported. " ++
"Either use integers only, or use bc or awk to compare." "Either use integers only, or use bc or awk to compare."
checkStrings hs = checkStrings =
mapM_ stringError . take 1 . filter isNonNum $ hs mapM_ stringError . take 1 . filter isNonNum
isNonNum t = fromMaybe False $ do isNonNum t = fromMaybe False $ do
s <- getLiteralStringExt (const $ return "") t s <- getLiteralStringExt (const $ return "") t
@@ -923,26 +942,19 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
invert "<=" = ">" invert "<=" = ">"
invert ">=" = "<" invert ">=" = "<"
floatRegex = mkRegex "^[0-9]+\\.[0-9]+$" floatRegex = mkRegex "^[-+]?[0-9]+\\.[0-9]+$"
checkNumberComparisons _ _ = return () checkNumberComparisons _ _ = return ()
prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]" prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]"
prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]" checkSingleBracketOperators params (TC_Binary id SingleBracket "=~" lhs rhs) =
prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]" when (shellType params `elem` [Bash, Ksh]) $
prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done" err id 2074 $ "Can't use =~ in [ ]. Use [[..]] instead."
prop_checkSingleBracketOperators6 = verifyNot checkSingleBracketOperators "[ $foo '>' $bar ]"
checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)
| typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] =
err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]."
checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)
| typ == SingleBracket && op == "=~" =
err id 2074 $ "Can't use " ++ op ++" in [ ]. Use [[..]] instead."
checkSingleBracketOperators _ _ = return () checkSingleBracketOperators _ _ = return ()
prop_checkDoubleBracketOperators1 = verify checkDoubleBracketOperators "[[ 3 \\< 4 ]]" prop_checkDoubleBracketOperators1 = verify checkDoubleBracketOperators "[[ 3 \\< 4 ]]"
prop_checkDoubleBracketOperators3 = verifyNot checkDoubleBracketOperators "[[ foo < bar ]]" prop_checkDoubleBracketOperators3 = verifyNot checkDoubleBracketOperators "[[ foo < bar ]]"
checkDoubleBracketOperators _ x@(TC_Binary id typ op lhs rhs) checkDoubleBracketOperators _ x@(TC_Binary id typ op lhs rhs)
| typ == DoubleBracket && op `elem` ["\\<", "\\>", "\\<=", "\\>="] = | typ == DoubleBracket && op `elem` ["\\<", "\\>"] =
err id 2075 $ "Escaping " ++ op ++" is required in [..], but invalid in [[..]]" err id 2075 $ "Escaping " ++ op ++" is required in [..], but invalid in [[..]]"
checkDoubleBracketOperators _ _ = return () checkDoubleBracketOperators _ _ = return ()
@@ -967,7 +979,7 @@ checkConditionalAndOrs _ t =
(TC_Or id SingleBracket "-o" _ _) -> (TC_Or id SingleBracket "-o" _ _) ->
warn id 2166 "Prefer [ p ] || [ q ] as [ p -o q ] is not well defined." warn id 2166 "Prefer [ p ] || [ q ] as [ p -o q ] is not well defined."
otherwise -> return () _ -> return ()
prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]" prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]"
prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]" prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]"
@@ -1115,11 +1127,13 @@ prop_checkArithmeticDeref10= verifyNot checkArithmeticDeref "(( a[\\$foo] ))"
prop_checkArithmeticDeref11= verifyNot checkArithmeticDeref "a[$foo]=wee" prop_checkArithmeticDeref11= verifyNot checkArithmeticDeref "a[$foo]=wee"
prop_checkArithmeticDeref12= verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done" prop_checkArithmeticDeref12= verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done"
prop_checkArithmeticDeref13= verifyNot checkArithmeticDeref "(( $$ ))" prop_checkArithmeticDeref13= verifyNot checkArithmeticDeref "(( $$ ))"
prop_checkArithmeticDeref14= verifyNot checkArithmeticDeref "(( $! ))"
prop_checkArithmeticDeref15= verifyNot checkArithmeticDeref "(( ${!var} ))"
checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) = checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
unless (isException $ bracedString b) getWarning unless (isException $ bracedString b) getWarning
where where
isException [] = True isException [] = True
isException s = any (`elem` "/.:#%?*@$-") s || isDigit (head s) isException s = any (`elem` "/.:#%?*@$-!") s || isDigit (head s)
getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t
warningFor t = warningFor t =
case t of case t of
@@ -1238,7 +1252,7 @@ checkUuoeVar _ p =
unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $ unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $
when (all couldBeOptimized vars) $ style id 2116 when (all couldBeOptimized vars) $ style id 2116
"Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'." "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."
otherwise -> return () _ -> return ()
prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1" prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"
@@ -1254,12 +1268,12 @@ checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors, suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors,
case t of -- and >> and similar redirections because these are probably not comparisons. case t of -- and >> and similar redirections because these are probably not comparisons.
T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op
otherwise -> False _ -> False
isComparison t = isComparison t =
case t of case t of
T_Greater _ -> True T_Greater _ -> True
T_Less _ -> True T_Less _ -> True
otherwise -> False _ -> False
checkTestRedirects _ _ = return () checkTestRedirects _ _ = return ()
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
@@ -1350,10 +1364,11 @@ prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUE
prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\"" prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\"" prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\""
prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}" prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}"
prop_checkInexplicablyUnquoted8 = verifyNot checkInexplicablyUnquoted " 'foo'\\\n 'bar'"
checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens) checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
where where
check (T_SingleQuoted _ _:T_Literal id str:_) check (T_SingleQuoted _ _:T_Literal id str:_)
| all isAlphaNum str = | not (null str) && all isAlphaNum str =
info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? " info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? "
check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) = check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) =
@@ -1502,10 +1517,11 @@ prop_subshellAssignmentCheck15 = verifyNotTree subshellAssignmentCheck "#!/bin/k
prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@" prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@"
prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar" prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar"
prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n" prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n"
prop_subshellAssignmentCheck19 = verifyNotTree subshellAssignmentCheck "#!/bin/bash\nshopt -s lastpipe; echo a | read -r b; echo \"$b\""
subshellAssignmentCheck params t = subshellAssignmentCheck params t =
let flow = variableFlow params let flow = variableFlow params
check = findSubshelled flow [("oops",[])] Map.empty check = findSubshelled flow [("oops",[])] Map.empty
in snd $ runWriter check in execWriter check
findSubshelled [] _ _ = return () findSubshelled [] _ _ = return ()
@@ -1586,6 +1602,7 @@ prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\
prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]" prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done" prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1" prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
checkSpacefulness params t = checkSpacefulness params t =
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params) doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
@@ -1604,9 +1621,9 @@ checkSpacefulness params t =
return [makeComment InfoC (getId token) 2086 warning | return [makeComment InfoC (getId token) 2086 warning |
isExpansion token && spaced isExpansion token && spaced
&& not (isArrayExpansion token) -- There's another warning for this && not (isArrayExpansion token) -- There's another warning for this
&& not (isCounting token) && not (isCountingReference token)
&& not (isQuoteFree parents token) && not (isQuoteFree parents token)
&& not (isQuotedAlternative token) && not (isQuotedAlternativeReference token)
&& not (usedAsCommandName parents token)] && not (usedAsCommandName parents token)]
where where
warning = "Double quote to prevent globbing and word splitting." warning = "Double quote to prevent globbing and word splitting."
@@ -1629,19 +1646,6 @@ checkSpacefulness params t =
(T_DollarBraced _ _ ) -> True (T_DollarBraced _ _ ) -> True
_ -> False _ -> False
isCounting (T_DollarBraced id token) =
case concat $ oversimplify token of
'#':_ -> True
_ -> False
isCounting _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternative t =
case t of
T_DollarBraced _ _ ->
":+" `isInfixOf` bracedString t
_ -> False
isSpacefulWord :: (String -> Bool) -> [Token] -> Bool isSpacefulWord :: (String -> Bool) -> [Token] -> Bool
isSpacefulWord f = any (isSpaceful f) isSpacefulWord f = any (isSpaceful f)
isSpaceful :: (String -> Bool) -> Token -> Bool isSpaceful :: (String -> Bool) -> Token -> Bool
@@ -1675,7 +1679,7 @@ prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/
checkQuotesInLiterals params t = checkQuotesInLiterals params t =
doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)
where where
getQuotes name = liftM (Map.lookup name) get getQuotes name = fmap (Map.lookup name) get
setQuotes name ref = modify $ Map.insert name ref setQuotes name ref = modify $ Map.insert name ref
deleteQuotes = modify . Map.delete deleteQuotes = modify . Map.delete
parents = parentMap params parents = parentMap params
@@ -1706,7 +1710,7 @@ checkQuotesInLiterals params t =
squashesQuotes t = squashesQuotes t =
case t of case t of
T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t
otherwise -> False _ -> False
readF _ expr name = do readF _ expr name = do
assignment <- getQuotes name assignment <- getQuotes name
@@ -1805,6 +1809,8 @@ prop_checkUnused32= verifyTree checkUnusedAssignments "let a=b=c; echo $a"
prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]" prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]"
prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t" prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t"
prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}" prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"
prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
where where
flow = variableFlow params flow = variableFlow params
@@ -1855,6 +1861,10 @@ prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "decla
prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b" prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b"
prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}" prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}"
prop_checkUnassignedReferences28= verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n" prop_checkUnassignedReferences28= verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n"
prop_checkUnassignedReferences29= verifyNotTree checkUnassignedReferences "if [[ -v foo ]]; then echo $foo; fi"
prop_checkUnassignedReferences30= verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"
prop_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"
prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"
checkUnassignedReferences params t = warnings checkUnassignedReferences params t = warnings
where where
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty) (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
@@ -1863,7 +1873,7 @@ checkUnassignedReferences params t = warnings
tally (Assignment (_, _, name, _)) = tally (Assignment (_, _, name, _)) =
modify (\(read, written) -> (read, Map.insert name () written)) modify (\(read, written) -> (read, Map.insert name () written))
tally (Reference (_, place, name)) = tally (Reference (_, place, name)) =
modify (\(read, written) -> (Map.insertWith' (const id) name place read, written)) modify (\(read, written) -> (Map.insertWith (const id) name place read, written))
tally _ = return () tally _ = return ()
unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned
@@ -1909,7 +1919,7 @@ checkUnassignedReferences params t = warnings
-- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these. -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.
isInArray var t = any isArray $ getPath (parentMap params) t isInArray var t = any isArray $ getPath (parentMap params) t
where where
isArray (T_Array {}) = True isArray T_Array {} = True
isArray b@(T_DollarBraced _ _) | var /= getBracedReference (bracedString b) = True isArray b@(T_DollarBraced _ _) | var /= getBracedReference (bracedString b) = True
isArray _ = False isArray _ = False
@@ -1929,8 +1939,9 @@ checkUnassignedReferences params t = warnings
prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt" prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"
prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*" prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*"
prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt" prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"
prop_checkGlobsAsOptions4 = verifyNot checkGlobsAsOptions "*.txt"
checkGlobsAsOptions _ (T_SimpleCommand _ _ args) = checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =
mapM_ check $ takeWhile (not . isEndOfArgs) args mapM_ check $ takeWhile (not . isEndOfArgs) (drop 1 args)
where where
check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" = check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" =
info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options." info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options."
@@ -2007,7 +2018,7 @@ checkPrefixAssignmentReference params t@(T_DollarBraced id value) =
check (t:rest) = check (t:rest) =
case t of case t of
T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars
otherwise -> check rest _ -> check rest
checkVar (T_Assignment aId mode aName [] value) | checkVar (T_Assignment aId mode aName [] value) |
aName == name && (aId `notElem` idPath) = do aName == name && (aId `notElem` idPath) = do
warn aId 2097 "This assignment is only seen by the forked process." warn aId 2097 "This assignment is only seen by the forked process."
@@ -2092,7 +2103,7 @@ checkLoopKeywordScope params t |
where where
name = getCommandName t name = getCommandName t
path = let p = getPath (parentMap params) t in filter relevant p path = let p = getPath (parentMap params) t in filter relevant p
subshellType t = case leadType (shellType params) (parentMap params) t of subshellType t = case leadType params t of
NoneScope -> Nothing NoneScope -> Nothing
SubshellScope str -> return str SubshellScope str -> return str
relevant t = isLoop t || isFunction t || isJust (subshellType t) relevant t = isLoop t || isFunction t || isJust (subshellType t)
@@ -2121,72 +2132,6 @@ checkFunctionDeclarations params
checkFunctionDeclarations _ _ = return () checkFunctionDeclarations _ _ = return ()
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =
when (any isRecursiveFlag simpleArgs) $
mapM_ (mapM_ checkWord . braceExpand) tokens
where
simpleArgs = oversimplify t
checkWord token =
case getLiteralString token of
Just str ->
when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $
warn (getId token) 2114 "Warning: deletes a system directory. Use 'rm --' to disable this message."
Nothing ->
checkWord' token
checkWord' token = fromMaybe (return ()) $ do
filename <- getPotentialPath token
let path = fixPath filename
return . when (path `elem` importantPaths) $
warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."
fixPath filename =
let normalized = skipRepeating '/' . skipRepeating '*' $ filename in
if normalized == "/" then normalized else stripTrailing '/' normalized
getPotentialPath = getLiteralStringExt f
where
f (T_Glob _ str) = return str
f (T_DollarBraced _ word) =
let var = onlyLiteralString word in
if any (flip isInfixOf var) [":?", ":-", ":="]
then Nothing
else return ""
f _ = return ""
isRecursiveFlag "--recursive" = True
isRecursiveFlag ('-':'-':_) = False
isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str
isRecursiveFlag _ = False
stripTrailing c = reverse . dropWhile (== c) . reverse
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
skipRepeating c (a:r) = a:skipRepeating c r
skipRepeating _ [] = []
paths = [
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
"/var", "/lib"
]
importantPaths = filter (not . null) $
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
checkCatastrophicRm _ _ = return ()
prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar" prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar"
prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar" prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar"
@@ -2210,6 +2155,7 @@ prop_checkUnpassedInFunctions8 = verifyNotTree checkUnpassedInFunctions "foo() {
prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;" prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;"
prop_checkUnpassedInFunctions10= verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;" prop_checkUnpassedInFunctions10= verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;"
prop_checkUnpassedInFunctions11= verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;" prop_checkUnpassedInFunctions11= verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;"
prop_checkUnpassedInFunctions12= verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;"
checkUnpassedInFunctions params root = checkUnpassedInFunctions params root =
execWriter $ mapM_ warnForGroup referenceGroups execWriter $ mapM_ warnForGroup referenceGroups
where where
@@ -2219,7 +2165,7 @@ checkUnpassedInFunctions params root =
functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
findFunction t@(T_Function id _ _ name body) = findFunction t@(T_Function id _ _ name body) =
let flow = getVariableFlow (shellType params) (parentMap params) body let flow = getVariableFlow params body
in in
if any (isPositionalReference t) flow && not (any isPositionalAssignment flow) if any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
then return t then return t
@@ -2236,7 +2182,11 @@ checkUnpassedInFunctions params root =
_ -> False _ -> False
isDirectChildOf child parent = fromMaybe False $ do isDirectChildOf child parent = fromMaybe False $ do
function <- find (\x -> case x of T_Function {} -> True; _ -> False) $ getPath (parentMap params) child function <- find (\x -> case x of
T_Function {} -> True
T_Script {} -> True -- for sourced files
_ -> False) $
getPath (parentMap params) child
return $ getId parent == getId function return $ getId parent == getId function
referenceList :: [(String, Bool, Token)] referenceList :: [(String, Bool, Token)]
@@ -2245,12 +2195,12 @@ checkUnpassedInFunctions params root =
checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ()) checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())
checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do
str <- getLiteralString cmd str <- getLiteralString cmd
unless (Map.member str functionMap) $ fail "irrelevant" guard $ Map.member str functionMap
return $ tell [(str, null args, t)] return $ tell [(str, null args, t)]
checkCommand _ = Nothing checkCommand _ = Nothing
isPositional str = str == "*" || str == "@" isPositional str = str == "*" || str == "@"
|| (all isDigit str && str /= "0") || (all isDigit str && str /= "0" && str /= "")
isArgumentless (_, b, _) = b isArgumentless (_, b, _) = b
referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList
@@ -2301,8 +2251,8 @@ checkTildeInPath _ (T_SimpleCommand _ vars _) =
checkVar _ = return () checkVar _ = return ()
hasTilde t = fromMaybe False (liftM2 elem (return '~') (getLiteralStringExt (const $ return "") t)) hasTilde t = fromMaybe False (liftM2 elem (return '~') (getLiteralStringExt (const $ return "") t))
isQuoted (T_DoubleQuoted {}) = True isQuoted T_DoubleQuoted {} = True
isQuoted (T_SingleQuoted {}) = True isQuoted T_SingleQuoted {} = True
isQuoted _ = False isQuoted _ = False
checkTildeInPath _ _ = return () checkTildeInPath _ _ = return ()
@@ -2323,7 +2273,7 @@ shellSupport t =
case t of case t of
T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list) T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list)
T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Ksh]) T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Ksh])
otherwise -> ("", []) _ -> ("", [])
where where
forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash]) forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash])
forCase seps | CaseFallThrough `elem` seps = ("cases with ;&", [Bash, Ksh]) forCase seps | CaseFallThrough `elem` seps = ("cases with ;&", [Bash, Ksh])
@@ -2339,7 +2289,7 @@ checkMultipleAppends params t =
mapM_ checkList $ getCommandSequences t mapM_ checkList $ getCommandSequences t
where where
checkList list = checkList list =
mapM_ checkGroup (groupWith (liftM fst) $ map getTarget list) mapM_ checkGroup (groupWith (fmap fst) $ map getTarget list)
checkGroup (f:_:_:_) | isJust f = checkGroup (f:_:_:_) | isJust f =
style (snd $ fromJust f) 2129 style (snd $ fromJust f) 2129
"Consider using { cmd1; cmd2; } >> file instead of individual redirects." "Consider using { cmd1; cmd2; } >> file instead of individual redirects."
@@ -2350,7 +2300,7 @@ checkMultipleAppends params t =
file <- mapMaybe getAppend list !!! 0 file <- mapMaybe getAppend list !!! 0
return (file, id) return (file, id)
getTarget _ = Nothing getTarget _ = Nothing
getAppend (T_FdRedirect _ _ (T_IoFile _ (T_DGREAT {}) f)) = return f getAppend (T_FdRedirect _ _ (T_IoFile _ T_DGREAT {} f)) = return f
getAppend _ = Nothing getAppend _ = Nothing
@@ -2425,12 +2375,19 @@ prop_checkTestArgumentSplitting12 = verify checkTestArgumentSplitting "[ *.png ]
prop_checkTestArgumentSplitting13 = verify checkTestArgumentSplitting "[ \"$@\" == \"\" ]" prop_checkTestArgumentSplitting13 = verify checkTestArgumentSplitting "[ \"$@\" == \"\" ]"
prop_checkTestArgumentSplitting14 = verify checkTestArgumentSplitting "[[ \"$@\" == \"\" ]]" prop_checkTestArgumentSplitting14 = verify checkTestArgumentSplitting "[[ \"$@\" == \"\" ]]"
prop_checkTestArgumentSplitting15 = verifyNot checkTestArgumentSplitting "[[ \"$*\" == \"\" ]]" prop_checkTestArgumentSplitting15 = verifyNot checkTestArgumentSplitting "[[ \"$*\" == \"\" ]]"
prop_checkTestArgumentSplitting16 = verifyNot checkTestArgumentSplitting "[[ -v foo[123] ]]"
checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] () checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] ()
checkTestArgumentSplitting _ t = checkTestArgumentSplitting _ t =
case t of case t of
(TC_Unary _ _ op token) | isGlob token -> (TC_Unary _ typ op token) | isGlob token ->
err (getId token) 2144 $ if op == "-v"
op ++ " doesn't work with globs. Use a for loop." then
when (typ == SingleBracket) $
err (getId token) 2208 $
"Use [[ ]] or quote arguments to -v to avoid glob expansion."
else
err (getId token) 2144 $
op ++ " doesn't work with globs. Use a for loop."
(TC_Nullary _ typ token) -> do (TC_Nullary _ typ token) -> do
checkBraces typ token checkBraces typ token
@@ -2497,38 +2454,53 @@ checkMaskedReturns _ _ = return ()
prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo" prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo" prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
checkReadWithoutR _ t@(T_SimpleCommand {}) | t `isUnqualifiedCommand` "read" = checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
unless ("r" `elem` map snd (getAllFlags t)) $ unless ("r" `elem` map snd (getAllFlags t)) $
info (getId t) 2162 "read without -r will mangle backslashes." info (getId t) 2162 "read without -r will mangle backslashes."
checkReadWithoutR _ _ = return () checkReadWithoutR _ _ = return ()
prop_checkUncheckedCd1 = verifyTree checkUncheckedCd "cd ~/src; rm -r foo" prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
prop_checkUncheckedCd2 = verifyNotTree checkUncheckedCd "cd ~/src || exit; rm -r foo" prop_checkUncheckedCd2 = verifyNotTree checkUncheckedCdPushdPopd "cd ~/src || exit; rm -r foo"
prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCd "set -e; cd ~/src; rm -r foo" prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; cd ~/src; rm -r foo"
prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi" prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCdPushdPopd "if cd foo; then rm foo; fi"
prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi" prop_checkUncheckedCd5 = verifyTree checkUncheckedCdPushdPopd "if true; then cd foo; fi"
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .." prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar" prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar" prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar"
checkUncheckedCd params root = prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"
if hasSetE then [] else execWriter $ doAnalysis checkElement root prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo"
prop_checkUncheckedPushd4 = verifyNotTree checkUncheckedCdPushdPopd "if pushd foo; then rm foo; fi"
prop_checkUncheckedPushd5 = verifyTree checkUncheckedCdPushdPopd "if true; then pushd foo; fi"
prop_checkUncheckedPushd6 = verifyNotTree checkUncheckedCdPushdPopd "pushd .."
prop_checkUncheckedPushd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npushd foo\nrm bar"
prop_checkUncheckedPushd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; pushd foo; rm bar"
prop_checkUncheckedPushd9 = verifyNotTree checkUncheckedCdPushdPopd "pushd -n foo"
prop_checkUncheckedPopd1 = verifyTree checkUncheckedCdPushdPopd "popd; rm -r foo"
prop_checkUncheckedPopd2 = verifyNotTree checkUncheckedCdPushdPopd "popd || exit; rm -r foo"
prop_checkUncheckedPopd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; popd; rm -r foo"
prop_checkUncheckedPopd4 = verifyNotTree checkUncheckedCdPushdPopd "if popd; then rm foo; fi"
prop_checkUncheckedPopd5 = verifyTree checkUncheckedCdPushdPopd "if true; then popd; fi"
prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
checkUncheckedCdPushdPopd params root =
if hasSetE params then
[]
else execWriter $ doAnalysis checkElement root
where where
checkElement t@(T_SimpleCommand {}) = checkElement t@T_SimpleCommand {} =
when(t `isUnqualifiedCommand` "cd" when(name t `elem` ["cd", "pushd", "popd"]
&& not (isCdDotDot t) && not (isSafeDir t)
&& not (name t == "pushd" && ("n" `elem` map snd (getAllFlags t)))
&& not (isCondition $ getPath (parentMap params) t)) $ && not (isCondition $ getPath (parentMap params) t)) $
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails." warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
checkElement _ = return () checkElement _ = return ()
isCdDotDot t = oversimplify t == ["cd", ".."] name t = fromMaybe "" $ getCommandName t
hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root isSafeDir t = case oversimplify t of
isSetE t = [_, ".."] -> True;
case t of _ -> False
T_Script _ str _ -> str `matches` re
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t || "e" `elem` map snd (getAllFlags t))
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done" prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done"
prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done" prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"
@@ -2565,7 +2537,7 @@ prop_checkTrailingBracket5 = verifyNot checkTrailingBracket "run bar ']'"
checkTrailingBracket _ token = checkTrailingBracket _ token =
case token of case token of
T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token
otherwise -> return () _ -> return ()
where where
check t command = check t command =
case t of case t of
@@ -2576,7 +2548,7 @@ checkTrailingBracket _ token =
guard $ opposite `notElem` parameters guard $ opposite `notElem` parameters
return $ warn id 2171 $ return $ warn id 2171 $
"Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?" "Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?"
otherwise -> return () _ -> return ()
invert s = invert s =
case s of case s of
"]]" -> "[[" "]]" -> "[["
@@ -2600,7 +2572,7 @@ checkReturnAgainstZero _ token =
when (isExitCode exp) $ message (getId exp) when (isExitCode exp) $ message (getId exp)
TA_Sequence _ [exp] -> TA_Sequence _ [exp] ->
when (isExitCode exp) $ message (getId exp) when (isExitCode exp) $ message (getId exp)
otherwise -> return () _ -> return ()
where where
check lhs rhs = check lhs rhs =
if isZero rhs && isExitCode lhs if isZero rhs && isExitCode lhs
@@ -2609,8 +2581,8 @@ checkReturnAgainstZero _ token =
isZero t = getLiteralString t == Just "0" isZero t = getLiteralString t == Just "0"
isExitCode t = isExitCode t =
case getWordParts t of case getWordParts t of
[exp@(T_DollarBraced {})] -> bracedString exp == "?" [exp@T_DollarBraced {}] -> bracedString exp == "?"
otherwise -> False _ -> False
message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?." message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?."
prop_checkRedirectedNowhere1 = verify checkRedirectedNowhere "> file" prop_checkRedirectedNowhere1 = verify checkRedirectedNowhere "> file"
@@ -2682,7 +2654,7 @@ checkArrayAssignmentIndices params root =
guard $ '=' `elem` str guard $ '=' `elem` str
return $ warn id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it." return $ warn id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it."
in in
if (null literalEquals && isAssociative) if null literalEquals && isAssociative
then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ." then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ."
else sequence_ literalEquals else sequence_ literalEquals
@@ -2692,23 +2664,51 @@ prop_checkUnmatchableCases1 = verify checkUnmatchableCases "case foo in bar) tru
prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac" prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac"
prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac" prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac"
prop_checkUnmatchableCases4 = verifyNot checkUnmatchableCases "case foo-$bar in foo*|*bar|*baz*) true; esac" prop_checkUnmatchableCases4 = verifyNot checkUnmatchableCases "case foo-$bar in foo*|*bar|*baz*) true; esac"
prop_checkUnmatchableCases5 = verify checkUnmatchableCases "case $f in *.txt) true;; f??.txt) false;; esac"
prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"
prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac"
prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac"
checkUnmatchableCases _ t = checkUnmatchableCases _ t =
case t of case t of
T_CaseExpression _ word list -> T_CaseExpression _ word list -> do
let patterns = concatMap snd3 list
if isConstant word if isConstant word
then warn (getId word) 2194 then warn (getId word) 2194
"This word is constant. Did you forget the $ on a variable?" "This word is constant. Did you forget the $ on a variable?"
else potentially $ do else potentially $ do
pg <- wordToPseudoGlob word pg <- wordToPseudoGlob word
return $ mapM_ (check pg) (concatMap (\(_,x,_) -> x) list) return $ mapM_ (check pg) patterns
let exactGlobs = tupMap wordToExactPseudoGlob patterns
let fuzzyGlobs = tupMap wordToPseudoGlob patterns
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
mapM_ checkDoms dominators
_ -> return () _ -> return ()
where where
snd3 (_,x,_) = x
check target candidate = potentially $ do check target candidate = potentially $ do
candidateGlob <- wordToPseudoGlob candidate candidateGlob <- wordToPseudoGlob candidate
guard . not $ pseudoGlobsCanOverlap target candidateGlob guard . not $ pseudoGlobsCanOverlap target candidateGlob
return $ warn (getId candidate) 2195 return $ warn (getId candidate) 2195
"This pattern will never match the case statement's word. Double check them." "This pattern will never match the case statement's word. Double check them."
tupMap f l = zip l (map f l)
checkDoms ((glob, Just x), rest) =
case filter (\(_, p) -> x `pseudoGlobIsSuperSetof` p) valids of
((first,_):_) -> do
warn (getId glob) 2221 "This pattern always overrides a later one."
warn (getId first) 2222 "This pattern never matches because of a previous pattern."
_ -> return ()
where
valids = concatMap f rest
f (x, Just y) = [(x,y)]
f _ = []
checkDoms _ = return ()
prop_checkSubshellAsTest1 = verify checkSubshellAsTest "( -e file )" prop_checkSubshellAsTest1 = verify checkSubshellAsTest "( -e file )"
prop_checkSubshellAsTest2 = verify checkSubshellAsTest "( 1 -gt 2 )" prop_checkSubshellAsTest2 = verify checkSubshellAsTest "( 1 -gt 2 )"
prop_checkSubshellAsTest3 = verifyNot checkSubshellAsTest "( grep -c foo bar )" prop_checkSubshellAsTest3 = verifyNot checkSubshellAsTest "( grep -c foo bar )"
@@ -2737,5 +2737,159 @@ checkSubshellAsTest _ t =
warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?" warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?"
prop_checkSplittingInArrays1 = verify checkSplittingInArrays "a=( $var )"
prop_checkSplittingInArrays2 = verify checkSplittingInArrays "a=( $(cmd) )"
prop_checkSplittingInArrays3 = verifyNot checkSplittingInArrays "a=( \"$var\" )"
prop_checkSplittingInArrays4 = verifyNot checkSplittingInArrays "a=( \"$(cmd)\" )"
prop_checkSplittingInArrays5 = verifyNot checkSplittingInArrays "a=( $! $$ $# )"
prop_checkSplittingInArrays6 = verifyNot checkSplittingInArrays "a=( ${#arr[@]} )"
prop_checkSplittingInArrays7 = verifyNot checkSplittingInArrays "a=( foo{1,2} )"
prop_checkSplittingInArrays8 = verifyNot checkSplittingInArrays "a=( * )"
checkSplittingInArrays params t =
case t of
T_Array _ elements -> mapM_ check elements
_ -> return ()
where
check word = case word of
T_NormalWord _ parts -> mapM_ checkPart parts
_ -> return ()
checkPart part = case part of
T_DollarExpansion id _ -> forCommand id
T_DollarBraceCommandExpansion id _ -> forCommand id
T_Backticked id _ -> forCommand id
T_DollarBraced id str |
not (isCountingReference part)
&& not (isQuotedAlternativeReference part)
&& not (getBracedReference (bracedString part) `elem` variablesWithoutSpaces)
-> warn id 2206 $
if shellType params == Ksh
then "Quote to prevent word splitting, or split robustly with read -A or while read."
else "Quote to prevent word splitting, or split robustly with mapfile or read -a."
_ -> return ()
forCommand id =
warn id 2207 $
if shellType params == Ksh
then "Prefer read -A or while read to split command output (or quote to avoid splitting)."
else "Prefer mapfile or read -a to split command output (or quote to avoid splitting)."
prop_checkRedirectionToNumber1 = verify checkRedirectionToNumber "( 1 > 2 )"
prop_checkRedirectionToNumber2 = verify checkRedirectionToNumber "foo 1>2"
prop_checkRedirectionToNumber3 = verifyNot checkRedirectionToNumber "echo foo > '2'"
prop_checkRedirectionToNumber4 = verifyNot checkRedirectionToNumber "foo 1>&2"
checkRedirectionToNumber _ t = case t of
T_IoFile id _ word -> potentially $ do
file <- getUnquotedLiteral word
guard $ all isDigit file
return $ warn id 2210 "This is a file redirection. Was it supposed to be a comparison or fd operation?"
_ -> return ()
prop_checkGlobAsCommand1 = verify checkGlobAsCommand "foo*"
prop_checkGlobAsCommand2 = verify checkGlobAsCommand "$(var[i])"
prop_checkGlobAsCommand3 = verifyNot checkGlobAsCommand "echo foo*"
checkGlobAsCommand _ t = case t of
T_SimpleCommand _ _ (first:_) ->
when (isGlob first) $
warn (getId first) 2211 "This is a glob used as a command name. Was it supposed to be in ${..}, array, or is it missing quoting?"
_ -> return ()
prop_checkFlagAsCommand1 = verify checkFlagAsCommand "-e file"
prop_checkFlagAsCommand2 = verify checkFlagAsCommand "foo\n --bar=baz"
prop_checkFlagAsCommand3 = verifyNot checkFlagAsCommand "'--myexec--' args"
prop_checkFlagAsCommand4 = verifyNot checkFlagAsCommand "var=cmd --arg" -- Handled by SC2037
checkFlagAsCommand _ t = case t of
T_SimpleCommand _ [] (first:_) ->
when (isUnquotedFlag first) $
warn (getId first) 2215 "This flag is used as a command name. Bad line break or missing [ .. ]?"
_ -> return ()
prop_checkEmptyCondition1 = verify checkEmptyCondition "if [ ]; then ..; fi"
prop_checkEmptyCondition2 = verifyNot checkEmptyCondition "[ foo -o bar ]"
checkEmptyCondition _ t = case t of
TC_Empty id _ -> style id 2212 "Use 'false' instead of empty [/[[ conditionals."
_ -> return ()
prop_checkPipeToNowhere1 = verify checkPipeToNowhere "foo | echo bar"
prop_checkPipeToNowhere2 = verify checkPipeToNowhere "basename < file.txt"
prop_checkPipeToNowhere3 = verify checkPipeToNowhere "printf 'Lol' <<< str"
prop_checkPipeToNowhere4 = verify checkPipeToNowhere "printf 'Lol' << eof\nlol\neof\n"
prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
checkPipeToNowhere _ t =
case t of
T_Pipeline _ _ (first:rest) -> mapM_ checkPipe rest
T_Redirecting _ redirects cmd -> when (any redirectsStdin redirects) $ checkRedir cmd
_ -> return ()
where
checkPipe redir = potentially $ do
cmd <- getCommand redir
name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd
return $ warn (getId cmd) 2216 $
"Piping to '" ++ name ++ "', a command that doesn't read stdin. Wrong command or missing xargs?"
checkRedir cmd = potentially $ do
name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd
return $ warn (getId cmd) 2217 $
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. Bad quoting or missing xargs?"
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
hasAdditionalConsumers t = fromMaybe True $ do
doAnalysis (guard . not . mayConsume) t
return False
mayConsume t =
case t of
T_ProcSub {} -> True
T_Backticked {} -> True
T_DollarExpansion {} -> True
_ -> False
redirectsStdin t =
case t of
T_FdRedirect _ _ (T_IoFile _ T_Less {} _) -> True
T_FdRedirect _ _ T_HereDoc {} -> True
T_FdRedirect _ _ T_HereString {} -> True
_ -> False
prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { true; }"
prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f"
prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi"
prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }"
checkUseBeforeDefinition _ t =
execWriter $ evalStateT (mapM_ examine $ revCommands) Map.empty
where
examine t = case t of
T_Pipeline _ _ [T_Redirecting _ _ (T_Function _ _ _ name _)] ->
modify $ Map.insert name t
T_Annotation _ _ w -> examine w
T_Pipeline _ _ cmds -> do
m <- get
unless (Map.null m) $
mapM_ (checkUsage m) $ concatMap recursiveSequences cmds
_ -> return ()
checkUsage map cmd = potentially $ do
name <- getCommandName cmd
def <- Map.lookup name map
return $
err (getId cmd) 2218
"This function is only defined later. Move the definition up."
revCommands = reverse $ concat $ getCommandSequences t
recursiveSequences x =
let list = concat $ getCommandSequences x in
if null list
then [x]
else concatMap recursiveSequences list
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -32,7 +32,7 @@ import qualified ShellCheck.Checks.ShellSupport
analyzeScript :: AnalysisSpec -> AnalysisResult analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = AnalysisResult { analyzeScript spec = AnalysisResult {
arComments = arComments =
filterByAnnotation (asScript spec) . nub $ filterByAnnotation spec params . nub $
runAnalytics spec runAnalytics spec
++ runChecker params (checkers params) ++ runChecker params (checkers params)
} }

View File

@@ -72,11 +72,13 @@ composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x composeAnalyzers f g x = f x >> g x
data Parameters = Parameters { data Parameters = Parameters {
variableFlow :: [StackData], hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
parentMap :: Map.Map Id Token, hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
shellType :: Shell, variableFlow :: [StackData], -- A linear (bad) analysis of data flow
shellTypeSpecified :: Bool, parentMap :: Map.Map Id Token, -- A map from Id to parent Token
rootNode :: Token shellType :: Shell, -- The shell type, such as Bash or Ksh
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
rootNode :: Token -- The root node of the AST
} }
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
@@ -94,7 +96,12 @@ data StackData =
data DataType = DataString DataSource | DataArray DataSource data DataType = DataString DataSource | DataArray DataSource
deriving (Show) deriving (Show)
data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger data DataSource =
SourceFrom [Token]
| SourceExternal
| SourceDeclaration
| SourceInteger
| SourceChecked
deriving (Show) deriving (Show)
data VariableState = Dead Token String | Alive deriving (Show) data VariableState = Dead Token String | Alive deriving (Show)
@@ -102,6 +109,7 @@ data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec { defaultSpec root = AnalysisSpec {
asScript = root, asScript = root,
asShellType = Nothing, asShellType = Nothing,
asCheckSourced = False,
asExecutionMode = Executed asExecutionMode = Executed
} }
@@ -109,7 +117,8 @@ pScript s =
let let
pSpec = ParseSpec { pSpec = ParseSpec {
psFilename = "script", psFilename = "script",
psScript = s psScript = s,
psCheckSourced = False
} }
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
@@ -137,13 +146,48 @@ makeParameters spec =
let params = Parameters { let params = Parameters {
rootNode = root, rootNode = root,
shellType = fromMaybe (determineShell root) $ asShellType spec, shellType = fromMaybe (determineShell root) $ asShellType spec,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
Bash -> containsLastpipe root
Dash -> False
Sh -> False
Ksh -> True,
shellTypeSpecified = isJust $ asShellType spec, shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = variableFlow = getVariableFlow params root
getVariableFlow (shellType params) (parentMap params) root
} in params } in params
where root = asScript spec where root = asScript spec
-- Does this script mention 'set -e' anywhere?
-- Used as a hack to disable certain warnings.
containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
where
isSetE t =
case t of
T_Script _ str _ -> str `matches` re
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t ||
"e" `elem` map snd (getAllFlags t))
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack.
containsLastpipe root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where
isShoptLastPipe t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" &&
("lastpipe" `elem` oversimplify t)
_ -> False
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
@@ -163,7 +207,7 @@ determineShell t = fromMaybe Bash $ do
(ShellOverride s) -> return s (ShellOverride s) -> return s
_ -> fail "" _ -> fail ""
getCandidates :: Token -> [Maybe String] getCandidates :: Token -> [Maybe String]
getCandidates t@(T_Script {}) = [Just $ fromShebang t] getCandidates t@T_Script {} = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) = getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++ map forAnnotation annotations ++
[Just $ fromShebang s] [Just $ fromShebang s]
@@ -179,8 +223,10 @@ executableFromShebang = shellFor
shellFor s = reverse . takeWhile (/= '/') . reverse $ s shellFor s = reverse . takeWhile (/= '/') . reverse $ s
--- Context seeking
-- Given a root node, make a map from Id to parent Token.
-- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token
getParentTree t = getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty) snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
where where
@@ -190,18 +236,24 @@ getParentTree t =
case rest of [] -> put (rest, map) case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map) (x:_) -> put (rest, Map.insert (getId t) x map)
-- Given a root node, make a map from Id to Token
getTokenMap :: Token -> Map.Map Id Token
getTokenMap t = getTokenMap t =
execState (doAnalysis f t) Map.empty execState (doAnalysis f t) Map.empty
where where
f t = modify (Map.insert (getId t) t) f t = modify (Map.insert (getId t) t)
-- Is this node self quoting for a regular element? -- Is this token in a quoting free context? (i.e. would variable expansion split)
isQuoteFree = isQuoteFreeNode False -- True: Assignments, [[ .. ]], here docs, already in double quotes
-- False: Regular words
-- Is this node striclty self quoting, for array expansions
isStrictlyQuoteFree = isQuoteFreeNode True isStrictlyQuoteFree = isQuoteFreeNode True
-- Like above, but also allow some cases where splitting may be desired.
-- True: Like above + for loops
-- False: Like above
isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t = isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) || (isQuoteFreeElement t == Just True) ||
@@ -234,6 +286,9 @@ isQuoteFreeNode strict tree t =
T_SelectIn {} -> return (not strict) T_SelectIn {} -> return (not strict)
_ -> Nothing _ -> Nothing
-- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
isParamTo tree cmd = isParamTo tree cmd =
go go
where where
@@ -249,16 +304,23 @@ isParamTo tree cmd =
T_Redirecting {} -> isCommand t cmd T_Redirecting {} -> isCommand t cmd
_ -> False _ -> False
-- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t = getClosestCommand tree t =
msum . map getCommand $ getPath tree t findFirst findCommand $ getPath tree t
where where
getCommand t@(T_Redirecting {}) = return t findCommand t =
getCommand _ = Nothing case t of
T_Redirecting {} -> return True
T_Script {} -> return False
_ -> Nothing
-- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do getClosestCommandM t = do
tree <- asks parentMap tree <- asks parentMap
return $ getClosestCommand tree t return $ getClosestCommand tree t
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where where
go currentId (T_NormalWord id [word]:rest) go currentId (T_NormalWord id [word]:rest)
@@ -269,7 +331,7 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
| currentId == getId word = True | currentId == getId word = True
go _ _ = False go _ _ = False
-- A list of the element and all its parents -- A list of the element and all its parents up to the root node.
getPath tree t = t : getPath tree t = t :
case Map.lookup (getId t) tree of case Map.lookup (getId t) tree of
Nothing -> [] Nothing -> []
@@ -290,6 +352,18 @@ pathTo t = do
parents <- reader parentMap parents <- reader parentMap
return $ getPath parents t return $ getPath parents t
-- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
findFirst p l =
case l of
[] -> Nothing
(x:xs) ->
case p x of
Just True -> return x
Just False -> Nothing
Nothing -> findFirst p xs
-- Check whether a word is entirely output from a single command -- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
@@ -302,29 +376,29 @@ tokenIsJustCommandOutput t = case t of
check _ = False check _ = False
-- TODO: Replace this with a proper Control Flow Graph -- TODO: Replace this with a proper Control Flow Graph
getVariableFlow shell parents t = getVariableFlow params t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) [] let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack in reverse stack
where where
startScope t = startScope t =
let scopeType = leadType shell parents t let scopeType = leadType params t
in do in do
when (scopeType /= NoneScope) $ modify (StackScope scopeType:) when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
when (assignFirst t) $ setWritten t when (assignFirst t) $ setWritten t
endScope t = endScope t =
let scopeType = leadType shell parents t let scopeType = leadType params t
in do in do
setRead t setRead t
unless (assignFirst t) $ setWritten t unless (assignFirst t) $ setWritten t
when (scopeType /= NoneScope) $ modify (StackScopeEnd:) when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
assignFirst (T_ForIn {}) = True assignFirst T_ForIn {} = True
assignFirst (T_SelectIn {}) = True assignFirst T_SelectIn {} = True
assignFirst _ = False assignFirst _ = False
setRead t = setRead t =
let read = getReferencedVariables parents t let read = getReferencedVariables (parentMap params) t
in mapM_ (\v -> modify (Reference v:)) read in mapM_ (\v -> modify (Reference v:)) read
setWritten t = setWritten t =
@@ -332,7 +406,7 @@ getVariableFlow shell parents t =
in mapM_ (\v -> modify (Assignment v:)) written in mapM_ (\v -> modify (Assignment v:)) written
leadType shell parents t = leadType params t =
case t of case t of
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion" T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
T_Backticked _ _ -> SubshellScope "`..` expansion" T_Backticked _ _ -> SubshellScope "`..` expansion"
@@ -346,7 +420,7 @@ leadType shell parents t =
_ -> NoneScope _ -> NoneScope
where where
parentPipeline = do parentPipeline = do
parent <- Map.lookup (getId t) parents parent <- Map.lookup (getId t) (parentMap params)
case parent of case parent of
T_Pipeline {} -> return parent T_Pipeline {} -> return parent
_ -> Nothing _ -> Nothing
@@ -355,17 +429,10 @@ leadType shell parents t =
(T_Pipeline _ _ list) <- parentPipeline (T_Pipeline _ _ list) <- parentPipeline
if length list <= 1 if length list <= 1
then return False then return False
else if lastCreatesSubshell else if not $ hasLastpipe params
then return True then return True
else return . not $ (getId . head $ reverse list) == getId t else return . not $ (getId . head $ reverse list) == getId t
lastCreatesSubshell =
case shell of
Bash -> True
Dash -> True
Sh -> True
Ksh -> False
getModifiedVariables t = getModifiedVariables t =
case t of case t of
T_SimpleCommand _ vars [] -> T_SimpleCommand _ vars [] ->
@@ -374,7 +441,7 @@ getModifiedVariables t =
[(x, x, name, dataTypeFrom DataString w)] [(x, x, name, dataTypeFrom DataString w)]
_ -> [] _ -> []
) vars ) vars
c@(T_SimpleCommand {}) -> c@T_SimpleCommand {} ->
getModifiedVariableCommand c getModifiedVariableCommand c
TA_Unary _ "++|" var -> maybeToList $ do TA_Unary _ "++|" var -> maybeToList $ do
@@ -388,6 +455,18 @@ getModifiedVariables t =
name <- getLiteralString lhs name <- getLiteralString lhs
return (t, t, name, DataString $ SourceFrom [rhs]) return (t, t, name, DataString $ SourceFrom [rhs])
-- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> Nothing
guard . not . null $ str
return (t, token, str, DataString $ SourceChecked)
T_DollarBraced _ l -> maybeToList $ do T_DollarBraced _ l -> maybeToList $ do
let string = bracedString t let string = bracedString t
let modifier = getBracedModifier string let modifier = getBracedModifier string
@@ -401,15 +480,15 @@ getModifiedVariables t =
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable --Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString $ SourceExternal)] T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> [] _ -> []
isClosingFileOp op = isClosingFileOp op =
case op of case op of
T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False _ -> False
@@ -496,7 +575,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getModifierParam def t@(T_Assignment _ _ name _ value) = getModifierParam def t@(T_Assignment _ _ name _ value) =
[(base, t, name, dataTypeFrom def value)] [(base, t, name, dataTypeFrom def value)]
getModifierParam def t@(T_NormalWord {}) = maybeToList $ do getModifierParam def t@T_NormalWord {} = maybeToList $ do
name <- getLiteralString t name <- getLiteralString t
guard $ isVariableName name guard $ isVariableName name
return (base, t, name, def SourceDeclaration) return (base, t, name, def SourceDeclaration)
@@ -546,7 +625,7 @@ getOffsetReferences mods = fromMaybe [] $ do
offsets <- match !!! 0 offsets <- match !!! 0
return $ matchAllStrings variableNameRegex offsets return $ matchAllStrings variableNameRegex offsets
where where
re = mkRegex "^ *:(.*)" re = mkRegex "^ *:([^-=?+].*)"
getReferencedVariables parents t = getReferencedVariables parents t =
case t of case t of
@@ -584,8 +663,10 @@ getReferencedVariables parents t =
getVariablesFromLiteralToken word getVariablesFromLiteralToken word
else [] else []
literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x literalizer t = case t of
literalizer _ = Nothing TA_Index {} -> return "" -- x[0] becomes a reference of x
T_Glob _ s -> return s -- Also when parsed as globs
_ -> Nothing
getIfReference context token = maybeToList $ do getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token str <- getLiteralStringExt literalizer token
@@ -604,13 +685,20 @@ dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultTyp
--- Command specific checks --- Command specific checks
-- Compare a command to a string: t `isCommand` "sed" (also matches /usr/bin/sed)
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd) isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
-- Compare a command to a literal. Like above, but checks full path.
isUnqualifiedCommand token str = isCommandMatch token (== str) isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ do isCommandMatch token matcher = fromMaybe False $ do
cmd <- getCommandName token cmd <- getCommandName token
return $ matcher cmd return $ matcher cmd
-- Does this regex look like it was intended as a glob?
-- True: *foo*
-- False: .*foo.*
isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True isConfusedGlobRegex [x,'*'] | x /= '\\' = True
isConfusedGlobRegex _ = False isConfusedGlobRegex _ = False
@@ -637,6 +725,7 @@ getVariablesFromLiteral string =
where where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo" prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo" prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#" prop_getBracedReference3 = getBracedReference "#" == "#"
@@ -687,13 +776,22 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest] dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x] dropModifier x = [x]
-- Useful generic functions -- Useful generic functions.
-- Run an action in a Maybe (or do nothing).
-- Example:
-- potentially $ do
-- s <- getLiteralString cmd
-- guard $ s `elem` ["--recursive", "-r"]
-- return $ warn .. "Something something recursive"
potentially :: Monad m => Maybe (m ()) -> m () potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ()) potentially = fromMaybe (return ())
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a headOrDefault _ (a:_) = a
headOrDefault def _ = def headOrDefault def _ = def
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i = (!!!) list i =
case drop i list of case drop i list of
[] -> Nothing [] -> Nothing
@@ -705,9 +803,10 @@ whenShell l c = do
when (shell `elem` l ) c when (shell `elem` l ) c
filterByAnnotation token = filterByAnnotation asSpec params =
filter (not . shouldIgnore) filter (not . shouldIgnore)
where where
token = asScript asSpec
idFor (TokenComment id _) = id idFor (TokenComment id _) = id
shouldIgnore note = shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $ any (shouldIgnoreFor (getCode note)) $
@@ -717,11 +816,28 @@ filterByAnnotation token =
where where
hasNum (DisableComment ts) = num == ts hasNum (DisableComment ts) = num == ts
hasNum _ = False hasNum _ = False
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor _ _ = False shouldIgnoreFor _ _ = False
parents = getParentTree token parents = parentMap params
getCode (TokenComment _ (Comment _ c _)) = c getCode (TokenComment _ (Comment _ c _)) = c
-- Is this a ${#anything}, to get string length or array count?
isCountingReference (T_DollarBraced id token) =
case concat $ oversimplify token of
'#':_ -> True
_ -> False
isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t =
case t of
T_DollarBraced _ _ ->
getBracedModifier (bracedString t) `matches` re
_ -> False
where
re = mkRegex "(^|\\]):?\\+"
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -54,7 +54,8 @@ checkScript sys spec = do
checkScript contents = do checkScript contents = do
result <- parseScript sys ParseSpec { result <- parseScript sys ParseSpec {
psFilename = csFilename spec, psFilename = csFilename spec,
psScript = contents psScript = contents,
psCheckSourced = csCheckSourced spec
} }
let parseMessages = prComments result let parseMessages = prComments result
let analysisMessages = let analysisMessages =
@@ -77,6 +78,7 @@ checkScript sys spec = do
AnalysisSpec { AnalysisSpec {
asScript = root, asScript = root,
asShellType = csShellTypeOverride spec, asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed asExecutionMode = Executed
} }
@@ -88,13 +90,21 @@ getErrors sys spec =
check = checkWithIncludes [] check = checkWithIncludes []
checkWithSpec includes =
getErrors (mockedSystemInterface includes)
checkWithIncludes includes src = checkWithIncludes includes src =
getErrors checkWithSpec includes emptyCheckSpec {
(mockedSystemInterface includes) csScript = src,
emptyCheckSpec { csExcludedWarnings = [2148]
csScript = src, }
csExcludedWarnings = [2148]
} checkRecursive includes src =
checkWithSpec includes emptyCheckSpec {
csScript = src,
csExcludedWarnings = [2148],
csCheckSourced = True
}
prop_findsParseIssue = check "echo \"$12\"" == [1037] prop_findsParseIssue = check "echo \"$12\"" == [1037]
@@ -153,6 +163,12 @@ prop_cantSourceDynamic2 =
prop_canSourceDynamicWhenRedirected = prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
prop_recursiveParsing =
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
prop_sourceDirectiveDoesntFollowFile = prop_sourceDirectiveDoesntFollowFile =
null $ checkWithIncludes null $ checkWithIncludes
[("foo", "source bar"), ("bar", "baz=3")] [("foo", "source bar"), ("bar", "baz=3")]

View File

@@ -38,7 +38,7 @@ import Control.Monad.RWS
import Data.Char import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
@@ -85,13 +85,16 @@ commandChecks = [
,checkDeprecatedTempfile ,checkDeprecatedTempfile
,checkDeprecatedEgrep ,checkDeprecatedEgrep
,checkDeprecatedFgrep ,checkDeprecatedFgrep
,checkWhileGetoptsCase
,checkCatastrophicRm
,checkLetUsage
] ]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty buildCommandMap = foldl' addCheck Map.empty
where where
addCheck map (CommandCheck name function) = addCheck map (CommandCheck name function) =
Map.insertWith' composeAnalyzers name function map Map.insertWith composeAnalyzers name function map
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
@@ -195,6 +198,9 @@ prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file" prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo" prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file" prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
checkGrepRe = CommandCheck (Basename "grep") check where checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd) check cmd = f cmd (arguments cmd)
@@ -202,8 +208,18 @@ checkGrepRe = CommandCheck (Basename "grep") check where
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
skippable _ = False skippable _ = False
f _ [] = return () f _ [] = return ()
f cmd (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f cmd r f cmd (x:r) =
f cmd (re:_) = do let str = getLiteralStringExt (const $ return "_") x
in
if str `elem` [Just "--", Just "-e", Just "--regex"]
then checkRE cmd r -- Regex is *after* this
else
if skippable str
then f cmd r -- Regex is elsewhere
else checkRE cmd (x:r) -- Regex is this
checkRE _ [] = return ()
checkRE cmd (re:_) = do
when (isGlob re) $ when (isGlob re) $
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
@@ -385,17 +401,26 @@ prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b" prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b" prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel" prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"
prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
checkMkdirDashPM = CommandCheck (Basename "mkdir") check checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where where
check t = potentially $ do check t = potentially $ do
let flags = getAllFlags t let flags = getAllFlags t
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not. -- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
guard $ any couldHaveSubdirs (drop 1 $ arguments t)
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory." return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
couldHaveSubdirs t = fromMaybe True $ do couldHaveSubdirs t = fromMaybe True $ do
name <- getLiteralString t name <- getLiteralString t
return $ '/' `elem` name return $ '/' `elem` name && not (name `matches` re)
re = mkRegex "^(\\.\\.?\\/)+[^/]+$"
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8" prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
@@ -404,9 +429,13 @@ prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL" prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9" prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop" prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' int"
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where where
f = mapM_ check f args = case args of
first:rest -> unless (isFlag first) $ mapM_ check rest
_ -> return ()
check param = potentially $ do check param = potentially $ do
str <- getLiteralString param str <- getLiteralString param
let id = getId param let id = getId param
@@ -605,6 +634,8 @@ prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f"
prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find" prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find"
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f" prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print" prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
checkFindWithoutPath = CommandCheck (Basename "find") f checkFindWithoutPath = CommandCheck (Basename "find") f
where where
f (T_SimpleCommand _ _ (cmd:args)) = f (T_SimpleCommand _ _ (cmd:args)) =
@@ -613,11 +644,12 @@ checkFindWithoutPath = CommandCheck (Basename "find") f
-- This is a bit of a kludge. find supports flag arguments both before and after the path, -- This is a bit of a kludge. find supports flag arguments both before and after the path,
-- as well as multiple non-flag arguments that are not the path. We assume that all the -- as well as multiple non-flag arguments that are not the path. We assume that all the
-- pre-path flags are single characters, which is generally the case. -- pre-path flags are single characters, which is generally the case except for -O3.
hasPath (first:rest) = hasPath (first:rest) =
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
not ("-" `isPrefixOf` flag) || length flag <= 2 && hasPath rest not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
hasPath [] = False hasPath [] = False
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10" prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
@@ -681,5 +713,142 @@ prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $ checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead." \t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
where
f :: Token -> Analysis
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
path <- getPathM t
potentially $ do
options <- getLiteralString arg1
(T_WhileExpression _ _ body) <- findFirst whileLoop path
caseCmd <- mapMaybe findCase body !!! 0
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
f _ = return ()
check :: Id -> [String] -> Token -> Analysis
check optId opts (T_CaseExpression id _ list) = do
unless (Nothing `Map.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $
warn id 2220 "Invalid flags are not handled. Add a *) case."
mapM_ warnRedundant $ Map.toList notRequested
where
handledMap = Map.fromList (concatMap getHandledStrings list)
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
notHandled = Map.difference requestedMap handledMap
notRequested = Map.difference handledMap requestedMap
warnUnhandled optId caseId str =
warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."
warnRedundant (key, expr) = potentially $ do
str <- key
guard $ str `notElem` ["*", ":", "?"]
return $ warn (getId expr) 2214 "This case is not specified by getopts."
getHandledStrings (_, globs, _) =
map (\x -> (literal x, x)) globs
literal :: Token -> Maybe String
literal t = do
getLiteralString t <> fromGlob t
fromGlob t =
case t of
T_Glob _ ('[':c:']':[]) -> return [c]
T_Glob _ "*" -> return "*"
T_Glob _ "?" -> return "?"
_ -> Nothing
whileLoop t =
case t of
T_WhileExpression {} -> return True
T_Script {} -> return False
_ -> Nothing
findCase t =
case t of
T_Annotation _ _ x -> findCase x
T_Pipeline _ _ [x] -> findCase x
T_Redirecting _ _ x@(T_CaseExpression {}) -> return x
_ -> Nothing
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
when (isRecursive t) $
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
where
isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags
checkWord token =
case getLiteralString token of
Just str ->
when (fixPath str `elem` importantPaths) $
warn (getId token) 2114 "Warning: deletes a system directory."
Nothing ->
checkWord' token
checkWord' token = fromMaybe (return ()) $ do
filename <- getPotentialPath token
let path = fixPath filename
return . when (path `elem` importantPaths) $
warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."
fixPath filename =
let normalized = skipRepeating '/' . skipRepeating '*' $ filename in
if normalized == "/" then normalized else stripTrailing '/' normalized
getPotentialPath = getLiteralStringExt f
where
f (T_Glob _ str) = return str
f (T_DollarBraced _ word) =
let var = onlyLiteralString word in
-- This shouldn't handle non-colon cases.
if any (`isInfixOf` var) [":?", ":-", ":="]
then Nothing
else return ""
f _ = return ""
stripTrailing c = reverse . dropWhile (== c) . reverse
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
skipRepeating c (a:r) = a:skipRepeating c r
skipRepeating _ [] = []
paths = [
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
"/var", "/lib", "/dev", "/media", "/boot", "/lib64", "/usr/bin"
]
importantPaths = filter (not . null) $
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
prop_checkLetUsage1 = verify checkLetUsage "let a=1"
prop_checkLetUsage2 = verifyNot checkLetUsage "(( a=1 ))"
checkLetUsage = CommandCheck (Exactly "let") f
where
f t = whenShell [Bash,Ksh] $ do
style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -58,6 +58,7 @@ checks = [
,checkEchoSed ,checkEchoSed
,checkBraceExpansionVars ,checkBraceExpansionVars
,checkMultiDimensionalArrays ,checkMultiDimensionalArrays
,checkPS1Assignments
] ]
testChecker (ForShell _ t) = testChecker (ForShell _ t) =
@@ -160,10 +161,15 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is" bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are" bashism (T_HereString id _) = warnMsg id "here-strings are"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef", "\\<", "\\>"] = | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef" ] =
unless isDash $ warnMsg id $ op ++ " is" unless isDash $ warnMsg id $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) = bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is" warnMsg id "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is"
bashism (TC_Unary id _ "-a" _) = bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is" warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
@@ -383,6 +389,32 @@ checkMultiDimensionalArrays = ForShell [Bash] f
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re isMultiDim t = getBracedModifier (bracedString t) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
checkPS1Assignments = ForShell [Bash] f
where
f token = case token of
(T_Assignment _ _ "PS1" _ word) -> warnFor word
_ -> return ()
warnFor word =
let contents = concat $ oversimplify word in
when (containsUnescaped contents) $
info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues"
containsUnescaped s =
let unenclosed = subRegex enclosedRegex s "" in
isJust $ matchRegex escapeRegex unenclosed
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -77,6 +77,14 @@ commonCommands = [
"zcat" "zcat"
] ]
nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "fg", "fuser", "getconf", "getopt",
"getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "locale", "mv",
"nice", "printf", "ps", "pwd", "renice", "rm", "rmdir", "set", "sleep",
"touch", "trap", "ulimit", "unalias", "uname"
]
sampleWords = [ sampleWords = [
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
"golf", "hotel", "india", "juliett", "kilo", "lima", "mike", "golf", "hotel", "india", "juliett", "kilo", "lima", "mike",

View File

@@ -34,14 +34,27 @@ format = return Formatter {
putStrLn "<checkstyle version='4.3'>", putStrLn "<checkstyle version='4.3'>",
onFailure = outputError, onFailure = outputError,
onResult = outputResult, onResult = outputResults,
footer = putStrLn "</checkstyle>" footer = putStrLn "</checkstyle>"
} }
outputResult result contents = do outputResults cr sys =
let comments = makeNonVirtual (crComments result) contents if null comments
putStrLn . formatFile (crFilename result) $ comments then outputFile (crFilename cr) "" []
else mapM_ outputGroup fileGroups
where
comments = crComments cr
fileGroups = groupWith sourceFile comments
outputGroup group = do
let filename = sourceFile (head group)
result <- (siReadFile sys) filename
let contents = either (const "") id result
outputFile filename contents group
outputFile filename contents warnings = do
let comments = makeNonVirtual warnings contents
putStrLn . formatFile filename $ comments
formatFile name comments = concat [ formatFile name comments = concat [
"<file ", attr "name" name, ">\n", "<file ", attr "name" name, ">\n",

View File

@@ -25,11 +25,12 @@ import ShellCheck.Interface
-- A formatter that carries along an arbitrary piece of data -- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter { data Formatter = Formatter {
header :: IO (), header :: IO (),
onResult :: CheckResult -> String -> IO (), onResult :: CheckResult -> SystemInterface IO -> IO (),
onFailure :: FilePath -> ErrorMessage -> IO (), onFailure :: FilePath -> ErrorMessage -> IO (),
footer :: IO () footer :: IO ()
} }
sourceFile (PositionedComment pos _ _) = posFile pos
lineNo (PositionedComment pos _ _) = posLine pos lineNo (PositionedComment pos _ _) = posLine pos
endLineNo (PositionedComment _ end _) = posLine end endLineNo (PositionedComment _ end _) = posLine end
colNo (PositionedComment pos _ _) = posColumn pos colNo (PositionedComment pos _ _) = posColumn pos

View File

@@ -31,14 +31,25 @@ format = return Formatter {
header = return (), header = return (),
footer = return (), footer = return (),
onFailure = outputError, onFailure = outputError,
onResult = outputResult onResult = outputAll
} }
outputError file error = hPutStrLn stderr $ file ++ ": " ++ error outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
outputResult result contents = do outputAll cr sys = mapM_ f groups
let comments = makeNonVirtual (crComments result) contents where
mapM_ (putStrLn . formatComment (crFilename result)) comments comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = do
let filename = sourceFile (head group)
result <- (siReadFile sys) filename
let contents = either (const "") id result
outputResult filename contents group
outputResult filename contents warnings = do
let comments = makeNonVirtual warnings contents
mapM_ (putStrLn . formatComment filename) comments
formatComment filename c = concat [ formatComment filename c = concat [
filename, ":", filename, ":",

View File

@@ -43,15 +43,22 @@ colorForLevel level =
"style" -> 32 -- green "style" -> 32 -- green
"message" -> 1 -- bold "message" -> 1 -- bold
"source" -> 0 -- none "source" -> 0 -- none
otherwise -> 0 -- none _ -> 0 -- none
outputError options file error = do outputError options file error = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult options result contents = do outputResult options result sys = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
let comments = crComments result let comments = crComments result
let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do
let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName
let contents = either (const "") id result
let fileLines = lines contents let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines let lineCount = fromIntegral $ length fileLines
let groups = groupWith lineNo comments let groups = groupWith lineNo comments
@@ -62,7 +69,7 @@ outputResult options result contents = do
else fileLines !! fromIntegral (lineNum - 1) else fileLines !! fromIntegral (lineNum - 1)
putStrLn "" putStrLn ""
putStrLn $ color "message" $ putStrLn $ color "message" $
"In " ++ crFilename result ++" line " ++ show lineNum ++ ":" "In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line) putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x
putStrLn "" putStrLn ""

View File

@@ -24,7 +24,7 @@ import Control.Monad.Identity
import qualified Data.Map as Map import qualified Data.Map as Map
data SystemInterface m = SystemInterface { newtype SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error -- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String) siReadFile :: String -> m (Either ErrorMessage String)
} }
@@ -33,6 +33,7 @@ data SystemInterface m = SystemInterface {
data CheckSpec = CheckSpec { data CheckSpec = CheckSpec {
csFilename :: String, csFilename :: String,
csScript :: String, csScript :: String,
csCheckSourced :: Bool,
csExcludedWarnings :: [Integer], csExcludedWarnings :: [Integer],
csShellTypeOverride :: Maybe Shell csShellTypeOverride :: Maybe Shell
} deriving (Show, Eq) } deriving (Show, Eq)
@@ -42,9 +43,11 @@ data CheckResult = CheckResult {
crComments :: [PositionedComment] crComments :: [PositionedComment]
} deriving (Show, Eq) } deriving (Show, Eq)
emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec { emptyCheckSpec = CheckSpec {
csFilename = "", csFilename = "",
csScript = "", csScript = "",
csCheckSourced = False,
csExcludedWarnings = [], csExcludedWarnings = [],
csShellTypeOverride = Nothing csShellTypeOverride = Nothing
} }
@@ -52,7 +55,8 @@ emptyCheckSpec = CheckSpec {
-- Parser input and output -- Parser input and output
data ParseSpec = ParseSpec { data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
psScript :: String psScript :: String,
psCheckSourced :: Bool
} deriving (Show, Eq) } deriving (Show, Eq)
data ParseResult = ParseResult { data ParseResult = ParseResult {
@@ -65,16 +69,17 @@ data ParseResult = ParseResult {
data AnalysisSpec = AnalysisSpec { data AnalysisSpec = AnalysisSpec {
asScript :: Token, asScript :: Token,
asShellType :: Maybe Shell, asShellType :: Maybe Shell,
asExecutionMode :: ExecutionMode asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool
} }
data AnalysisResult = AnalysisResult { newtype AnalysisResult = AnalysisResult {
arComments :: [TokenComment] arComments :: [TokenComment]
} }
-- Formatter options -- Formatter options
data FormatterOptions = FormatterOptions { newtype FormatterOptions = FormatterOptions {
foColorOption :: ColorOption foColorOption :: ColorOption
} }

View File

@@ -14,7 +14,7 @@
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
yOU should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell, FlexibleContexts #-} {-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell, FlexibleContexts #-}
@@ -47,7 +47,7 @@ import qualified Data.Map as Map
import Test.QuickCheck.All (quickCheckAll) import Test.QuickCheck.All (quickCheckAll)
type SCBase m = Mr.ReaderT (SystemInterface m) (Ms.StateT SystemState m) type SCBase m = Mr.ReaderT (Environment m) (Ms.StateT SystemState m)
type SCParser m v = ParsecT String UserState (SCBase m) v type SCParser m v = ParsecT String UserState (SCBase m) v
backslash :: Monad m => SCParser m Char backslash :: Monad m => SCParser m Char
@@ -62,13 +62,16 @@ singleQuote = char '\''
doubleQuote = char '"' doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_" variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_" variableChars = upper <|> lower <|> digit <|> oneOf "_"
functionChars = variableChars <|> oneOf ":+-.?" -- Chars to allow in function names
functionChars = variableChars <|> oneOf ":+?-./^"
-- Chars to allow in functions using the 'function' keyword
extendedFunctionChars = functionChars <|> oneOf "[]*=!"
specialVariable = oneOf "@*#?-$!" specialVariable = oneOf "@*#?-$!"
paramSubSpecialChars = oneOf "/:+-=%" paramSubSpecialChars = oneOf "/:+-=%"
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
quotable = almostSpace <|> oneOf quotableChars quotable = almostSpace <|> oneOf quotableChars
bracedQuotable = oneOf "}\"$`'" bracedQuotable = oneOf "}\"$`'"
doubleQuotableChars = "\"$`" doubleQuotableChars = "\\\"$`"
doubleQuotable = oneOf doubleQuotableChars doubleQuotable = oneOf doubleQuotableChars
whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed
linewhitespace = oneOf " \t" <|> almostSpace linewhitespace = oneOf " \t" <|> almostSpace
@@ -248,12 +251,14 @@ addParseNote n = do
shouldIgnoreCode code = do shouldIgnoreCode code = do
context <- getCurrentContexts context <- getCurrentContexts
return $ any disabling context checkSourced <- Mr.asks checkSourced
return $ any (disabling checkSourced) context
where where
disabling (ContextAnnotation list) = disabling checkSourced item =
any disabling' list case item of
disabling (ContextSource _) = True -- Don't add messages for sourced files ContextAnnotation list -> any disabling' list
disabling _ = False ContextSource _ -> not $ checkSourced
_ -> False
disabling' (DisableComment n) = code == n disabling' (DisableComment n) = code == n
disabling' _ = False disabling' _ = False
@@ -297,6 +302,11 @@ initialSystemState = SystemState {
parseProblems = [] parseProblems = []
} }
data Environment m = Environment {
systemInterface :: SystemInterface m,
checkSourced :: Bool
}
parseProblem level code msg = do parseProblem level code msg = do
pos <- getPosition pos <- getPosition
parseProblemAt pos level code msg parseProblemAt pos level code msg
@@ -357,10 +367,8 @@ unexpecting s p = try $
notFollowedBy2 = unexpecting "" notFollowedBy2 = unexpecting ""
disregard = void
reluctantlyTill p end = reluctantlyTill p end =
(lookAhead (disregard (try end) <|> eof) >> return []) <|> do (lookAhead (void (try end) <|> eof) >> return []) <|> do
x <- p x <- p
more <- reluctantlyTill p end more <- reluctantlyTill p end
return $ x:more return $ x:more
@@ -440,19 +448,9 @@ readConditionContents single =
getOp = do getOp = do
id <- getNextId id <- getNextId
op <- anyQuotedOp <|> anyEscapedOp <|> anyOp op <- readRegularOrEscaped anyOp
return $ TC_Binary id typ op return $ TC_Binary id typ op
-- hacks to read quoted operators without having to read a shell word
anyEscapedOp = try $ do
char '\\'
escaped <$> anyOp
anyQuotedOp = try $ do
c <- oneOf "'\""
s <- anyOp
char c
return $ escaped s
anyOp = flagOp <|> flaglessOp <|> fail anyOp = flagOp <|> flaglessOp <|> fail
"Expected comparison operator (don't wrap commands in []/[[]])" "Expected comparison operator (don't wrap commands in []/[[]])"
flagOp = try $ do flagOp = try $ do
@@ -461,10 +459,25 @@ readConditionContents single =
return s return s
flaglessOp = flaglessOp =
choice $ map (try . string) flaglessOps choice $ map (try . string) flaglessOps
escaped s = if any (`elem` s) "<>" then '\\':s else s
-- hacks to read quoted operators without having to read a shell word
readEscaped p = try $ withEscape <|> withQuotes
where
withEscape = do
char '\\'
escaped <$> p
withQuotes = do
c <- oneOf "'\""
s <- p
char c
return $ escaped s
escaped s = if any (`elem` s) "<>()" then '\\':s else s
readRegularOrEscaped p = readEscaped p <|> p
guardArithmetic = do guardArithmetic = do
try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ") try . lookAhead $ void (oneOf "+*/%") <|> void (string "- ")
parseProblem ErrorC 1076 $ parseProblem ErrorC 1076 $
if single if single
then "Trying to do math? Use e.g. [ $((i/2+7)) -ge 18 ]." then "Trying to do math? Use e.g. [ $((i/2+7)) -ge 18 ]."
@@ -507,7 +520,7 @@ readConditionContents single =
parseProblemAt pos ErrorC 1021 parseProblemAt pos ErrorC 1021
"You need a space before the \\)" "You need a space before the \\)"
fail "Missing space before )" fail "Missing space before )"
disregard spacing void spacing
return x return x
where endedWith str (T_NormalWord id s@(_:_)) = where endedWith str (T_NormalWord id s@(_:_)) =
case last s of T_Literal id s -> str `isSuffixOf` s case last s of T_Literal id s -> str `isSuffixOf` s
@@ -560,29 +573,30 @@ readConditionContents single =
"You need a space before and after the " ++ trailingOp ++ " ." "You need a space before and after the " ++ trailingOp ++ " ."
readCondGroup = do readCondGroup = do
id <- getNextId id <- getNextId
pos <- getPosition pos <- getPosition
lparen <- try $ string "(" <|> string "\\(" lparen <- try $ readRegularOrEscaped (string "(")
when (single && lparen == "(") $ when (single && lparen == "(") $
parseProblemAt pos ErrorC 1028 "In [..] you have to escape (). Use [[..]] instead." singleWarning pos
when (not single && lparen == "\\(") $ when (not single && lparen == "\\(") $
parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ()." doubleWarning pos
condSpacing single condSpacing single
x <- readCondContents x <- readCondContents
cpos <- getPosition cpos <- getPosition
rparen <- string ")" <|> string "\\)" rparen <- readRegularOrEscaped (string ")")
condSpacing single condSpacing single
when (single && rparen == ")") $ when (single && rparen == ")") $
parseProblemAt cpos ErrorC 1030 "In [..] you have to escape (). Use [[..]] instead." singleWarning cpos
when (not single && rparen == "\\)") $ when (not single && rparen == "\\)") $
parseProblemAt cpos ErrorC 1031 "In [[..]] you shouldn't escape ()." doubleWarning cpos
when (isEscaped lparen `xor` isEscaped rparen) $ return $ TC_Group id typ x
parseProblemAt pos ErrorC 1032 "Did you just escape one half of () but not the other?"
return $ TC_Group id typ x
where where
isEscaped ('\\':_) = True singleWarning pos =
isEscaped _ = False parseProblemAt pos ErrorC 1028 "In [..] you have to escape \\( \\) or preferably combine [..] expressions."
xor x y = x && not y || not x && y doubleWarning pos =
parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ( or )."
-- Currently a bit of a hack since parsing rules are obscure -- Currently a bit of a hack since parsing rules are obscure
regexOperatorAhead = lookAhead (do regexOperatorAhead = lookAhead (do
@@ -599,7 +613,7 @@ readConditionContents single =
readNormalLiteral "( " <|> readNormalLiteral "( " <|>
readPipeLiteral <|> readPipeLiteral <|>
readGlobLiteral) readGlobLiteral)
disregard spacing void spacing
return $ T_NormalWord id parts return $ T_NormalWord id parts
where where
readGlobLiteral = do readGlobLiteral = do
@@ -848,11 +862,14 @@ prop_readCondition14= isOk readCondition "[ foo '>' bar ]"
prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]" prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]"
prop_readCondition16= isOk readCondition "[ foo \\< bar ]" prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]" prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
prop_readCondition18= isOk readCondition "[ ]"
prop_readCondition19= isOk readCondition "[ '(' x \")\" ]"
readCondition = called "test expression" $ do readCondition = called "test expression" $ do
opos <- getPosition opos <- getPosition
id <- getNextId id <- getNextId
open <- try (string "[[") <|> string "[" open <- try (string "[[") <|> string "["
let single = open == "[" let single = open == "["
let typ = if single then SingleBracket else DoubleBracket
pos <- getPosition pos <- getPosition
space <- allspacing space <- allspacing
@@ -864,7 +881,11 @@ readCondition = called "test expression" $ do
when (single && '\n' `elem` space) $ when (single && '\n' `elem` space) $
parseProblemAt pos ErrorC 1080 "You need \\ before line feeds to break lines in [ ]." parseProblemAt pos ErrorC 1080 "You need \\ before line feeds to break lines in [ ]."
condition <- readConditionContents single condition <- readConditionContents single <|> do
guard . not . null $ space
lookAhead $ string "]"
id <- getNextIdAt pos
return $ TC_Empty id typ
cpos <- getPosition cpos <- getPosition
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])" close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
@@ -872,7 +893,7 @@ readCondition = called "test expression" $ do
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?" when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
spacing spacing
many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme? many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
return $ T_Condition id (if single then SingleBracket else DoubleBracket) condition return $ T_Condition id typ condition
readAnnotationPrefix = do readAnnotationPrefix = do
char '#' char '#'
@@ -883,47 +904,51 @@ prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n"
prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n" prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n"
prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n" prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n"
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n" prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
readAnnotation = called "shellcheck annotation" $ do prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
readAnnotation = called "shellcheck directive" $ do
try readAnnotationPrefix try readAnnotationPrefix
many1 linewhitespace many1 linewhitespace
values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey) values <- many1 readKey
linefeed optional readAnyComment
void linefeed <|> do
parseNote ErrorC 1125 "Invalid key=value pair? Ignoring the rest of this directive starting here."
many (noneOf "\n")
void linefeed <|> eof
many linewhitespace many linewhitespace
return $ concat values return $ concat values
where where
readDisable = forKey "disable" $ readKey = do
readCode `sepBy` char ',' keyPos <- getPosition
where key <- many1 letter
readCode = do char '=' <|> fail "Expected '=' after directive key"
optional $ string "SC" annotations <- case key of
int <- many1 digit "disable" -> readCode `sepBy` char ','
return $ DisableComment (read int) where
readCode = do
optional $ string "SC"
int <- many1 digit
return $ DisableComment (read int)
readSourceOverride = forKey "source" $ do "source" -> do
filename <- many1 $ noneOf " \n" filename <- many1 $ noneOf " \n"
return [SourceOverride filename] return [SourceOverride filename]
readShellOverride = forKey "shell" $ do "shell" -> do
pos <- getPosition pos <- getPosition
shell <- many1 $ noneOf " \n" shell <- many1 $ noneOf " \n"
when (isNothing $ shellForExecutable shell) $ when (isNothing $ shellForExecutable shell) $
parseNoteAt pos ErrorC 1103 parseNoteAt pos ErrorC 1103
"This shell type is unknown. Use e.g. sh or bash." "This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell] return [ShellOverride shell]
_ -> do
parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored."
anyChar `reluctantlyTill` whitespace
return []
forKey s p = do
try $ string s
char '='
value <- p
many linewhitespace many linewhitespace
return value return annotations
anyKey = do
pos <- getPosition
anyChar `reluctantlyTill1` whitespace
many linewhitespace
parseNoteAt pos WarningC 1107 "This directive is unknown. It will be ignored."
return []
readAnnotations = do readAnnotations = do
annotations <- many (readAnnotation `thenSkip` allspacing) annotations <- many (readAnnotation `thenSkip` allspacing)
@@ -931,6 +956,9 @@ readAnnotations = do
readComment = do readComment = do
unexpecting "shellcheck annotation" readAnnotationPrefix unexpecting "shellcheck annotation" readAnnotationPrefix
readAnyComment
readAnyComment = do
char '#' char '#'
many $ noneOf "\r\n" many $ noneOf "\r\n"
@@ -1132,7 +1160,7 @@ readBackTicked quoted = called "backtick expansion" $ do
verifyEof verifyEof
return cmds return cmds
backtick = backtick =
disregard (char '`') <|> do void (char '`') <|> do
pos <- getPosition pos <- getPosition
char '´' char '´'
parseProblemAt pos ErrorC 1077 parseProblemAt pos ErrorC 1077
@@ -1173,6 +1201,8 @@ prop_readDoubleQuoted5 = isOk readSimpleCommand "lol \"foo\nbar\" etc"
prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\"" prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\""
prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\"" prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\""
prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\"" prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\""
prop_readDoubleQuoted9 = isWarning readDoubleQuoted "\"foo\\n\""
prop_readDoubleQuoted10 = isOk readDoubleQuoted "\"foo\\\\n\""
readDoubleQuoted = called "double quoted string" $ do readDoubleQuoted = called "double quoted string" $ do
id <- getNextId id <- getNextId
startPos <- getPosition startPos <- getPosition
@@ -1219,7 +1249,7 @@ readDoubleLiteral = do
return $ T_Literal id (concat s) return $ T_Literal id (concat s)
readDoubleLiteralPart = do readDoubleLiteralPart = do
x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars ++ unicodeDoubleQuotes))) x <- many1 (readDoubleEscaped <|> many1 (noneOf (doubleQuotableChars ++ unicodeDoubleQuotes)))
return $ concat x return $ concat x
readNormalLiteral end = do readNormalLiteral end = do
@@ -1339,17 +1369,22 @@ readSingleEscaped = do
x <- lookAhead anyChar x <- lookAhead anyChar
case x of case x of
'\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\''s done'."; '\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'.";
'\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line." '\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line."
_ -> return () _ -> return ()
return [s] return [s]
readDoubleEscaped = do readDoubleEscaped = do
pos <- getPosition
bs <- backslash bs <- backslash
(linefeed >> return "") (linefeed >> return "")
<|> liftM return doubleQuotable <|> liftM return doubleQuotable
<|> liftM (\ x -> [bs, x]) anyChar <|> do
c <- anyChar
parseNoteAt pos StyleC 1117 $
"Backslash is literal in \"\\" ++ [c] ++ "\". Prefer explicit escaping: \"\\\\" ++ [c] ++ "\"."
return [bs, c]
readBraceEscaped = do readBraceEscaped = do
bs <- backslash bs <- backslash
@@ -1583,6 +1618,11 @@ 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_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n" prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo" prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"
prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\n"
prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n"
readHereDoc = called "here document" $ do readHereDoc = called "here document" $ do
fid <- getNextId fid <- getNextId
pos <- getPosition pos <- getPosition
@@ -1616,20 +1656,37 @@ readPendingHereDocs = do
where where
readDoc (T_HereDoc id dashed quoted endToken _) = do readDoc (T_HereDoc id dashed quoted endToken _) = do
pos <- getPosition pos <- getPosition
hereData <- anyChar `reluctantlyTill` do hereData <- concat <$> rawLine `reluctantlyTill` do
spacing linewhitespace `reluctantlyTill` string endToken
string endToken string endToken
disregard (char '\n') <|> eof void linewhitespace <|> void (oneOf "\n;&#)") <|> eof
do do
spaces <- spacing spaces <- linewhitespace `reluctantlyTill` string endToken
verifyHereDoc dashed quoted spaces hereData verifyHereDoc dashed quoted spaces hereData
string endToken string endToken
trailingPos <- getPosition
trailers <- lookAhead $ many (noneOf "\n")
let ppt = parseProblemAt trailingPos ErrorC
unless (null trailers) $
if all isSpace trailers
then ppt 1118 "Delete whitespace after the here-doc end token."
else case (head $ dropWhile isSpace trailers) of
')' -> ppt 1119 $ "Add a linefeed between end token and terminating ')'."
'#' -> ppt 1120 "No comments allowed after here-doc token. Comment the next line instead."
c | c `elem` ";&" ->
ppt 1121 "Add ;/& terminators (and other syntax) on the line with the <<, not here."
_ -> ppt 1122 "Nothing allowed after end token. To continue a command, put it on the line with the <<."
parsedData <- parseHereData quoted pos hereData parsedData <- parseHereData quoted pos hereData
list <- parseHereData quoted pos hereData list <- parseHereData quoted pos hereData
addToHereDocMap id list addToHereDocMap id list
`attempting` (eof >> debugHereDoc pos endToken hereData) `attempting` (eof >> debugHereDoc pos endToken hereData)
rawLine = do
c <- many $ noneOf "\n"
void (char '\n') <|> eof
return $ c ++ "\n"
parseHereData Quoted startPos hereData = do parseHereData Quoted startPos hereData = do
id <- getNextIdAt startPos id <- getNextIdAt startPos
return [T_Literal id hereData] return [T_Literal id hereData]
@@ -1654,9 +1711,9 @@ readPendingHereDocs = do
debugHereDoc pos endToken doc debugHereDoc pos endToken doc
| endToken `isInfixOf` doc = | endToken `isInfixOf` doc =
let lookAt line = when (endToken `isInfixOf` line) $ let lookAt line = when (endToken `isInfixOf` line) $
parseProblemAt pos ErrorC 1041 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').") parseProblemAt pos ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').")
in do in do
parseProblemAt pos ErrorC 1042 ("Found '" ++ endToken ++ "' further down, but not entirely by itself.") parseProblemAt pos ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.")
mapM_ lookAt (lines doc) mapM_ lookAt (lines doc)
| map toLower endToken `isInfixOf` map toLower doc = | map toLower endToken `isInfixOf` map toLower doc =
parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.") parseProblemAt pos ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.")
@@ -1702,6 +1759,7 @@ readIoRedirect = do
id <- getNextId id <- getNextId
n <- readIoSource n <- readIoSource
redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile
skipAnnotationAndWarn
spacing spacing
return $ T_FdRedirect id n redir return $ T_FdRedirect id n redir
@@ -1745,7 +1803,7 @@ readSeparatorOp = do
spacing spacing
return f return f
readSequentialSep = disregard (g_Semi >> readLineBreak) <|> disregard readNewlineList readSequentialSep = void (g_Semi >> readLineBreak) <|> void readNewlineList
readSeparator = readSeparator =
do do
separator <- readSeparatorOp separator <- readSeparatorOp
@@ -1779,11 +1837,13 @@ prop_readSimpleCommand3 = isOk readSimpleCommand "export foo=(bar baz)"
prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)" prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)"
prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi" prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi"
prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )" prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"
prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls"
readSimpleCommand = called "simple command" $ do readSimpleCommand = called "simple command" $ do
pos <- getPosition pos <- getPosition
id1 <- getNextId id1 <- getNextId
id2 <- getNextId id2 <- getNextId
prefix <- option [] readCmdPrefix prefix <- option [] readCmdPrefix
skipAnnotationAndWarn
cmd <- option Nothing $ do { f <- readCmdName; return $ Just f; } cmd <- option Nothing $ do { f <- readCmdName; return $ Just f; }
when (null prefix && isNothing cmd) $ fail "Expected a command" when (null prefix && isNothing cmd) $ fail "Expected a command"
case cmd of case cmd of
@@ -1831,7 +1891,7 @@ readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do
"This file appears to be recursively sourced. Ignoring." "This file appears to be recursively sourced. Ignoring."
return t return t
else do else do
sys <- Mr.ask sys <- Mr.asks systemInterface
input <- input <-
if filename == "/dev/null" -- always allow /dev/null if filename == "/dev/null" -- always allow /dev/null
then return (Right "") then return (Right "")
@@ -1879,8 +1939,13 @@ prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
prop_readAndOr2 = isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo" prop_readAndOr2 = isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo"
readAndOr = do readAndOr = do
aid <- getNextId aid <- getNextId
apos <- getPosition
annotations <- readAnnotations annotations <- readAnnotations
unless (null annotations) $ optional $ do
try . lookAhead $ readKeyword
parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."
andOr <- withAnnotations annotations $ andOr <- withAnnotations annotations $
chainr1 readPipeline $ do chainr1 readPipeline $ do
op <- g_AND_IF <|> g_OR_IF op <- g_AND_IF <|> g_OR_IF
@@ -1948,8 +2013,24 @@ readCommand = choice [
readSimpleCommand readSimpleCommand
] ]
readCmdName = readCmdWord readCmdName = do
readCmdWord = readNormalWord <* spacing -- Ignore alias suppression
optional . try $ do
char '\\'
lookAhead $ variableChars
readCmdWord
readCmdWord = do
skipAnnotationAndWarn
readNormalWord <* spacing
-- Due to poor planning, annotations after commands isn't handled well.
-- At the time this function is used, it's usually too late to skip
-- comments, so you end up with a parse failure instead.
skipAnnotationAndWarn = optional $ do
try . lookAhead $ readAnnotationPrefix
parseProblem ErrorC 1126 "Place shellcheck directives before commands, not after."
readAnyComment
prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi" prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi"
prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi" prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"
@@ -1990,7 +2071,7 @@ readIfPart = do
parseProblem ErrorC 1050 "Expected 'then'." parseProblem ErrorC 1050 "Expected 'then'."
return "Expected 'then'" return "Expected 'then'"
acceptButWarn g_Semi ErrorC 1051 "No semicolons directly after 'then'." acceptButWarn g_Semi ErrorC 1051 "Semicolons directly after 'then' are not allowed. Just remove it."
allspacing allspacing
verifyNotEmptyIf "then" verifyNotEmptyIf "then"
@@ -2009,7 +2090,7 @@ readElifPart = called "elif clause" $ do
parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?" parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?"
g_Then g_Then
acceptButWarn g_Semi ErrorC 1052 "No semicolons directly after 'then'." acceptButWarn g_Semi ErrorC 1052 "Semicolons directly after 'then' are not allowed. Just remove it."
allspacing allspacing
verifyNotEmptyIf "then" verifyNotEmptyIf "then"
action <- readTerm action <- readTerm
@@ -2021,7 +2102,7 @@ readElifPart = called "elif clause" $ do
readElsePart = called "else clause" $ do readElsePart = called "else clause" $ do
pos <- getPosition pos <- getPosition
g_Else g_Else
acceptButWarn g_Semi ErrorC 1053 "No semicolons directly after 'else'." acceptButWarn g_Semi ErrorC 1053 "Semicolons directly after 'else' are not allowed. Just remove it."
allspacing allspacing
verifyNotEmptyIf "else" verifyNotEmptyIf "else"
readTerm readTerm
@@ -2043,10 +2124,13 @@ readSubshell = called "explicit subshell" $ do
prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }" prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"
prop_readBraceGroup2 = isWarning readBraceGroup "{foo;}" prop_readBraceGroup2 = isWarning readBraceGroup "{foo;}"
prop_readBraceGroup3 = isOk readBraceGroup "{(foo)}"
readBraceGroup = called "brace group" $ do readBraceGroup = called "brace group" $ do
id <- getNextId id <- getNextId
char '{' char '{'
allspacingOrFail <|> parseProblem ErrorC 1054 "You need a space after the '{'." void allspacingOrFail <|> optional (do
lookAhead $ noneOf "(" -- {( is legal
parseProblem ErrorC 1054 "You need a space after the '{'.")
optional $ do optional $ do
pos <- getPosition pos <- getPosition
lookAhead $ char '}' lookAhead $ char '}'
@@ -2108,6 +2192,7 @@ prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone"
prop_readForClause9 = isOk readForClause "for i do true; done" prop_readForClause9 = isOk readForClause "for i do true; done"
prop_readForClause10= isOk readForClause "for ((;;)) { true; }" prop_readForClause10= isOk readForClause "for ((;;)) { true; }"
prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done" prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done"
prop_readForClause13= isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done"
readForClause = called "for loop" $ do readForClause = called "for loop" $ do
pos <- getPosition pos <- getPosition
(T_For id) <- g_For (T_For id) <- g_For
@@ -2135,7 +2220,7 @@ readForClause = called "for loop" $ do
readRegular id pos = do readRegular id pos = do
acceptButWarn (char '$') ErrorC 1086 acceptButWarn (char '$') ErrorC 1086
"Don't use $ on the iterator name in for loops." "Don't use $ on the iterator name in for loops."
name <- readVariableName `thenSkip` spacing name <- readVariableName `thenSkip` allspacing
values <- readInClause <|> (optional readSequentialSep >> return []) values <- readInClause <|> (optional readSequentialSep >> return [])
group <- readDoGroup pos group <- readDoGroup pos
return $ T_ForIn id name values group return $ T_ForIn id name values group
@@ -2159,14 +2244,14 @@ readSelectClause = called "select loop" $ do
readInClause = do readInClause = do
g_In g_In
things <- readCmdWord `reluctantlyTill` things <- readCmdWord `reluctantlyTill`
(disregard g_Semi <|> disregard linefeed <|> disregard g_Do) (void g_Semi <|> void linefeed <|> void g_Do)
do { do {
lookAhead g_Do; lookAhead g_Do;
parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'."; parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'.";
} <|> do { } <|> do {
optional g_Semi; optional g_Semi;
disregard allspacing; void allspacing;
} }
return things return things
@@ -2191,9 +2276,12 @@ readCaseList = many readCaseItem
readCaseItem = called "case item" $ do readCaseItem = called "case item" $ do
notFollowedBy2 g_Esac notFollowedBy2 g_Esac
optional $ do
try . lookAhead $ readAnnotationPrefix
parseProblem ErrorC 1124 "ShellCheck directives are only valid in front of complete commands like 'case' statements, not individual case branches."
optional g_Lparen optional g_Lparen
spacing spacing
pattern <- readPattern pattern' <- readPattern
void g_Rparen <|> do void g_Rparen <|> do
parseProblem ErrorC 1085 parseProblem ErrorC 1085
"Did you forget to move the ;; after extending this case item?" "Did you forget to move the ;; after extending this case item?"
@@ -2206,7 +2294,7 @@ readCaseItem = called "case item" $ do
parseProblemAt pos ErrorC 1074 parseProblemAt pos ErrorC 1074
"Did you forget the ;; after the previous case item?" "Did you forget the ;; after the previous case item?"
readLineBreak readLineBreak
return (separator, pattern, list) return (separator, pattern', list)
readCaseSeparator = choice [ readCaseSeparator = choice [
tryToken ";;&" (const ()) >> return CaseContinue, tryToken ";;&" (const ()) >> return CaseContinue,
@@ -2225,10 +2313,11 @@ prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }" prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"
prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }" prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"
prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}" prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"
prop_readFunctionDefinition12= isOk readFunctionDefinition "function []!() { true; }"
readFunctionDefinition = called "function" $ do readFunctionDefinition = called "function" $ do
functionSignature <- try readFunctionSignature functionSignature <- try readFunctionSignature
allspacing allspacing
disregard (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition." void (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition."
group <- readBraceGroup <|> readSubshell group <- readBraceGroup <|> readSubshell
return $ functionSignature group return $ functionSignature group
where where
@@ -2241,7 +2330,7 @@ readFunctionDefinition = called "function" $ do
string "function" string "function"
whitespace whitespace
spacing spacing
name <- readFunctionName name <- many1 extendedFunctionChars
spaces <- spacing spaces <- spacing
hasParens <- wasIncluded readParens hasParens <- wasIncluded readParens
when (not hasParens && null spaces) $ when (not hasParens && null spaces) $
@@ -2251,7 +2340,7 @@ readFunctionDefinition = called "function" $ do
readWithoutFunction = try $ do readWithoutFunction = try $ do
id <- getNextId id <- getNextId
name <- readFunctionName name <- many1 functionChars
guard $ name /= "time" -- Interfers with time ( foo ) guard $ name /= "time" -- Interfers with time ( foo )
spacing spacing
readParens readParens
@@ -2266,8 +2355,6 @@ readFunctionDefinition = called "function" $ do
g_Rparen g_Rparen
return () return ()
readFunctionName = many1 functionChars
prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }" prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }"
prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }"
prop_readCoProc3 = isOk readCoProc "coproc echo bar" prop_readCoProc3 = isOk readCoProc "coproc echo bar"
@@ -2394,6 +2481,9 @@ prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42" prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
readAssignmentWord = readAssignmentWordExt True readAssignmentWord = readAssignmentWordExt True
readWellFormedAssignment = readAssignmentWordExt False readWellFormedAssignment = readAssignmentWordExt False
readAssignmentWordExt lenient = try $ do readAssignmentWordExt lenient = try $ do
@@ -2410,7 +2500,7 @@ readAssignmentWordExt lenient = try $ do
pos <- getPosition pos <- getPosition
op <- readAssignmentOp op <- readAssignmentOp
hasRightSpace <- liftM (not . null) spacing hasRightSpace <- liftM (not . null) spacing
isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof)) isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
if not hasLeftSpace && (hasRightSpace || isEndOfCommand) if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
then do then do
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
@@ -2420,7 +2510,11 @@ readAssignmentWordExt lenient = try $ do
return $ T_Assignment id op variable indices value return $ T_Assignment id op variable indices value
else do else do
when (hasLeftSpace || hasRightSpace) $ when (hasLeftSpace || hasRightSpace) $
parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments." parseNoteAt pos ErrorC 1068 $
"Don't put spaces around the "
++ if op == Append
then "+= when appending."
else "= in assignments."
value <- readArray <|> readNormalWord value <- readArray <|> readNormalWord
spacing spacing
return $ T_Assignment id op variable indices value return $ T_Assignment id op variable indices value
@@ -2453,7 +2547,11 @@ readArrayIndex = do
readArray :: Monad m => SCParser m Token readArray :: Monad m => SCParser m Token
readArray = called "array assignment" $ do readArray = called "array assignment" $ do
id <- getNextId id <- getNextId
opening <- getPosition
char '(' char '('
optional $ do
lookAhead $ char '('
parseProblemAt opening ErrorC 1116 "Missing $ on a $((..)) expression? (or use ( ( for arrays)."
allspacing allspacing
words <- readElement `reluctantlyTill` char ')' words <- readElement `reluctantlyTill` char ')'
char ')' <|> fail "Expected ) to close array assignment" char ')' <|> fail "Expected ) to close array assignment"
@@ -2466,9 +2564,9 @@ readArray = called "array assignment" $ do
x <- many1 readArrayIndex x <- many1 readArrayIndex
char '=' char '='
return x return x
value <- readNormalWord <|> nothing value <- readRegular <|> nothing
return $ T_IndexedElement id index value return $ T_IndexedElement id index value
readRegular = readNormalWord readRegular = readArray <|> readNormalWord
nothing = do nothing = do
id <- getNextId id <- getNextId
@@ -2498,7 +2596,7 @@ tryParseWordToken keyword t = try $ do
try . lookAhead $ char '#' try . lookAhead $ char '#'
parseProblem ErrorC 1099 "You need a space before the #." parseProblem ErrorC 1099 "You need a space before the #."
try $ lookAhead keywordSeparator lookAhead keywordSeparator
when (str /= keyword) $ when (str /= keyword) $
parseProblem ErrorC 1081 $ parseProblem ErrorC 1081 $
"Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'." "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."
@@ -2534,7 +2632,7 @@ g_While = tryWordToken "while" T_While
g_Until = tryWordToken "until" T_Until g_Until = tryWordToken "until" T_Until
g_For = tryWordToken "for" T_For g_For = tryWordToken "for" T_For
g_Select = tryWordToken "select" T_Select g_Select = tryWordToken "select" T_Select
g_In = tryWordToken "in" T_In g_In = tryWordToken "in" T_In <* skipAnnotationAndWarn
g_Lbrace = tryWordToken "{" T_Lbrace g_Lbrace = tryWordToken "{" T_Lbrace
g_Rbrace = do -- handled specially due to ksh echo "${ foo; }bar" g_Rbrace = do -- handled specially due to ksh echo "${ foo; }bar"
id <- getNextId id <- getNextId
@@ -2557,7 +2655,7 @@ g_Semi = do
tryToken ";" T_Semi tryToken ";" T_Semi
keywordSeparator = keywordSeparator =
eof <|> disregard whitespace <|> disregard (oneOf ";()[<>&|") eof <|> void (try allspacingOrFail) <|> void (oneOf ";()[<>&|")
readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ] readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ]
@@ -2569,7 +2667,13 @@ prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n" prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n"
prop_readShebang4 = isWarning readShebang "! /bin/sh" prop_readShebang4 = isWarning readShebang "! /bin/sh"
readShebang = do readShebang = do
try readCorrect <|> try readSwapped <|> try readMissingHash choice $ map try [
readCorrect,
readSwapped,
readTooManySpaces,
readMissingHash,
readMissingBang
]
many linewhitespace many linewhitespace
str <- many $ noneOf "\r\n" str <- many $ noneOf "\r\n"
optional carriageReturn optional carriageReturn
@@ -2577,21 +2681,47 @@ readShebang = do
return str return str
where where
readCorrect = void $ string "#!" readCorrect = void $ string "#!"
readSwapped = do readSwapped = do
pos <- getPosition pos <- getPosition
string "!#" string "!#"
parseProblemAt pos ErrorC 1084 parseProblemAt pos ErrorC 1084
"Use #!, not !#, for the shebang." "Use #!, not !#, for the shebang."
skipSpaces = liftM (not . null) $ many linewhitespace
readTooManySpaces = do
startPos <- getPosition
startSpaces <- skipSpaces
char '#'
middlePos <- getPosition
middleSpaces <- skipSpaces
char '!'
when startSpaces $
parseProblemAt startPos ErrorC 1114
"Remove leading spaces before the shebang."
when middleSpaces $
parseProblemAt middlePos ErrorC 1115
"Remove spaces between # and ! in the shebang."
readMissingHash = do readMissingHash = do
pos <- getPosition pos <- getPosition
char '!' char '!'
lookAhead $ do ensurePathAhead
many linewhitespace
char '/'
parseProblemAt pos ErrorC 1104 parseProblemAt pos ErrorC 1104
"Use #!, not just !, for the shebang." "Use #!, not just !, for the shebang."
readMissingBang = do
char '#'
pos <- getPosition
ensurePathAhead
parseProblemAt pos ErrorC 1113
"Use #!, not just #, for the shebang."
ensurePathAhead = lookAhead $ do
many linewhitespace
char '/'
verifyEof = eof <|> choice [ verifyEof = eof <|> choice [
ifParsable g_Lparen $ ifParsable g_Lparen $
parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?", parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
@@ -2684,22 +2814,42 @@ readScript = do
script <- readScriptFile script <- readScriptFile
reparseIndices script reparseIndices script
isWarning p s = parsesCleanly p s == Just False
isOk p s = parsesCleanly p s == Just True
isNotOk p s = parsesCleanly p s == Nothing
testParse string = runIdentity $ do -- Interactively run a parser in ghci:
(res, _) <- runParser (mockedSystemInterface []) readScript "-" string -- debugParse readScript "echo 'hello world'"
debugParse p string = runIdentity $ do
(res, _) <- runParser testEnvironment p "-" string
return res return res
testEnvironment =
Environment {
systemInterface = (mockedSystemInterface []),
checkSourced = False
}
isOk p s = parsesCleanly p s == Just True -- The string parses with no warnings
isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings
isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse
parsesCleanly parser string = runIdentity $ do parsesCleanly parser string = runIdentity $ do
(res, sys) <- runParser (mockedSystemInterface []) (res, sys) <- runParser testEnvironment
(parser >> eof >> getState) "-" string (parser >> eof >> getState) "-" string
case (res, sys) of case (res, sys) of
(Right userState, systemState) -> (Right userState, systemState) ->
return $ Just . null $ parseNotes userState ++ parseProblems systemState return $ Just . null $ parseNotes userState ++ parseProblems systemState
(Left _, _) -> return Nothing (Left _, _) -> return Nothing
-- For printf debugging: print the value of an expression
-- Example: return $ dump $ T_Literal id [c]
dump :: Show a => a -> a
dump x = trace (show x) x
-- Like above, but print a specific expression:
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
dumps :: Show x => x -> a -> a
dumps t = trace (show t)
parseWithNotes parser = do parseWithNotes parser = do
item <- parser item <- parser
state <- getState state <- getState
@@ -2728,22 +2878,22 @@ getStringFromParsec errors =
Message s -> if null s then Nothing else return $ s ++ "." Message s -> if null s then Nothing else return $ s ++ "."
runParser :: Monad m => runParser :: Monad m =>
SystemInterface m -> Environment m ->
SCParser m v -> SCParser m v ->
String -> String ->
String -> String ->
m (Either ParseError v, SystemState) m (Either ParseError v, SystemState)
runParser sys p filename contents = runParser env p filename contents =
Ms.runStateT Ms.runStateT
(Mr.runReaderT (Mr.runReaderT
(runParserT p initialUserState filename contents) (runParserT p initialUserState filename contents)
sys) env)
initialSystemState initialSystemState
system = lift . lift . lift system = lift . lift . lift
parseShell sys name contents = do parseShell env name contents = do
(result, state) <- runParser sys (parseWithNotes readScript) name contents (result, state) <- runParser env (parseWithNotes readScript) name contents
case result of case result of
Right (script, userstate) -> Right (script, userstate) ->
return ParseResult { return ParseResult {
@@ -2829,12 +2979,14 @@ posToPos sp = Position {
parseScript :: Monad m => parseScript :: Monad m =>
SystemInterface m -> ParseSpec -> m ParseResult SystemInterface m -> ParseSpec -> m ParseResult
parseScript sys spec = parseScript sys spec =
parseShell sys (psFilename spec) (psScript spec) parseShell env (psFilename spec) (psScript spec)
where
env = Environment {
systemInterface = sys,
checkSourced = psCheckSourced spec
}
lt x = trace (show x) x
ltt t = trace (show t)
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View File

@@ -5,6 +5,6 @@ shopt -s globstar
for i in 1 2 for i in 1 2
do do
last=$(grep -hv "^prop" **/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1) last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1)
echo "Next ${i}xxx: $((last+1))" echo "Next ${i}xxx: $((last+1))"
done done

View File

@@ -32,6 +32,12 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS # OPTIONS
**-a**,\ **--check-sourced**
: Emit warnings in sourced files. Normally, `shellcheck` will only warn
about issues in the specified files. With this option, any issues in
sourced files files will also be reported.
**-C**[*WHEN*],\ **--color**[=*WHEN*] **-C**[*WHEN*],\ **--color**[=*WHEN*]
: For TTY output, enable colors *always*, *never* or *auto*. The default : For TTY output, enable colors *always*, *never* or *auto*. The default
@@ -67,6 +73,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
line (plus `/dev/null`). This option allows following any file the script line (plus `/dev/null`). This option allows following any file the script
may `source`. may `source`.
# FORMATS # FORMATS
**tty** **tty**
@@ -157,6 +164,11 @@ Valid keys are:
used to tell shellcheck where to look for a file whose name is determined used to tell shellcheck where to look for a file whose name is determined
at runtime, or to skip a source by telling it to use `/dev/null`. at runtime, or to skip a source by telling it to use `/dev/null`.
**shell**
: Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'.
# ENVIRONMENT VARIABLES # ENVIRONMENT VARIABLES
The environment variable `SHELLCHECK_OPTS` can be set with default flags: The environment variable `SHELLCHECK_OPTS` can be set with default flags:

View File

@@ -33,8 +33,10 @@ import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Data.Bits import Data.Bits
import Data.Char import Data.Char
import Data.Functor
import Data.Either import Data.Either
import Data.Functor
import Data.IORef
import Data.List
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Maybe import Data.Maybe
import Data.Monoid import Data.Monoid
@@ -74,19 +76,23 @@ defaultOptions = Options {
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
options = [ options = [
Option "e" ["exclude"] Option "a" ["check-sourced"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings", (NoArg $ Flag "sourced" "false") "Include warnings from sourced files",
Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") "output format",
Option "C" ["color"] Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN") (OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)", "Use color (auto, always, never)",
Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")",
Option "s" ["shell"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)", (ReqArg (Flag "shell") "SHELLNAME")
Option "x" ["external-sources"] "Specify dialect (sh, bash, dash, ksh)",
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
Option "V" ["version"] Option "V" ["version"]
(NoArg $ Flag "version" "true") "Print version information" (NoArg $ Flag "version" "true") "Print version information",
Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
] ]
printErr = lift . hPutStrLn stderr printErr = lift . hPutStrLn stderr
@@ -107,6 +113,10 @@ formats options = Map.fromList [
("tty", ShellCheck.Formatter.TTY.format options) ("tty", ShellCheck.Formatter.TTY.format options)
] ]
formatList = intercalate ", " names
where
names = Map.keys $ formats (formatterOptions defaultOptions)
getOption [] _ = Nothing getOption [] _ = Nothing
getOption (Flag var val:_) name | name == var = return val getOption (Flag var val:_) name | name == var = return val
getOption (_:rest) flag = getOption rest flag getOption (_:rest) flag = getOption rest flag
@@ -129,7 +139,7 @@ getExclusions options =
in in
map (Prelude.read . clean) elements :: [Int] map (Prelude.read . clean) elements :: [Int]
toStatus = liftM (either id id) . runExceptT toStatus = fmap (either id id) . runExceptT
getEnvArgs = do getEnvArgs = do
opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv
@@ -186,23 +196,27 @@ runFormatter sys format options files = do
newStatus <- process file `catch` handler file newStatus <- process file `catch` handler file
return $ status `mappend` newStatus return $ status `mappend` newStatus
handler :: FilePath -> IOException -> IO Status handler :: FilePath -> IOException -> IO Status
handler file e = do handler file e = reportFailure file (show e)
onFailure format file (show e) reportFailure file str = do
onFailure format file str
return RuntimeException return RuntimeException
process :: FilePath -> IO Status process :: FilePath -> IO Status
process filename = do process filename = do
contents <- inputFile filename input <- (siReadFile sys) filename
let checkspec = (checkSpec options) { either (reportFailure filename) check input
csFilename = filename, where
csScript = contents check contents = do
} let checkspec = (checkSpec options) {
result <- checkScript sys checkspec csFilename = filename,
onResult format result contents csScript = contents
return $ }
if null (crComments result) result <- checkScript sys checkspec
then NoProblems onResult format result sys
else SomeProblems return $
if null (crComments result)
then NoProblems
else SomeProblems
parseColorOption colorOption = parseColorOption colorOption =
case colorOption of case colorOption of
@@ -247,6 +261,13 @@ parseOption flag options =
} }
} }
Flag "sourced" _ ->
return options {
checkSpec = (checkSpec options) {
csCheckSourced = True
}
}
_ -> return options _ -> return options
where where
die s = do die s = do
@@ -261,14 +282,28 @@ parseOption flag options =
ioInterface options files = do ioInterface options files = do
inputs <- mapM normalize files inputs <- mapM normalize files
cache <- newIORef emptyCache
return SystemInterface { return SystemInterface {
siReadFile = get inputs siReadFile = get cache inputs
} }
where where
get inputs file = do emptyCache :: Map.Map FilePath String
emptyCache = Map.empty
get cache inputs file = do
map <- readIORef cache
case Map.lookup file map of
Just x -> return $ Right x
Nothing -> fetch cache inputs file
fetch cache inputs file = do
ok <- allowable inputs file ok <- allowable inputs file
if ok if ok
then (Right <$> inputFile file) `catch` handler then (do
(contents, shouldCache) <- inputFile file
when shouldCache $
modifyIORef cache $ Map.insert file contents
return $ Right contents
) `catch` handler
else return $ Left (file ++ " was not specified as input (see shellcheck -x).") else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where where
@@ -289,16 +324,19 @@ ioInterface options files = do
fallback path _ = return path fallback path _ = return path
inputFile file = do inputFile file = do
handle <- (handle, shouldCache) <-
if file == "-" if file == "-"
then return stdin then return (stdin, True)
else openBinaryFile file ReadMode else do
h <- openBinaryFile file ReadMode
reopenable <- hIsSeekable h
return (h, not reopenable)
hSetBinaryMode handle True hSetBinaryMode handle True
contents <- decodeString <$> hGetContents handle -- closes handle contents <- decodeString <$> hGetContents handle -- closes handle
seq (length contents) $ seq (length contents) $
return contents return (contents, shouldCache)
-- Decode a char8 string into a utf8 string, with fallback on -- Decode a char8 string into a utf8 string, with fallback on
-- ISO-8859-1. This avoids depending on additional libraries. -- ISO-8859-1. This avoids depending on additional libraries.

View File

@@ -2,7 +2,7 @@
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/ # 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) # 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 resolver: lts-8.5
# Local packages, usually specified by relative directory name # Local packages, usually specified by relative directory name
packages: packages: