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