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