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
before_install:
- export DOCKER_REPO=koalaman/shellcheck
- |-
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH")
- DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
- DOCKER_BUILDS=""
- 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:
- docker build -t builder -f Dockerfile_builder .
- docker run --rm -it -v $(pwd):/mnt builder
- docker build -t $DOCKER_REPO:$TAG .
- mkdir deploy
# Windows .exe
- 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:
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- |-
([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG"
- for repo in $DOCKER_BUILDS;
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/
COPY package/lib/ /usr/local/lib/
RUN ldconfig /usr/local/lib
# This file assumes ShellCheck has already been built.
# See https://github.com/koalaman/scbuilder
COPY shellcheck /bin/shellcheck
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/"]

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,6 @@ shopt -s globstar
for i in 1 2
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))"
done

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-5.5
resolver: lts-8.5
# Local packages, usually specified by relative directory name
packages: