111 Commits

Author SHA1 Message Date
Vidar Holen
e5ad4cf420 Stable version 0.8.0
This release is dedicated to dibblego, who pushed me down the Haskell
rabbit hole. In 2006 I thought you were crazy. Today I *know* you are.
2021-11-06 22:12:39 -07:00
Vidar Holen
eea823e3d0 Fix bad version on stable releases 2021-11-06 22:12:39 -07:00
Vidar Holen
3b6972fbf1 Update copyright years 2021-11-06 19:07:34 -07:00
Vidar Holen
14a38b94cc Update stack resolver 2021-11-06 18:59:24 -07:00
Vidar Holen
71f1db6609 Update distro tests 2021-11-06 18:21:11 -07:00
Vidar Holen
bcca66eb6b Update release checklist 2021-11-06 15:46:57 -07:00
Vidar Holen
8db220ae43 Include local -r in check-extra-masked-returns (fixes #2362) 2021-11-06 15:37:59 -07:00
Vidar Holen
efd49e486f Consider all forms of TA_Assignment to remove spaces (fixes #2364) 2021-10-30 17:47:30 -07:00
Vidar Holen
0dd5c67bdf Warn about [^..] in Dash (fixes #2361) 2021-10-21 21:00:39 -07:00
Vidar Holen
290fc8b945 Have quickscripts search for relevant paths (fixes #2286) 2021-10-15 18:08:24 -07:00
Vidar Holen
7b2092b3cd Give more examples of what ShellCheck looks for 2021-10-15 15:29:52 -07:00
Vidar Holen
788aee1b7c Treat typeset similar to declare (fixes #2354) 2021-10-15 14:41:48 -07:00
Vidar Holen
0d128dd918 Mention known incompatibilities in man page 2021-10-15 12:06:33 -07:00
Vidar Holen
c3aaa27540 Skip SC2214 if variable is modified in loop (fixes #2351) 2021-10-09 12:13:41 -07:00
Vidar Holen
3aedda766d For while getopts; do case .. checks, make sure variable matches 2021-10-09 11:40:52 -07:00
Vidar Holen
205ba429b3 Warn about read foo[i] expanding as glob (fixes #2345) 2021-10-07 18:50:44 -07:00
Vidar Holen
05bdeae3ab Mention require-double-brackets in CHANGELOG 2021-10-07 17:26:08 -07:00
Vidar Holen
38251abe26 Add suggestion level in text for TTY output (fixes #2339) 2021-10-07 17:14:41 -07:00
Vidar Holen
6f7eee4a27 Mention check-extra-masked-returns in changelog 2021-10-02 12:59:55 -07:00
Vidar Holen
23cddb037e Merge pull request #2320 from DoxasticFox/set-e-proc-sub
Add extra checks for masked return codes
2021-10-02 12:52:59 -07:00
Christian Nassif-Haynes
093df8cb24 Add extra checks for masked return codes 2021-10-02 01:36:40 +10:00
Vidar Holen
fac97a5301 Don't emit SC2140 when trapped string is /, = or : (fixes #2334) 2021-09-25 20:23:58 -07:00
Vidar Holen
ad92cb4112 Disable UUOC for cat with unquoted variable (fixes #2333) 2021-09-25 19:46:27 -07:00
Vidar Holen
3a296cd788 The removed check was SC1004, not SC1003 2021-09-19 12:27:16 -07:00
Vidar Holen
db4701d8b5 Add a setgitversion script to update the version string with git 2021-09-18 20:46:46 -07:00
Vidar Holen
e7df718724 Strip lines containing "STRIP" from ./striptests 2021-09-18 20:43:42 -07:00
Vidar Holen
b044f5b23a Don't trigger SC2140 on ${x+"a" "b"} (fixes #2265) 2021-09-18 18:59:42 -07:00
Vidar Holen
8012f6761d Suppress SC2094 when both are input redirections (fixes #2325) 2021-09-18 18:00:15 -07:00
Vidar Holen
2536507060 Remove SC1004 (fixes #2326) 2021-09-18 17:43:55 -07:00
Vidar Holen
09aa15c9b7 Allow disable=all to disable all warnings (fixes #2323) 2021-09-18 12:50:01 -07:00
Vidar Holen
9a54e91195 Merge pull request #2318 from FabianWolff/grep-lL-wc-l
Do not suggest `grep -c` as a replacement for `grep -l/-L | wc -l`
2021-09-16 19:40:40 -07:00
Vidar Holen
4e703e5c61 Allow specifying external-sources=true in shellcheckrc (fixes #1818) 2021-09-15 18:02:37 -07:00
Vidar Holen
64733cc110 Merge pull request #2303 from DoxasticFox/set-e-functions
Show info about `set -e` suppression during function calls
2021-09-04 17:06:24 -04:00
Christian Nassif-Haynes
dc9032fca5 Show info about set -e suppression during function calls 2021-09-05 04:23:25 +10:00
Fabian Wolff
40216487d6 Do not suggest grep -c as a replacement for grep -l/-L | wc -l 2021-09-02 17:47:06 +02:00
Vidar Holen
747bd8fd6a Warn about strings for numerical operators in [[ ]] (fixes #2312) 2021-08-30 19:50:00 -07:00
Vidar Holen
f5fd9c2fed Improve warnings about unnecessary subshells (fixes #2169) 2021-08-30 10:56:55 -07:00
Vidar Holen
10817533d6 Add shellcheck-precommit hook to README.md 2021-08-29 17:08:09 -07:00
Vidar Holen
b5da99c6b0 Add pre-commit instructions 2021-08-29 12:43:23 -07:00
Vidar Holen
b0f05018c1 Revert "Allow running this repo as a pre-commit hook"
This reverts commit 9d64d78c32.
2021-08-29 12:12:08 -07:00
Vidar Holen
9d64d78c32 Allow running this repo as a pre-commit hook 2021-08-28 22:37:22 -07:00
Vidar Holen
081f7eba24 Fix parsing of [$var] (fixes #2309) 2021-08-26 23:05:14 -07:00
Vidar Holen
ecacc2e9bb Merge pull request #2307 from a1346054/fixes
Fix redirect in license file and remove trailing whitespace elsewhere
2021-08-26 19:46:16 -07:00
Vidar Holen
81b7ee5598 Don't warn about unused variables starting with _ (fixes #1498) 2021-08-26 19:40:21 -07:00
Vidar Holen
c85ce2cb06 Add rg to list of commands ignored for SC2016 (fixes #2209) 2021-08-26 18:50:40 -07:00
a1346054
98c7934c46 Remove trailing whitespace 2021-08-25 16:17:56 +00:00
a1346054
7384cec3f6 Fix redirect in LICENSE file
The file was obtained from:
https://www.gnu.org/licenses/gpl-3.0.txt
2021-08-25 14:15:36 +00:00
Vidar Holen
5b6fd60279 Improve warnings for expr (fixes #2033) 2021-08-22 21:12:58 -07:00
Vidar Holen
da7b28213e Recognize wait -p as assigning a variable (fixes #2179) 2021-08-17 21:53:27 -07:00
Vidar Holen
c61fc7546e Don't warn about variables guarded with :+ (fixes #2296) 2021-08-17 16:20:32 -07:00
Vidar Holen
8c0bf8d41f Warn about looping over array values and using them as keys 2021-08-17 12:51:27 -07:00
Vidar Holen
bb0a571a1e Improve warnings for bad parameter expansion (fixes #2297) 2021-08-16 21:02:20 -07:00
Vidar Holen
fed4a048bc Suppress SC2167 when name is "_" (fixes #2298) 2021-08-13 23:11:20 -07:00
Vidar Holen
e5745568e8 Extend warnings about spaces around = to 'let' 2021-08-08 15:48:50 -07:00
Vidar Holen
4dd762253f Remove defunct SonarQube plugin link (fixes #2292) 2021-08-03 13:52:06 -07:00
Vidar Holen
378c9a2f2c Switch build status badge from TravisCI to GitHub 2021-08-03 13:45:09 -07:00
Vidar Holen
cf8066c07c SC2295 Warn about unquoted variables in PE patterns (fixes #2290) 2021-08-03 13:02:53 -07:00
Vidar Holen
9b61506e0b Merge pull request #2289 from nafigator/master
Minor changes in README
2021-08-03 10:13:55 -07:00
Yancharuk Alexander
2f61b17518 Review fixes in README 2021-08-02 19:09:24 +03:00
Yancharuk Alexander
b939f86331 Minor changes in README
It's a recommended practice to use apt instead apt-get:
>apt is a second command-line based front end provided by APT which overcomes some design mistakes of apt-get.
https://debian-handbook.info/browse/stable/sect.apt-get.html

Also added sudo for commands needed root privileges.
2021-07-31 06:24:20 +03:00
Vidar Holen
a44f3edb14 Warn about eval'ing arrays 2021-07-30 18:46:19 -07:00
Vidar Holen
e33146d530 Avoid trigger SC2181 on composite $? checks (fixes #1167) 2021-07-29 20:51:19 -07:00
Vidar Holen
fe81dc1c27 Optionally suggest [[ over [ in Bash scripts (-o require-double-brackets) (fixes #887) 2021-07-27 18:53:30 -07:00
Vidar Holen
fbc8d2cb2f Don't consider [ -n/-z/-v $var ] assignments for subshell modification (fixes #2217) 2021-07-27 09:33:22 -07:00
Vidar Holen
c471e45822 Allow printf/return/assignments after exec (fixes #2249) 2021-07-26 19:32:33 -07:00
Vidar Holen
754ab22d94 Warn about unquoted blanks in echo (fixes #377) 2021-07-26 18:59:33 -07:00
Vidar Holen
4956b006ac Fix broken test from previous commit 2021-07-25 19:56:51 -07:00
Vidar Holen
02e07625d1 Warn about quoting in assignments to sh declaration utilities (fixes #1556) 2021-07-25 19:36:42 -07:00
Vidar Holen
44471b73cc Have SC2155 trigger on 'typeset' as well (fixes #2262) 2021-07-25 17:34:14 -07:00
Vidar Holen
364c33395e Don't print colors when $TERM is 'dumb' or unset (fixes #2260) 2021-07-25 14:44:35 -07:00
Vidar Holen
0d58337cdd Don't warn about repeated range in [[ -v arr[xxx] ]] (fixes #2285) 2021-07-25 13:01:57 -07:00
Vidar Holen
9eb63c97e6 Re-add warnings about 'declare var = value' (fixes #2279) 2021-07-24 13:25:56 -07:00
Vidar Holen
8be60028ef Don't warn when line starts with &> (fixes #2281) 2021-07-22 19:25:48 -07:00
Vidar Holen
9b077e28cb Add :/. to chars recognized for \alias suppression (fixes #2287) 2021-07-21 16:44:21 -07:00
Vidar Holen
99f6554c9b SC2181: Add '!' in suggestion as appropriate (fixes #2189) 2021-07-18 16:59:45 -07:00
Vidar Holen
163629825f Merge pull request #2234 from juhp/patch-1
move readme to extra-doc-files and add changelog to releases
2021-07-18 15:47:26 -07:00
Vidar Holen
022bc8277c Merge pull request #2238 from bcran/legacy-backticks-msg
Fix typo in SC2006 message: "backticked" vs "backticks"
2021-07-02 09:55:18 -07:00
Vidar Holen
5e60f1eddb Merge pull request #2241 from Kamilcuk/master
Add a comma to function characters
2021-07-02 09:54:51 -07:00
Vidar Holen
163b2f12e2 Sanity check command names (fixes #2227) 2021-06-05 18:16:22 -07:00
Kamil Cukrowski
5100960303 Add a comma to function characters
Bash has very relaxed function name rules and a comma is also a valid
character. This commit silences SC1036 check when a function name has a
comma in its name.
2021-05-26 10:58:38 +02:00
Rebecca Cran
b61a7658d6 Fix typo in SC2006 message: "backticked" vs "backticks" 2021-05-24 13:33:50 -06:00
Jens Petersen
ab369a35c9 move readme to extra-doc-files and add changelog to releases 2021-05-18 11:10:17 +08:00
Vidar Holen
331e89be99 Fix bad warning for ${#arr[*]}. Fixes #2218. 2021-04-26 10:44:33 -07:00
Vidar Holen
fe25a2b00e Treat ${arr[*]} like $* for SC2048 2021-04-24 17:08:10 -07:00
Vidar Holen
9e60b3ea84 Fix haddock failures (fixes #2216) 2021-04-22 22:17:51 -07:00
Vidar Holen
d47f3ff986 Add wait between GitHub and Docker to allow replication 2021-04-19 22:46:35 -07:00
Vidar Holen
2f26600653 Update Cabal version for Hackage 2021-04-19 17:36:53 -07:00
Vidar Holen
aaa3554720 Post-release CHANGELOG update 2021-04-19 16:40:25 -07:00
Vidar Holen
cff3e22911 Stable version v0.7.2
This release is dedicated to ethanol, for keeping
COVID-19 off both our hands and our minds.
2021-04-19 14:44:27 -07:00
Vidar Holen
5669eb2203 Make x-comparison warning default 2021-04-11 15:52:13 -07:00
Vidar Holen
b68df1882d Merge pull request #2181 from matthiasdiener/patch-1
Clarify 'which'
2021-03-31 20:03:23 -07:00
Matthias Diener
087865c680 Clarify 'which' 2021-03-20 20:43:18 -05:00
Vidar Holen
19c6f22c3f Merge branch 'm-ildefons-comment-backslash' 2021-03-20 18:13:50 -07:00
Vidar Holen
98952df35b Improve warnings on backslashes in comments 2021-03-20 18:12:39 -07:00
Vidar Holen
a277efdbb1 Merge branch 'comment-backslash' of https://github.com/m-ildefons/shellcheck into m-ildefons-comment-backslash 2021-03-20 13:34:40 -07:00
Vidar Holen
45687b0548 Merge pull request #2166 from avoidik/patch-1
suppress ntlm error messages in Windows build
2021-03-20 13:31:04 -07:00
Vidar Holen
ecdc21b0b7 Merge pull request #2112 from pepeiborra/patch-1
Add Haddock markup to SystemInterface
2021-03-14 13:16:29 -07:00
Vidar Holen
4eb42fa3c1 Merge branch 'austin987-busybox' 2021-03-14 13:02:41 -07:00
Vidar Holen
f02c297fdd Merge parser and analyzer shebang parsing 2021-03-11 23:04:17 -08:00
Vidar Holen
ea83b602d7 Merge branch 'busybox' of https://github.com/austin987/shellcheck into austin987-busybox 2021-03-11 21:44:17 -08:00
Vidar Holen
88cd21fd0f Fix missing +x with new cabal and use previous release deps for caching 2021-03-08 14:01:48 -08:00
Vidar Holen
83435c4f2e Merge pull request #2134 from kolyshkin/podman-sc2016
Whitelist podman for SC2016 about '$var'
2021-03-07 20:48:58 -08:00
Vidar Holen
4324b4a213 Merge pull request #2122 from freddii/master
fixed typing mistakes in changelog
2021-03-07 20:48:24 -08:00
Vidar Holen
a69d6cb661 Merge pull request #2117 from brother/patch-1
Change error 2076 to a warning.
2021-03-07 20:47:53 -08:00
Viacheslav Vasilyev
8442695b73 suppress ntlm error messages in Windows build
when building for windows there are many error message like below

```
003a:err:winediag:SECUR32_initNTLMSP ntlm_auth was not found or is outdated. Make sure that ntlm_auth >= 3.0.25 is in your path. Usually, you can find it in the winbind package of your distribution.
```
2021-03-06 14:19:08 +02:00
Moritz Röhrich
d6bb8fc0d8 Error on backslash in comment #2132
- Report error in case of a backspace in a comment

Backspaces in comments are no good. In most cases they are the result of
commenting out a longer line, that was broken down. This usually results
in the shell treating the following lines as their own commands on their
own lines instead of as parts of the longer, broken down line.
2021-02-14 19:13:29 +01:00
Austin English
2e59eba6eb add support for /bin/busybox sh shebang 2021-02-05 19:56:44 -06:00
Kir Kolyshkin
99e9d5c54b Whitelist podman for SC2016 about '$var'
Same as 08d2eef411 but for podman.

Fixes https://github.com/koalaman/shellcheck/issues/2057

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
2021-01-27 16:21:44 -08:00
freddii
c5756760cb fixed typing mistakes in changelog 2021-01-05 14:08:03 +01:00
Martin Bagge / brother
19355226e1 Change error 2076 to a warning.
Implementing the suggestion by @pixarbuff #1985.
2020-12-27 00:27:36 +01:00
Pepe Iborra
4e7e3f9456 Add Haddock markup to SystemInterface 2020-12-22 09:15:57 +00:00
44 changed files with 1647 additions and 353 deletions

View File

@@ -1,4 +1,4 @@
name: Build Lol
name: Build ShellCheck
# Run this workflow every time a new commit pushed to your repository
on: push
@@ -16,22 +16,25 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Package Source
run: |
mkdir source
cabal sdist
mv dist/*.tar.gz source/source.tar.gz
with:
fetch-depth: 0
- name: Deduce tags
run: |
exec > source/tags
echo "latest"
mkdir source
echo "latest" > source/tags
if tag=$(git describe --exact-match --tags)
then
echo "stable"
echo "$tag"
echo "stable" >> source/tags
echo "$tag" >> source/tags
fi
cat source/tags
- name: Package Source
run: |
grep "stable" source/tags || ./setgitversion
cabal sdist
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v2
@@ -113,6 +116,10 @@ jobs:
export TAGS="$(cat source/tags)"
./.github_deploy
- name: Waiting for GitHub to replicate uploaded releases
run: |
sleep 300
- name: Upload to Docker Hub
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}

View File

@@ -26,4 +26,3 @@ do
done
gh release upload "$tag" "${files[@]}" --clobber || exit 1
done

View File

@@ -1,13 +1,53 @@
## Git
## v0.8.0 - 2021-11-06
### Added
- `disable=all` now conveniently disables all warnings
- `external-sources=true` directive can be added to .shellcheckrc to make
shellcheck behave as if `-x` was specified.
- Optional `check-extra-masked-returns` for pointing out commands with
suppressed exit codes (SC2312).
- Optional `require-double-brackets` for recommending \[\[ ]] (SC2292).
- SC2286-SC2288: Warn when command name ends in a symbol like `/.)'"`
- SC2289: Warn when command name contains tabs or linefeeds
- SC2291: Warn about repeated unquoted spaces between words in echo
- SC2292: Suggest [[ over [ in Bash/Ksh scripts (optional)
- SC2293/SC2294: Warn when calling `eval` with arrays
- SC2295: Warn about "${x#$y}" treating $y as a pattern when not quoted
- SC2296-SC2301: Improved warnings for bad parameter expansions
- SC2302/SC2303: Warn about loops over array values when using them as keys
- SC2304-SC2306: Warn about unquoted globs in expr arguments
- SC2307: Warn about insufficient number of arguments to expr
- SC2308: Suggest other approaches for non-standard expr extensions
- SC2313: Warn about `read` with unquoted, array indexed variable
### Fixed
- SC2102 about repetitions in ranges no longer triggers on [[ -v arr[xx] ]]
- SC2155 now recognizes `typeset` and local read-only `declare` statements
- SC2181 now tries to avoid triggering for error handling functions
- SC2290: Warn about misused = in declare & co, which were not caught by SC2270+
- The flag --color=auto no longer outputs color when TERM is "dumb" or unset
### Changed
- SC2048: Warning about $\* now also applies to ${array[\*]}
- SC2181 now only triggers on single condition tests like `[ $? = 0 ]`.
- Quote warnings are now emitted for declaration utilities in sh
- Leading `_` can now be used to suppress warnings about unused variables
- TTY output now includes warning level in text as well as color
### Removed
- SC1004: Literal backslash+linefeed in '' was found to be usually correct
## v0.7.2 - 2021-04-19
### Added
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
- SC1143: Warn about line continuations in comments
- SC2259/SC2260: Warn when redirections override pipes
- SC2261: Warn about multiple competing redirections
- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit
- SC2264: Warn about wrapper functions that blatantly recurse
- SC2265/SC2266: Warn when using & or | with test statements
- SC2267: Warn when using xargs -i instead of -I
- Optional avoid-x-comparisons: Style warning SC2268 for `[ x$var = xval ]`
- SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]`
### Fixed
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
@@ -21,7 +61,7 @@
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files
- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
- SC2270-SC2285: Improved warnings about misused =, e.g. `${var}=42`
- SC2270-SC2285: Improved warnings about misused `=`, e.g. `${var}=42`
## v0.7.1 - 2020-04-04
@@ -164,7 +204,7 @@
- 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
- SC2196/SC2197: Warn about deprecated egrep/fgrep
- SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
@@ -181,7 +221,7 @@
### 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
- Here doc delimiters 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
@@ -194,7 +234,7 @@
- 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`
- 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 ))`
@@ -365,7 +405,7 @@
### 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`
- 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`
@@ -416,7 +456,7 @@
### Removed
- Suggestions about using parameter expansion over basename
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
## v0.2.0 - 2013-10-27

View File

@@ -681,4 +681,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,4 +1,5 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck)
[![Build Status](https://github.com/koalaman/shellcheck/actions/workflows/build.yml/badge.svg)](https://github.com/koalaman/shellcheck/actions/workflows/build.yml)
# ShellCheck - A shell script static analysis tool
@@ -112,10 +113,6 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux)
Services and platforms with third party plugins:
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).
@@ -143,7 +140,7 @@ On systems with Stack (installs to `~/.local/bin`):
On Debian based distros:
apt-get install shellcheck
sudo apt install shellcheck
On Arch Linux based distros:
@@ -157,8 +154,8 @@ On Gentoo based distros:
On EPEL based distros:
yum -y install epel-release
yum install ShellCheck
sudo yum -y install epel-release
sudo yum install ShellCheck
On Fedora based distros:
@@ -242,6 +239,19 @@ pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
```
### pre-commit
To run ShellCheck via [pre-commit](https://pre-commit.com/), add the hook to your `.pre-commit-config.yaml`:
```
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.7.2
hooks:
- id: shellcheck
# args: ["--severity=warning"] # Optionally only show errors and warnings
```
### Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
@@ -348,6 +358,7 @@ 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
unset var[i] # Array index treated as glob
```
### Conditionals
@@ -366,6 +377,7 @@ ShellCheck can recognize many types of incorrect test statements.
[ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..))
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
```
### Frequently misused commands
@@ -437,6 +449,8 @@ echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count
eval "${array[@]}" # Lost word boundaries in array eval
for i in "${x[@]}"; do ${x[$i]} # Using array value as key
```
### Robustness
@@ -461,6 +475,7 @@ ShellCheck will warn when using features not supported by the shebang. For examp
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
expr match str regex # Unportable alias for `expr str : regex`
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
@@ -481,10 +496,15 @@ 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
!# 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
var2=$var2 # Variable assigned to itself
[ x$var = xval ] # Antiquated x-comparisons
ls() { ls -l "$@"; } # Infinitely recursive wrapper
alias ls='ls -l'; ls foo # Alias used before it takes effect
for x; do for x; do # Nested loop uses same variable
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
```

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.7.1
Version: 0.8.0
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -8,7 +8,7 @@ Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: https://www.shellcheck.net/
Build-Type: Simple
Cabal-Version: >= 1.8
Cabal-Version: 1.18
Bug-reports: https://github.com/koalaman/shellcheck/issues
Description:
The goals of ShellCheck are:
@@ -22,9 +22,11 @@ Description:
* To point out subtle caveats, corner cases and pitfalls, that may cause an
advanced user's otherwise working script to fail under future circumstances.
Extra-Doc-Files:
README.md
CHANGELOG.md
Extra-Source-Files:
-- documentation
README.md
shellcheck.1.md
-- A script to build the man page using pandoc
manpage
@@ -83,6 +85,7 @@ library
ShellCheck.Regex
other-modules:
Paths_ShellCheck
default-language: Haskell98
executable shellcheck
if impl(ghc < 8.0)
@@ -103,6 +106,7 @@ executable shellcheck
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
default-language: Haskell98
main-is: shellcheck.hs
test-suite test-shellcheck
@@ -122,5 +126,5 @@ test-suite test-shellcheck
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
default-language: Haskell98
main-is: test/shellcheck.hs

View File

@@ -22,7 +22,7 @@ RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-in
ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin

View File

@@ -2,7 +2,7 @@
set -xe
{
tar xzv --strip-components=1
./striptests
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS )

View File

@@ -21,7 +21,7 @@ RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-in
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin

View File

@@ -2,7 +2,7 @@
set -xe
{
tar xzv --strip-components=1
./striptests
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )

View File

@@ -51,7 +51,7 @@ RUN pirun apt-get install -y ghc cabal-install
# Finally we can build the current dependencies. This takes hours.
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
RUN pirun cabal update
RUN IFS=";" && pirun cabal install --lib $CABALOPTS Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.1.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.1 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.4 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.6.0
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
WORKDIR /pi/scratch

View File

@@ -3,7 +3,7 @@ set -xe
cd /scratch
{
tar xzv --strip-components=1
./striptests
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
# This script does not cabal update because compiling anything new is slow
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )

View File

@@ -2,7 +2,7 @@
set -xe
{
tar xzv --strip-components=1
./striptests
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )

View File

@@ -5,7 +5,7 @@ ENV TARGETNAME windows.x86_64
# We don't need wine32, even though it complains
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y curl busybox wine
RUN apt-get update && apt-get install -y curl busybox wine winbind
# Fetch Windows version, will be available under z:\haskell
WORKDIR /haskell
@@ -19,8 +19,8 @@ ENV WINEPATH /haskell/bin
# that necessitated this but I don't care enough to find out
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds. This list is just copied from `cabal build`
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
# Precompile some deps to speed up later builds
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
COPY build /usr/bin
WORKDIR /scratch

View File

@@ -6,7 +6,7 @@ cabal() {
set -xe
{
tar xzv --strip-components=1
./striptests
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS )

View File

@@ -2,4 +2,12 @@
# quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling.
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
if [ -z "$path" ]
then
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
exit 1
fi
path="${path%/*}"
exec runghc -isrc -i"$path" shellcheck.hs "$@"

View File

@@ -3,8 +3,17 @@
# This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth.
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
if [ -z "$path" ]
then
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
exit 1
fi
path="${path%/*}"
(
var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
var=$(echo 'main' | ghci -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr)
if [[ $var == *ExitSuccess* ]]
then
exit 0

11
setgitversion Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh -xe
# This script hardcodes the `git describe` version as ShellCheck's version number.
# This is done to allow shellcheck --version to differ from the cabal version when
# building git snapshots.
file="src/ShellCheck/Data.hs"
test -e "$file"
tmp=$(mktemp)
version=$(git describe)
sed -e "s/=.*VERSIONSTRING.*/= \"$version\" -- VERSIONSTRING, DO NOT SUBMIT/" "$file" > "$tmp"
mv "$tmp" "$file"

View File

@@ -112,6 +112,9 @@ 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`.
This option may also be enabled using `external-sources=true` in
`.shellcheckrc`. This flag takes precedence.
**FILES...**
: One or more script files to check, or "-" for standard input.
@@ -234,11 +237,20 @@ Valid keys are:
The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop. A range can be
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
All warnings can be disabled with `disable=all`.
**enable**
: Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered.
**external-sources**
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
arbitrary files from 'source' statements (the way most tools do).
This option defaults to `false` only due to ShellCheck's origin as a
remote service for checking untrusted scripts. It can safely be enabled
for normal development.
**source**
: Overrides the filename included by a `source`/`.` statement. This can be
used to tell shellcheck where to look for a file whose name is determined
@@ -270,6 +282,9 @@ Here is an example `.shellcheckrc`:
source-path=SCRIPTDIR
source-path=/mnt/chroot
# Allow opening any 'source'd file, even if not specified as input
external-sources=true
# Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables
@@ -320,10 +335,32 @@ locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHORS
# KNOWN INCOMPATIBILITIES
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
(If nothing in this section makes sense, you are unlikely to be affected by it)
To avoid confusing and misguided suggestions, ShellCheck requires function
bodies to be either `{ brace groups; }` or `( subshells )`, and function names
containing `[]*=!` are only recognized after a `function` keyword.
The following unconventional function definitions are identical in Bash,
but ShellCheck only recognizes the latter.
[x!=y] () [[ $1 ]]
function [x!=y] () { [[ $1 ]]; }
Shells without the `function` keyword do not allow these characters in function
names to begin with. Function names containing `{}` are not supported at all.
Further, if ShellCheck sees `[x!=y]` it will assume this is an invalid
comparison. To invoke the above function, quote the command as in `'[x!=y]'`,
or to retain the same globbing behavior, use `command [x!=y]`.
ShellCheck imposes additional restrictions on the `[` command to help diagnose
common invalid uses. While `[ $x= 1 ]` is defined in POSIX, ShellCheck will
assume it was intended as the much more likely comparison `[ "$x" = 1 ]` and
fail accordingly. For unconventional or dynamic uses of the `[` command, use
`test` or `\[` instead.
# REPORTING BUGS
@@ -331,9 +368,14 @@ Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues
# AUTHORS
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
# COPYRIGHT
Copyright 2012-2019, Vidar Holen and contributors.
Copyright 2012-2021, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html

View File

@@ -234,7 +234,7 @@ runFormatter sys format options files = do
process :: FilePath -> IO Status
process filename = do
input <- siReadFile sys filename
input <- siReadFile sys Nothing filename
either (reportFailure filename) check input
where
check contents = do
@@ -389,6 +389,7 @@ parseOption flag options =
throwError SyntaxFailure
return (Prelude.read num :: Integer)
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
ioInterface options files = do
inputs <- mapM normalize files
cache <- newIORef emptyCache
@@ -402,14 +403,14 @@ ioInterface options files = do
emptyCache :: Map.Map FilePath String
emptyCache = Map.empty
get cache inputs file = do
get cache inputs rcSuggestsExternal file = do
map <- readIORef cache
case Map.lookup file map of
Just x -> return $ Right x
Nothing -> fetch cache inputs file
Nothing -> fetch cache inputs rcSuggestsExternal file
fetch cache inputs file = do
ok <- allowable inputs file
fetch cache inputs rcSuggestsExternal file = do
ok <- allowable rcSuggestsExternal inputs file
if ok
then (do
(contents, shouldCache) <- inputFile file
@@ -417,13 +418,16 @@ ioInterface options files = do
modifyIORef cache $ Map.insert file contents
return $ Right contents
) `catch` handler
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
else
if rcSuggestsExternal == Just False
then return $ Left (file ++ " was not specified as input, and external files were disabled via directive.")
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where
handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex
allowable inputs x =
if externalSources options
allowable rcSuggestsExternal inputs x =
if fromMaybe (externalSources options) rcSuggestsExternal
then return True
else do
path <- normalize x
@@ -497,7 +501,7 @@ ioInterface options files = do
b <- p x
if b then pure (Just x) else acc
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original =
if isAbsolute original
then
let (_, relative) = splitDrive original
@@ -506,7 +510,7 @@ ioInterface options files = do
find original original
where
find filename deflt = do
sources <- findM ((allowable inputs) `andM` doesFileExist) $
sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
Nothing -> return deflt

View File

@@ -16,12 +16,12 @@ description: |
advanced user's otherwise working script to fail under future
circumstances.
By default ShellCheck can only check non-hidden files under /home, to make
By default ShellCheck can only check non-hidden files under /home, to make
ShellCheck be able to check files under /media and /run/media you must
connect it to the `removable-media` interface manually:
# snap connect shellcheck:removable-media
version: git
base: core18
grade: stable

View File

@@ -150,6 +150,7 @@ data Annotation =
| SourceOverride String
| ShellOverride String
| SourcePath String
| ExternalSources Bool
deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -17,9 +17,11 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.ASTLib where
import ShellCheck.AST
import ShellCheck.Regex
import Control.Monad.Writer
import Control.Monad
@@ -31,6 +33,8 @@ import Data.Maybe
import qualified Data.Map as Map
import Numeric (showHex)
import Test.QuickCheck
arguments (T_SimpleCommand _ _ (cmd:args)) = args
-- Is this a type of loop?
@@ -55,10 +59,28 @@ willSplit x =
T_NormalWord _ l -> any willSplit l
_ -> False
isGlob T_Extglob {} = True
isGlob T_Glob {} = True
isGlob (T_NormalWord _ l) = any isGlob l
isGlob _ = False
isGlob t = case t of
T_Extglob {} -> True
T_Glob {} -> True
T_NormalWord _ l -> any isGlob l || hasSplitRange l
_ -> False
where
-- foo[x${var}y] gets parsed as foo,[,x,$var,y],
-- so check if there's such an interval
hasSplitRange l =
let afterBracket = dropWhile (not . isHalfOpenRange) l
in any isClosingRange afterBracket
isHalfOpenRange t =
case t of
T_Literal _ "[" -> True
_ -> False
isClosingRange t =
case t of
T_Literal _ str -> ']' `elem` str
_ -> False
-- Is this shell word a constant?
isConstant token =
@@ -224,6 +246,39 @@ getOpts (gnu, arbitraryLongOpts) string longopts args = process args
listToArgs = map (\x -> ("", (x, x)))
-- Generic getOpts that doesn't rely on a format string, but may also be inaccurate.
-- This provides a best guess interpretation instead of failing when new options are added.
--
-- "--" is treated as end of arguments
-- "--anything[=foo]" is treated as a long option without argument
-- "-any" is treated as -a -n -y, with the next arg as an option to -y unless it starts with -
-- anything else is an argument
getGenericOpts :: [Token] -> [(String, (Token, Token))]
getGenericOpts = process
where
process (token:rest) =
case getLiteralStringDef "\0" token of
"--" -> map (\c -> ("", (c,c))) rest
'-':'-':word -> (takeWhile (`notElem` "\0=") word, (token, token)) : process rest
'-':optString ->
let opts = takeWhile (/= '\0') optString
in
case rest of
next:_ | "-" `isPrefixOf` getLiteralStringDef "\0" next ->
map (\c -> ([c], (token, token))) opts ++ process rest
next:remainder ->
case reverse opts of
last:initial ->
map (\c -> ([c], (token, token))) (reverse initial)
++ [([last], (token, next))]
++ process remainder
[] -> process remainder
[] -> map (\c -> ([c], (token, token))) opts
_ -> ("", (token, token)) : process rest
process [] = []
-- Is this an expansion of multiple items of an array?
isArrayExpansion (T_DollarBraced _ _ l) =
let string = concat $ oversimplify l in
@@ -232,14 +287,14 @@ isArrayExpansion (T_DollarBraced _ _ l) =
isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
where
f (T_DollarBraced _ _ l) =
f quoted (T_DollarBraced _ _ l) =
let string = concat $ oversimplify l in
"!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
not quoted || "!" `isPrefixOf` string
f quoted (T_DoubleQuoted _ parts) = any (f True) parts
f quoted (T_NormalWord _ parts) = any (f quoted) parts
f _ _ = False
-- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t
@@ -247,7 +302,6 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
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
@@ -672,3 +726,43 @@ isAnnotationIgnoringCode code t =
where
hasNum (DisableComment from to) = code >= from && code < to
hasNum _ = False
prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
-- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
re = mkRegex "/env +(-S|--split-string=?)? *(.*)"
shellFor s | s `matches` re =
case matchRegex re s of
Just [flag, shell] -> fromEnvArgs (words shell)
_ -> ""
shellFor sb =
case words sb of
[] -> ""
[x] -> basename x
(first:second:args) | basename first == "busybox" ->
case basename second of
"sh" -> "ash" -- busybox sh is ash
x -> x
(first:args) | basename first == "env" ->
fromEnvArgs args
(first:_) -> basename first
fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
return []
runTests = $quickCheckAll

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -79,8 +79,12 @@ composeAnalyzers f g x = f x >> g x
data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool,
-- Whether this script has the 'inherit_errexit' option set/default.
hasInheritErrexit :: Bool,
-- Whether this script has 'set -e' anywhere.
hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- A map from Id to parent Token
@@ -142,7 +146,7 @@ producesComments c s = do
prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec
return . not . null $ runChecker params c
return . not . null $ filterByAnnotation spec params $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
@@ -167,6 +171,8 @@ errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m (
errWithFix = addCommentWithFix ErrorC
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
warnWithFix = addCommentWithFix WarningC
infoWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
infoWithFix = addCommentWithFix InfoC
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
styleWithFix = addCommentWithFix StyleC
@@ -178,7 +184,8 @@ makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str
withFix = comment {
tcFix = Just fix
-- If fix is empty, pretend it wasn't there.
tcFix = if null (fixReplacements fix) then Nothing else Just fix
}
in force withFix
@@ -193,7 +200,18 @@ makeParameters spec =
Dash -> False
Sh -> False
Ksh -> True,
hasInheritErrexit =
case shellType params of
Bash -> containsInheritErrexit root
Dash -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> containsPipefail root
Dash -> True
Sh -> True
Ksh -> containsPipefail root,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
@@ -216,18 +234,33 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack.
containsLastpipe root =
containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root
where
isPipefail t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("pipefail" `elem` oversimplify t ||
"o" `elem` map snd (getAllFlags t))
_ -> False
containsShopt shopt root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where
isShoptLastPipe t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" &&
("lastpipe" `elem` oversimplify t)
(shopt `elem` oversimplify t)
_ -> False
-- Does this script mention 'shopt -s inherit_errexit' anywhere?
containsInheritErrexit = containsShopt "inherit_errexit"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack.
containsLastpipe = containsShopt "lastpipe"
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
@@ -240,6 +273,8 @@ prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@@ -253,19 +288,6 @@ determineShell fallbackShell t = fromMaybe Bash $
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash"
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
shellFor s | "/env " `isInfixOf` s = case matchRegex re s of
Just [flag, shell] -> shell
_ -> ""
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
re = mkRegex "/env +(-S|--split-string=?)? *([^ ]*)"
-- 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
@@ -298,14 +320,14 @@ isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t =
isQuoteFreeNode strict shell tree t =
isQuoteFreeElement t ||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
(fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t)
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment {} -> True
T_Assignment {} -> assignmentIsQuoting t
T_FdRedirect {} -> True
_ -> False
@@ -317,7 +339,7 @@ isQuoteFreeNode strict tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Assignment {} -> return $ assignmentIsQuoting t
T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
@@ -329,6 +351,18 @@ isQuoteFreeNode strict tree t =
T_SelectIn {} -> return (not strict)
_ -> Nothing
-- Check whether this assigment is self-quoting due to being a recognized
-- assignment passed to a Declaration Utility. This will soon be required
-- by POSIX: https://austingroupbugs.net/view.php?id=351
assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
shellParsesParamsAsAssignments = shell /= Sh
-- Is this assignment a parameter to a command like export/typeset/etc?
isAssignmentParamToCommand (T_Assignment id _ _ _ _) =
case Map.lookup id tree of
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
_ -> False
-- 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
@@ -493,14 +527,8 @@ getModifiedVariables t =
-- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> []
guard . not . null $ str
TC_Unary id _ "-v" token -> maybeToList $ do
str <- getVariableForTestDashV token
return (t, token, str, DataString SourceChecked)
TC_Unary _ _ "-n" token -> markAsChecked t token
@@ -543,14 +571,12 @@ isClosingFileOp op =
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of
"declare" -> forDeclare
"typeset" -> forDeclare
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
"local" -> if "x" `elem` flags
then concatMap getReference rest
else []
@@ -561,6 +587,13 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
_ -> []
where
forDeclare =
if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = []
@@ -600,8 +633,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> declaredVars
"declare" -> forDeclare
"typeset" -> forDeclare
"local" -> concatMap getModifierParamString rest
"readonly" ->
@@ -613,6 +646,7 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest
"wait" -> maybeToList $ getWaitVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest
@@ -632,6 +666,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t
forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
declaredVars = concatMap (getModifierParam defaultType) rest
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
@@ -670,15 +706,15 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
where
(varName, varType) = case elemIndex '[' var of
Just i -> (take i var, DataArray)
Nothing -> (var, DataString)
f (_:rest) = f rest
f [] = fail "not found"
getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list
getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list
getFlagAssignedVariable str dataSource maybeFlags = do
flags <- maybeFlags
(_, (flag, value)) <- find ((== str) . fst) flags
variableName <- getLiteralStringExt (const $ return "!") value
let (baseName, index) = span (/= '[') variableName
return (base, value, baseName, (if null index then DataString else DataArray) dataSource)
-- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any.
@@ -719,6 +755,20 @@ getIndexReferences s = fromMaybe [] $ do
where
re = mkRegex "(\\[.*\\])"
-- Given a NormalWord like foo or foo[$bar], get foo.
-- Primarily used to get references for [[ -v foo[bar] ]]
getVariableForTestDashV :: Token -> Maybe String
getVariableForTestDashV t = do
str <- takeWhile ('[' /=) <$> getLiteralStringExt toStr t
guard $ isVariableName str
return str
where
-- foo[bar] gets parsed with [bar] as a glob, so undo that
toStr (T_Glob _ s) = return s
-- Turn foo[$x] into foo[\0] so that we can get the constant array name
-- in a non-constant expression (while filtering out foo$x[$y])
toStr _ = return "\0"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
@@ -748,7 +798,7 @@ getReferencedVariables parents t =
TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
if isDereferencingBinaryOp op
then concatMap (getIfReference t) [lhs, rhs]
else []
@@ -777,17 +827,16 @@ getReferencedVariables parents t =
T_Glob _ s -> return s -- Also when parsed as globs
_ -> []
getIfReference context token = do
str@(h:_) <- getLiteralStringExt literalizer token
when (isDigit h) $ fail "is a number"
getIfReference context token = maybeToList $ do
str <- getVariableForTestDashV token
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
@@ -812,6 +861,7 @@ isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
isSpecialVariableChar = (`elem` "*@#?-$!")
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
@@ -857,7 +907,7 @@ getBracedReference s = fromMaybe s $
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) | c `elem` "*@#?-$!" = return [c]
getSpecial (c:_) | isSpecialVariableChar c = return [c]
getSpecial _ = fail "empty or not special"
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
@@ -870,6 +920,8 @@ getBracedReference s = fromMaybe s $
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
getBracedModifier s = headOrDefault "" $ do
let var = getBracedReference s
a <- dropModifier s
@@ -888,6 +940,10 @@ getBracedModifier s = headOrDefault "" $ do
headOrDefault _ (a:_) = a
headOrDefault def _ = def
-- Get the last element or a default. Like `last` but safe.
lastOrDefault def [] = def
lastOrDefault _ list = last list
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
@@ -945,5 +1001,34 @@ isBashLike params =
Dash -> False
Sh -> False
-- Returns whether a token is a parameter expansion without any modifiers.
-- True for $var ${var} $1 $#
-- False for ${#var} ${var[x]} ${var:-0}
isUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ False _ -> True
T_DollarBraced _ _ list ->
let str = concat $ oversimplify list
in getBracedReference str == str
_ -> False
isTrueAssignmentSource c =
case c of
DataString SourceChecked -> False
DataString SourceDeclaration -> False
DataArray SourceChecked -> False
DataArray SourceDeclaration -> False
_ -> True
modifiesVariable params token name =
or $ map check flow
where
flow = getVariableFlow params token
check t =
case t of
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
_ -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2020 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -156,6 +156,11 @@ checkWithIncludesAndSourcePath includes mapper = getErrors
siFindSource = mapper
}
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
(mockRcFile rc $ mockedSystemInterface includes) {
siFindSource = mapper
}
prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 =
@@ -301,6 +306,13 @@ prop_canDisableShebangWarning = null $ result
csScript = "#shellcheck disable=SC2148\nfoo"
}
prop_canDisableAllWarnings = result == [2086]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`"
}
prop_canDisableParseErrors = null $ result
where
result = checkWithSpec [] emptyCheckSpec {
@@ -384,7 +396,7 @@ prop_canEnableOptionalsWithRc = result == [2244]
prop_sourcePathRedirectsName = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f "dir/myscript" _ _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript",
@@ -393,7 +405,7 @@ prop_sourcePathRedirectsName = result == [2086]
prop_sourcePathAddsAnnotation = result == [2086]
where
f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript",
@@ -402,13 +414,75 @@ prop_sourcePathAddsAnnotation = result == [2086]
prop_sourcePathRedirectsDirective = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null"
f "dir/myscript" _ _ "lib" = return "foo/lib"
f _ _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanAllowExternalSources = result == [2086]
where
f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanDenyExternalSources = result == [2086]
where
f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanLeaveExternalSourcesUnspecified = result == [2086]
where
f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCanDisableExternalSources = result == [2006, 2086]
where
f "dir/myscript" (Just True) _ "withExternal" = return "withExternal"
f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec {
csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources = result == [1144]
where
f "dir/myscript" Nothing _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources2 = result == [1144]
where
f "dir/myscript" (Just False) _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
return []
runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -57,7 +57,7 @@ commandChecks :: [CommandCheck]
commandChecks = [
checkTr
,checkFindNameGlob
,checkNeedlessExpr
,checkExpr
,checkGrepRe
,checkTrapQuotes
,checkReturn
@@ -95,7 +95,12 @@ commandChecks = [
,checkSourceArgs
,checkChmodDashr
,checkXargsDashi
,checkUnquotedEchoSpaces
,checkEvalArray
]
++ map checkArgComparison declaringCommands
++ map checkMaskedReturns declaringCommands
optionalChecks = map fst optionalCommandChecks
optionalCommandChecks :: [(CheckDescription, CommandCheck)]
@@ -131,18 +136,30 @@ prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True)
prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" []
prop_checkGenericOptsS1 = checkGetOpts "-f x" ["f"] [] $ return . getGenericOpts
prop_checkGenericOptsS2 = checkGetOpts "-abc x" ["a", "b", "c"] [] $ return . getGenericOpts
prop_checkGenericOptsS3 = checkGetOpts "-abc -x" ["a", "b", "c", "x"] [] $ return . getGenericOpts
prop_checkGenericOptsS4 = checkGetOpts "-x" ["x"] [] $ return . getGenericOpts
-- Long options
prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" []
prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" []
prop_checkGenericOptsL1 = checkGetOpts "--foo=bar" ["foo"] [] $ return . getGenericOpts
prop_checkGenericOptsL2 = checkGetOpts "--foo bar" ["foo"] ["bar"] $ return . getGenericOpts
prop_checkGenericOptsL3 = checkGetOpts "-x --foo" ["x", "foo"] [] $ return . getGenericOpts
-- Know when to terminate
prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" []
prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" []
prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGenericOpts
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty
@@ -235,19 +252,74 @@ checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acc b
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t =
prop_checkExpr = verify checkExpr "foo=$(expr 3 + 2)"
prop_checkExpr2 = verify checkExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkExpr3 = verifyNot checkExpr "foo=$(expr foo : regex)"
prop_checkExpr4 = verifyNot checkExpr "foo=$(expr foo \\< regex)"
prop_checkExpr5 = verify checkExpr "# shellcheck disable=SC2003\nexpr match foo bar"
prop_checkExpr6 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo : fo*"
prop_checkExpr7 = verify checkExpr "# shellcheck disable=SC2003\nexpr 5 -3"
prop_checkExpr8 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr \"$@\""
prop_checkExpr9 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr 5 $rest"
prop_checkExpr10 = verify checkExpr "# shellcheck disable=SC2003\nexpr length \"$var\""
prop_checkExpr11 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo > bar"
prop_checkExpr12 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 | 2"
prop_checkExpr13 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 * 2"
prop_checkExpr14 = verify checkExpr "# shellcheck disable=SC2003\nexpr \"$x\" >= \"$y\""
checkExpr = CommandCheck (Basename "expr") f where
f t = do
when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
case arguments t of
[lhs, op, rhs] -> do
checkOp lhs
case getWordParts op of
[T_Glob _ "*"] ->
err (getId op) 2304
"* must be escaped to multiply: \\*. Modern $((x * y)) avoids this issue."
[T_Literal _ ":"] | isGlob rhs ->
warn (getId rhs) 2305
"Quote regex argument to expr to avoid it expanding as a glob."
_ -> return ()
[single] | not (willSplit single) ->
warn (getId single) 2307
"'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
[first, second] |
(fromMaybe "" $ getLiteralString first) /= "length"
&& not (willSplit first || willSplit second) -> do
checkOp first
warn (getId t) 2307
"'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|."
(first:rest) -> do
checkOp first
forM_ rest $ \t ->
-- We already find 95%+ of multiplication and regex earlier, so don't bother classifying this further.
when (isGlob t) $ warn (getId t) 2306 "Escape glob characters in arguments to expr to avoid pathname expansion."
_ -> return ()
-- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ]
exceptions = [ ":", "<", ">", "<=", ">=",
-- We can offer better suggestions for these
"match", "length", "substr", "index"]
words = mapMaybe getLiteralString
checkOp side =
case getLiteralString side of
Just "match" -> msg "'expr match' has unspecified results. Prefer 'expr str : regex'."
Just "length" -> msg "'expr length' has unspecified results. Prefer ${#var}."
Just "substr" -> msg "'expr substr' has unspecified results. Prefer 'cut' or ${var#???}."
Just "index" -> msg "'expr index' has unspecified results. Prefer x=${var%%[chars]*}; $((${#x}+1))."
_ -> return ()
where
msg = info (getId side) 2308
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
@@ -718,6 +790,7 @@ prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
prop_checkReadExpansions9 = verify checkReadExpansions "read arr[val]"
checkReadExpansions = CommandCheck (Exactly "read") check
where
options = getGnuOpts flagsForRead
@@ -725,13 +798,26 @@ checkReadExpansions = CommandCheck (Exactly "read") check
opts <- options $ arguments cmd
return [y | (x,(_, y)) <- opts, null x || x == "a"]
check cmd = mapM_ warning $ getVars cmd
warning t = sequence_ $ do
check cmd = do
mapM_ dollarWarning $ getVars cmd
mapM_ arrayWarning $ arguments cmd
dollarWarning t = sequence_ $ do
name <- getSingleUnmodifiedBracedString t
guard $ isVariableName name -- e.g. not $1
return . warn (getId t) 2229 $
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
arrayWarning word =
when (any isUnquotedBracket $ getWordParts word) $
warn (getId word) 2313 $
"Quote array indices to avoid them expanding as globs."
isUnquotedBracket t =
case t of
T_Glob _ ('[':_) -> True
_ -> False
-- Return the single variable expansion that makes up this word, if any.
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
getSingleUnmodifiedBracedString :: Token -> Maybe String
@@ -771,6 +857,9 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
prop_checkUnsetGlobs3 = verify checkUnsetGlobs "unset foo[$i]"
prop_checkUnsetGlobs4 = verify checkUnsetGlobs "unset foo[x${i}y]"
prop_checkUnsetGlobs5 = verifyNot checkUnsetGlobs "unset foo]["
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where
check arg =
@@ -870,15 +959,29 @@ prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x
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"
prop_checkWhileGetoptsCase6 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $y in a) foo;; esac; done"
prop_checkWhileGetoptsCase7 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case x$x in xa) foo;; xb) foo;; esac; done"
prop_checkWhileGetoptsCase8 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do x=a; case $x in a) foo;; esac; done"
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
where
f :: Token -> Analysis
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
f t@(T_SimpleCommand _ _ (cmd:arg1:name:_)) = do
path <- getPathM t
params <- ask
sequence_ $ do
options <- getLiteralString arg1
getoptsVar <- getLiteralString name
(T_WhileExpression _ _ body) <- findFirst whileLoop path
caseCmd <- mapMaybe findCase body !!! 0
caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0
-- Make sure getopts name and case variable matches
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
[T_Literal _ caseVar] <- return $ getWordParts bracedWord
guard $ caseVar == getoptsVar
-- Make sure the variable isn't modified
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
f _ = return ()
@@ -1056,7 +1159,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
prop_checkWhich = verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "which") $
\t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."
\t -> info (getId $ getCommandTokenOrThis t) 2230 "'which' is non-standard. Use builtin 'command -v' instead."
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
@@ -1143,5 +1246,145 @@ checkXargsDashi = CommandCheck (Basename "xargs") f
return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
prop_checkArgComparison1 = verify (checkArgComparison "declare") "declare a = b"
prop_checkArgComparison2 = verify (checkArgComparison "declare") "declare a =b"
prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b"
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
where
wordsWithEqual t = mapM_ check $ arguments t
check arg = do
sequence_ $ do
str <- getLeadingUnquotedString arg
case str of
'=':_ ->
return $ err (headId arg) 2290 $
"Remove spaces around = to assign."
'+':'=':_ ->
return $ err (headId arg) 2290 $
"Remove spaces around += to append."
_ -> Nothing
-- 'let' is parsed as a sequence of arithmetic expansions,
-- so we want the additional warning for "x="
when (cmd == "let") $ sequence_ $ do
token <- getTrailingUnquotedLiteral arg
str <- getLiteralString token
guard $ "=" `isSuffixOf` str
return $ err (getId token) 2290 $
"Remove spaces around = to assign."
headId t =
case t of
T_NormalWord _ (x:_) -> getId x
_ -> getId t
prop_checkMaskedReturns1 = verify (checkMaskedReturns "local") "f() { local a=$(false); }"
prop_checkMaskedReturns2 = verify (checkMaskedReturns "declare") "declare a=$(false)"
prop_checkMaskedReturns3 = verify (checkMaskedReturns "declare") "declare a=\"`false`\""
prop_checkMaskedReturns4 = verify (checkMaskedReturns "readonly") "readonly a=$(false)"
prop_checkMaskedReturns5 = verify (checkMaskedReturns "readonly") "readonly a=\"`false`\""
prop_checkMaskedReturns6 = verifyNot (checkMaskedReturns "declare") "declare a; a=$(false)"
prop_checkMaskedReturns7 = verifyNot (checkMaskedReturns "local") "f() { local -r a=$(false); }"
prop_checkMaskedReturns8 = verifyNot (checkMaskedReturns "readonly") "a=$(false); readonly a"
prop_checkMaskedReturns9 = verify (checkMaskedReturns "typeset") "#!/bin/ksh\n f() { typeset -r x=$(false); }"
prop_checkMaskedReturns10 = verifyNot (checkMaskedReturns "typeset") "#!/bin/ksh\n function f { typeset -r x=$(false); }"
prop_checkMaskedReturns11 = verifyNot (checkMaskedReturns "typeset") "#!/bin/bash\n f() { typeset -r x=$(false); }"
prop_checkMaskedReturns12 = verify (checkMaskedReturns "typeset") "typeset -r x=$(false);"
prop_checkMaskedReturns13 = verify (checkMaskedReturns "typeset") "f() { typeset -g x=$(false); }"
prop_checkMaskedReturns14 = verify (checkMaskedReturns "declare") "declare x=${ false; }"
prop_checkMaskedReturns15 = verify (checkMaskedReturns "declare") "f() { declare x=$(false); }"
checkMaskedReturns str = CommandCheck (Exactly str) checkCmd
where
checkCmd t = do
path <- getPathM t
shell <- asks shellType
sequence_ $ do
name <- getCommandName t
let flags = map snd (getAllFlags t)
let hasDashR = "r" `elem` flags
let hasDashG = "g" `elem` flags
let isInScopedFunction = any (isScopedFunction shell) path
let isLocal = not hasDashG && isLocalInFunction name && isInScopedFunction
let isReadOnly = name == "readonly" || hasDashR
-- Don't warn about local variables that are declared readonly,
-- because the workaround `local x; x=$(false); local -r x;` is annoying
guard . not $ isLocal && isReadOnly
return $ mapM_ checkArgs $ arguments t
checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
warn id 2155 "Declare and assign separately to avoid masking return values."
checkArgs _ = return ()
isLocalInFunction = (`elem` ["local", "declare", "typeset"])
isScopedFunction shell t =
case t of
T_BatsTest {} -> True
-- In ksh, only functions declared with 'function' have their own scope
T_Function _ (FunctionKeyword hasFunction) _ _ _ -> shell /= Ksh || hasFunction
_ -> False
hasReturn t = case t of
T_Backticked {} -> True
T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True
_ -> False
prop_checkUnquotedEchoSpaces1 = verify checkUnquotedEchoSpaces "echo foo bar"
prop_checkUnquotedEchoSpaces2 = verifyNot checkUnquotedEchoSpaces "echo foo"
prop_checkUnquotedEchoSpaces3 = verifyNot checkUnquotedEchoSpaces "echo foo bar"
prop_checkUnquotedEchoSpaces4 = verifyNot checkUnquotedEchoSpaces "echo 'foo bar'"
prop_checkUnquotedEchoSpaces5 = verifyNot checkUnquotedEchoSpaces "echo a > myfile.txt b"
prop_checkUnquotedEchoSpaces6 = verifyNot checkUnquotedEchoSpaces " echo foo\\\n bar"
checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
where
check t = do
let args = arguments t
m <- asks tokenPositions
redir <- getClosestCommandM t
sequence_ $ do
let positions = mapMaybe (\c -> Map.lookup (getId c) m) args
let pairs = zip positions (drop 1 positions)
(T_Redirecting _ redirTokens _) <- redir
let redirPositions = mapMaybe (\c -> fst <$> Map.lookup (getId c) m) redirTokens
guard $ any (hasSpacesBetween redirPositions) pairs
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
hasSpacesBetween redirs ((a,b), (c,d)) =
posLine a == posLine d
&& ((posColumn c) - (posColumn b)) >= 4
&& not (any (\x -> b < x && x < c) redirs)
prop_checkEvalArray1 = verify checkEvalArray "eval $@"
prop_checkEvalArray2 = verify checkEvalArray "eval \"${args[@]}\""
prop_checkEvalArray3 = verify checkEvalArray "eval \"${args[@]@Q}\""
prop_checkEvalArray4 = verifyNot checkEvalArray "eval \"${args[*]@Q}\""
prop_checkEvalArray5 = verifyNot checkEvalArray "eval \"$*\""
checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordParts . arguments)
where
check t =
when (isArrayExpansion t) $
if isEscaped t
then style (getId t) 2293 "When eval'ing @Q-quoted words, use * rather than @ as the index."
else warn (getId t) 2294 "eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string)."
isEscaped q =
case q of
-- Match ${arr[@]@Q} and ${@@Q} and such
T_DollarBraced _ _ l -> 'Q' `elem` getBracedModifier (concat $ oversimplify l)
_ -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2016 Vidar Holen
Copyright 2012-2020 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -180,7 +180,7 @@ prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
prop_checkBashisms99 = verifyNot checkBashisms "#!/bin/dash\necho [^f]oo"
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask
kludge params t
@@ -235,7 +235,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
where
file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | not isDash && "[^" `isInfixOf` str =
bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str =

View File

@@ -4,7 +4,7 @@ import ShellCheck.Interface
import Data.Version (showVersion)
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version
shellcheckVersion = showVersion version -- VERSIONSTRING
internalVariables = [
-- Generic
@@ -138,3 +138,5 @@ shellForExecutable name =
_ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]

View File

@@ -48,7 +48,7 @@ outputResults cr sys =
fileGroups = groupWith sourceFile comments
outputGroup group = do
let filename = sourceFile (head group)
result <- (siReadFile sys) filename
result <- siReadFile sys (Just True) filename
let contents = either (const "") id result
outputFile filename contents group

View File

@@ -38,9 +38,6 @@ import System.FilePath
import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter
format options = do
foundIssues <- newIORef False
@@ -90,7 +87,7 @@ reportResult foundIssues reportedIssues color result sys = do
mapM_ output $ M.toList fixmap
where
output (name, fix) = do
file <- (siReadFile sys) name
file <- siReadFile sys (Just True) name
case file of
Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix

View File

@@ -28,6 +28,7 @@ import Data.Array
import Data.List
import System.IO
import System.Info
import System.Environment
-- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter {
@@ -68,12 +69,14 @@ makeNonVirtual comments contents =
shouldOutputColor :: ColorOption -> IO Bool
shouldOutputColor colorOption = do
term <- hIsTerminalDevice stdout
let windows = "mingw" `isPrefixOf` os
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return useColor
shouldOutputColor colorOption =
case colorOption of
ColorAlways -> return True
ColorNever -> return False
ColorAuto -> do
isTerminal <- hIsTerminalDevice stdout
term <- lookupEnv "TERM"
let windows = "mingw" `isPrefixOf` os
let dumbTerm = term `elem` [Just "dumb", Just "", Nothing]
let isUsableTty = isTerminal && not windows && not dumbTerm
return isUsableTty

View File

@@ -43,7 +43,7 @@ outputAll cr sys = mapM_ f groups
f :: [PositionedComment] -> IO ()
f group = do
let filename = sourceFile (head group)
result <- (siReadFile sys) filename
result <- siReadFile sys (Just True) filename
let contents = either (const "") id result
outputResult filename contents group

View File

@@ -117,7 +117,7 @@ collectResult ref cr sys = mapM_ f groups
f :: [PositionedComment] -> IO ()
f group = do
let filename = sourceFile (head group)
result <- siReadFile sys filename
result <- siReadFile sys (Just True) filename
let contents = either (const "") id result
let comments' = makeNonVirtual comments contents
modifyIORef ref (\x -> comments' ++ x)

View File

@@ -121,7 +121,7 @@ outputResult options ref result sys = do
outputForFile color sys comments = do
let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName
result <- siReadFile sys (Just True) fileName
let contents = either (const "") id result
let fileLinesList = lines contents
let lineCount = length fileLinesList
@@ -174,7 +174,7 @@ showFixedString color comments lineNum fileLines =
cuteIndent :: PositionedComment -> String
cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment
where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow =

View File

@@ -73,15 +73,19 @@ import qualified Data.Map as Map
data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String),
-- Given:
-- | Given:
-- What annotations say about including external files (if anything)
-- A resolved filename from siFindSource
-- Read the file or return an error
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
-- | Given:
-- the current script,
-- what annotations say about including external files (if anything)
-- a list of source-path annotations in effect,
-- and a sourced file,
-- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath,
-- Get the configuration file (name, contents) for a filename
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
-- | Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String))
}
@@ -313,11 +317,11 @@ mockedSystemInterface files = SystemInterface {
siGetConfig = const $ return Nothing
}
where
rf file = return $
rf _ file = return $
case find ((== file) . fst) files of
Nothing -> Left "File not included in mock."
Just (_, contents) -> Right contents
fs _ _ file = return file
fs _ _ _ file = return file
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)

View File

@@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@@ -24,7 +24,7 @@
module ShellCheck.Parser (parseScript, runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.ASTLib hiding (runTests)
import ShellCheck.Data
import ShellCheck.Interface
@@ -37,7 +37,7 @@ import Data.Functor
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
import Data.Maybe
import Data.Monoid
import Debug.Trace
import Debug.Trace -- STRIP
import GHC.Exts (sortWith)
import Prelude hiding (readList)
import System.IO
@@ -66,7 +66,7 @@ doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_"
-- Chars to allow in function names
functionChars = variableChars <|> oneOf ":+?-./^@"
functionChars = variableChars <|> oneOf ":+?-./^@,"
-- Chars to allow in functions using the 'function' keyword
extendedFunctionChars = functionChars <|> oneOf "[]*=!"
specialVariable = oneOf (concat specialVariables)
@@ -87,11 +87,23 @@ extglobStart = oneOf extglobStartChars
unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
unicodeSingleQuotes = "\x2018\x2019"
prop_spacing = isOk spacing " \\\n # Comment"
prop_spacing1 = isOk spacing " \\\n # Comment"
prop_spacing2 = isOk spacing "# We can continue lines with \\"
prop_spacing3 = isWarning spacing " \\\n # --verbose=true \\"
spacing = do
x <- many (many1 linewhitespace <|> try (string "\\\n" >> return ""))
x <- many (many1 linewhitespace <|> continuation)
optional readComment
return $ concat x
where
continuation = do
try (string "\\\n")
-- The line was continued. Warn if this next line is a comment with a trailing \
whitespace <- many linewhitespace
optional $ do
x <- readComment
when ("\\" `isSuffixOf` x) $
parseProblem ErrorC 1143 "This backslash is part of a comment and does not continue the line."
return whitespace
spacing1 = do
spacing <- spacing
@@ -972,12 +984,13 @@ prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\n"
readAnnotation = called "shellcheck directive" $ do
try readAnnotationPrefix
many1 linewhitespace
readAnnotationWithoutPrefix
readAnnotationWithoutPrefix True
readAnnotationWithoutPrefix = do
readAnnotationWithoutPrefix sandboxed = do
values <- many1 readKey
optional readAnyComment
void linefeed <|> eof <|> do
@@ -992,8 +1005,12 @@ readAnnotationWithoutPrefix = do
key <- many1 (letter <|> char '-')
char '=' <|> fail "Expected '=' after directive key"
annotations <- case key of
"disable" -> readRange `sepBy` char ','
"disable" -> readElement `sepBy` char ','
where
readElement = readRange <|> readAll
readAll = do
string "all"
return $ DisableComment 0 1000000
readRange = do
from <- readCode
to <- choice [ char '-' *> readCode, return $ from+1 ]
@@ -1023,6 +1040,21 @@ readAnnotationWithoutPrefix = do
"This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell]
"external-sources" -> do
pos <- getPosition
value <- many1 letter
case value of
"true" ->
if sandboxed
then do
parseNoteAt pos ErrorC 1144 "external-sources can only be enabled in .shellcheckrc, not in individual files."
return []
else return [ExternalSources True]
"false" -> return [ExternalSources False]
_ -> do
parseNoteAt pos ErrorC 1145 "Unknown external-sources value. Expected true/false."
return []
_ -> do
parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored."
anyChar `reluctantlyTill` whitespace
@@ -1039,6 +1071,7 @@ readComment = do
unexpecting "shellcheck annotation" readAnnotationPrefix
readAnyComment
prop_readAnyComment = isOk readAnyComment "# Comment"
readAnyComment = do
char '#'
many $ noneOf "\r\n"
@@ -1359,6 +1392,8 @@ prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"
prop_readGlob6 = isOk readGlob "[\\|]"
prop_readGlob7 = isOk readGlob "[^[]"
prop_readGlob8 = isOk readGlob "[*?]"
prop_readGlob9 = isOk readGlob "[!]^]"
prop_readGlob10 = isOk readGlob "[]]"
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
where
readSimple = do
@@ -1366,22 +1401,25 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
c <- oneOf "*?"
id <- endSpan start
return $ T_Glob id [c]
-- Doesn't handle weird things like [^]a] and [$foo]. fixme?
readClass = try $ do
start <- startSpan
char '['
s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars)
negation <- charToString (oneOf "!^") <|> return ""
leadingBracket <- charToString (oneOf "]") <|> return ""
s <- many (predefined <|> readNormalLiteralPart "]" <|> globchars)
guard $ not (null leadingBracket) || not (null s)
char ']'
id <- endSpan start
return $ T_Glob id $ "[" ++ concat s ++ "]"
return $ T_Glob id $ "[" ++ concat (negation:leadingBracket:s) ++ "]"
where
globchars = fmap return . oneOf $ "!$[" ++ extglobStartChars
globchars = charToString $ oneOf $ "![" ++ extglobStartChars
predefined = do
try $ string "[:"
s <- many1 letter
string ":]"
return $ "[:" ++ s ++ ":]"
charToString = fmap return
readGlobbyLiteral = do
start <- startSpan
c <- extglobStart <|> char '['
@@ -1404,6 +1442,8 @@ readNormalEscaped = called "escaped char" $ do
do
next <- quotable <|> oneOf "?*@!+[]{}.,~#"
when (next == ' ') $ checkTrailingSpaces pos <|> return ()
-- Check if this line is followed by a commented line with a trailing backslash
when (next == '\n') $ try . lookAhead $ void spacing
return $ if next == '\n' then "" else [next]
<|>
do
@@ -1471,7 +1511,6 @@ readSingleEscaped = do
case x of
'\'' -> 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]
@@ -1981,12 +2020,14 @@ readHereString = called "here string" $ do
word <- readNormalWord
return $ T_HereString id word
prop_readNewlineList1 = isOk readScript "&> /dev/null echo foo"
readNewlineList =
many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak
where
checkBadBreak = optional $ do
pos <- getPosition
try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or &&
notFollowedBy2 (string "&>") -- Except &> or &>> which is valid
parseProblemAt pos ErrorC 1133
"Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one."
readLineBreak = optional readNewlineList
@@ -2046,6 +2087,7 @@ 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"
prop_readSimpleCommand7b = isOk readSimpleCommand "\\:"
prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol"
prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */"
prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */"
@@ -2153,10 +2195,12 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
if filename == "/dev/null" -- always allow /dev/null
then return (Right "", filename)
else do
allAnnotations <- getCurrentAnnotations True
currentScript <- Mr.asks currentFilename
paths <- mapMaybe getSourcePath <$> getCurrentAnnotations True
resolved <- system $ siFindSource sys currentScript paths filename
contents <- system $ siReadFile sys resolved
let paths = mapMaybe getSourcePath allAnnotations
let externalSources = listToMaybe $ mapMaybe getExternalSources allAnnotations
resolved <- system $ siFindSource sys currentScript externalSources paths filename
contents <- system $ siReadFile sys externalSources resolved
return (contents, resolved)
case input of
Left err -> do
@@ -2190,6 +2234,11 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
SourcePath x -> Just x
_ -> Nothing
getExternalSources t =
case t of
ExternalSources b -> Just b
_ -> Nothing
-- If the word has a single expansion as the directory, try stripping it
-- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file`
stripDynamicPrefix word =
@@ -2306,7 +2355,7 @@ readCmdName = do
-- Ignore alias suppression
optional . try $ do
char '\\'
lookAhead $ variableChars
lookAhead $ variableChars <|> oneOf ":."
readCmdWord
readCmdWord = do
@@ -2792,7 +2841,7 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
startPos <- getPosition
expression <- readStringForParser readCmdWord
let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
subParse newPos readArithmeticContents unQuoted
subParse newPos (readArithmeticContents <* eof) unQuoted
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p =
@@ -3179,7 +3228,7 @@ prop_readConfigKVs4 = isOk readConfigKVs "\n\n\n\n\t \n"
prop_readConfigKVs5 = isOk readConfigKVs "# shellcheck accepts annotation-like comments in rc files\ndisable=1234"
readConfigKVs = do
anySpacingOrComment
annotations <- many (readAnnotationWithoutPrefix <* anySpacingOrComment)
annotations <- many (readAnnotationWithoutPrefix False <* anySpacingOrComment)
eof
return $ concat annotations
anySpacingOrComment =
@@ -3216,8 +3265,8 @@ readScriptFile sourced = do
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
unless ignoreShebang $
verifyShebang pos (getShell shebangString)
if ignoreShebang || isValidShell (getShell shebangString) /= Just False
verifyShebang pos (executableFromShebang shebangString)
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
then do
commands <- withAnnotations annotations readCompoundListOrEmpty
id <- endSpan start
@@ -3231,17 +3280,6 @@ readScriptFile sourced = do
return $ T_Script id shebang []
where
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
getShell sb =
case words sb of
[] -> ""
[x] -> basename x
(first:args) ->
if basename first == "env"
then fromMaybe "" $ find (notElem '=') $ skipFlags args
else basename first
verifyShebang pos s = do
case isValidShell s of
Just True -> return ()
@@ -3334,13 +3372,13 @@ parsesCleanly parser string = runIdentity $ do
-- 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
dump :: Show a => a -> a -- STRIP
dump x = trace (show x) x -- STRIP
-- 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)
dumps :: Show x => x -> a -> a -- STRIP
dumps t = trace (show t) -- STRIP
parseWithNotes parser = do
item <- parser

View File

@@ -2,7 +2,7 @@
# For more information, see: https://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-13.26
resolver: lts-18.15
# Local packages, usually specified by relative directory name
packages:

View File

@@ -29,6 +29,7 @@ detestify() {
state = 0;
}
/STRIP/ { next; }
/LANGUAGE TemplateHaskell/ { next; }
/^import.*Test\./ { next; }
@@ -75,4 +76,3 @@ find . -name '.git' -prune -o -type f -name '*.hs' -print |
do
modify "$file" detestify
done

View File

@@ -29,6 +29,8 @@ cabal build ||
die "build failed"
cabal test ||
die "test failed"
cabal haddock ||
die "haddock failed"
sc="$(find . -name shellcheck -type f -perm -111)"
[ -x "$sc" ] || die "Can't find executable"

View File

@@ -56,19 +56,19 @@ cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Make sure Travis build currently passes: https://travis-ci.org/koalaman/shellcheck
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds, so that all files are
$((i++)). Make sure the Hackage package builds.
Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for Travis to build
$((j++)). Wait for GitHub Actions to build.
$((j++)). Verify release:
a. Check that the new versions are uploaded: https://shellcheck.storage.googleapis.com/index.html
a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md

View File

@@ -64,13 +64,13 @@ ubuntu:latest apt-get update && apt-get install -y cabal-install
haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Other versions we want to support
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
# Ubuntu LTS
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build
ubuntu:18.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
# Stack on Ubuntu LTS
ubuntu:20.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF
exit "$final"

View File

@@ -4,6 +4,7 @@ import Control.Monad
import System.Exit
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.ASTLib
import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
@@ -17,6 +18,7 @@ main = do
results <- sequence [
ShellCheck.Analytics.runTests
,ShellCheck.AnalyzerLib.runTests
,ShellCheck.ASTLib.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.Custom.runTests

View File

@@ -18,10 +18,11 @@ command -v stack ||
stack setup || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
# Nice to haves, but not necessary
for resolver in "${resolvers[@]}"
do
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter."
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter."
done
echo "Success"