267 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
Vidar Holen
670c1de01b Merge pull request #2145 from avdv/fix-2143
Allow `env` to have flags and variables in shebang
2021-02-22 21:00:04 -08:00
Vidar Holen
b9b6975bfa Brand New Build!
Features Linux x86_64 docker builds for all archs
2021-02-22 19:12:57 -08: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
Claudio Bley
8bb5e01401 Allow env to have flags and variables in shebang
The `env` command has a `-S,--split-string` option which enables
having arguments for the command in a shebang.

Also, one could use variable assignments for the command since
`env` treats only the first word without a `=` character as the
command to run.

Fixes #2143.
2021-02-12 10:53:54 +01:00
Austin English
2e59eba6eb add support for /bin/busybox sh shebang 2021-02-05 19:56:44 -06:00
Vidar Holen
15ff87cf80 Merge pull request #2119 from josephcsible/refactors
Various refactorings
2021-02-02 18:14:27 -08: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
Vidar Holen
dff8f9492a Improve SC2283 message and position 2021-01-05 10:07:39 -08:00
freddii
c5756760cb fixed typing mistakes in changelog 2021-01-05 14:08:03 +01:00
Vidar Holen
2e5c56b270 Parse heredocs correctly with carriage returns (fixes #2103) 2020-12-31 13:19:14 -08:00
Vidar Holen
9584266a8b Escape control characters when adding user data to messages 2020-12-31 12:28:48 -08:00
Vidar Holen
5fbaae2bb3 Don't treat ${!x@} as reference of x (fixes #2116) 2020-12-30 20:55:18 -08:00
Vidar Holen
fbb14d6b38 Improve checks for = in command names (fixes #2102) 2020-12-30 20:30:43 -08:00
Joseph C. Sible
2cfd1f2714 Fuse maps 2020-12-28 18:13:34 -05:00
Joseph C. Sible
953d9bc56d Remove unused helper stub 2020-12-28 18:13:34 -05:00
Joseph C. Sible
e272fa04ee Remove redundant bind and return 2020-12-28 18:13:34 -05:00
Joseph C. Sible
81e84c2939 Use execState instead of snd . runState 2020-12-28 18:13:34 -05:00
Joseph C. Sible
34939ca0b7 Fuse map into any 2020-12-28 18:13:34 -05:00
Joseph C. Sible
e7820479f0 Use find 2020-12-28 18:13:34 -05:00
Joseph C. Sible
8480563672 Use syntactic sugar instead of building lists by hand 2020-12-28 18:13:34 -05:00
Joseph C. Sible
dfbcc9595e Use mapM instead of reimplementing it 2020-12-28 17:48:58 -05:00
Joseph C. Sible
2c0766825e Implement groupByLink in terms of foldr 2020-12-28 17:45:11 -05:00
Joseph C. Sible
cb4f4e7edc Use mapM_ instead of reimplementing it 2020-12-28 17:34:52 -05:00
Joseph C. Sible
0607039d41 Simplify actualArgs 2020-12-28 17:21:47 -05:00
Joseph C. Sible
46f177b5be Simplify parseArgs 2020-12-28 17:19:08 -05:00
Joseph C. Sible
eaccd3d02c Simplify parser 2020-12-28 17:19:08 -05:00
Joseph C. Sible
35033a9f2f Remove unnecessary use of Maybe from shellFor 2020-12-28 17:09:50 -05: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
Vidar Holen
bd3299edd3 Treat 'exec $1' like '$1' for the purpose of quoting (fixes #2068) 2020-12-17 20:31:45 -08:00
Vidar Holen
cc3884cf9f Support env -S/--split-string in shebangs (fixes #2105) 2020-12-12 20:24:32 -08:00
Vidar Holen
6ba1af0898 Warn when a variable is assigned to itself 2020-12-11 20:28:36 -08:00
Vidar Holen
8e332ce879 Improve handling of trailing tokens for []/compounds (fixes #2091) 2020-12-06 21:26:24 -08:00
Vidar Holen
7e40d97e7a Merge pull request #1857 from lukelbd/conda-install-instructions
Add conda install instructions
2020-12-05 20:35:20 -08:00
Vidar Holen
775c0c11d7 Merge pull request #1899 from ArturKlauser/simplify-prepare-deploy
Simplify .prepare-deploy
2020-12-05 20:19:16 -08:00
Vidar Holen
5196ab1f95 Merge pull request #2097 from ylluminarious/patch-1
Add MacPorts as installation option in README.md
2020-12-05 20:14:46 -08:00
Vidar Holen
b625562d60 Add POSIX checks for more Bash-specific variables (fixes #2093) 2020-12-05 20:11:12 -08:00
George Plymale II
18e80284ec add macports as installation option in README.md 2020-12-01 16:15:22 -05:00
Vidar Holen
65044c2568 SC2095: Also warn if the command is backgrounded 2020-11-29 13:01:23 -08:00
Vidar Holen
61b7dd610d Merge pull request #2077 from keith/ks/readonly-masking
Add readonly to SC2155
2020-11-13 17:38:43 -08:00
Artur Klauser
4b0e5ca119 Simplify .prepare-deploy
Reduce amount of duplicated code.
2020-11-12 20:02:31 -08:00
Keith Smiley
619662adb6 Add readonly to SC2155
This adds a warning for readonly masking the return value of function.
This is mentioned in the wiki
https://github.com/koalaman/shellcheck/wiki/SC2155#problematic-code-in-the-case-of-readonly
but didn't actually produce a warning.

Fixes https://github.com/koalaman/shellcheck/issues/1336
2020-10-23 17:29:04 -07:00
Vidar Holen
28d3279ba6 Optional style warning about [ x$var = xval ] 2020-10-19 20:04:58 -07:00
Vidar Holen
256457c47a Use getopts parser to find 'read' arrays (fixes #2073) 2020-10-18 22:57:16 -07:00
Vidar Holen
3104cec770 SC2267: Warn about xargs -i (fixes #2058) 2020-10-18 22:10:14 -07:00
Vidar Holen
f100c2939e Rewrite getopts style option parser 2020-10-18 21:34:58 -07:00
Vidar Holen
8d99926554 Recognize local -x similarly to export (fixes #2069) 2020-10-18 15:15:31 -07:00
Vidar Holen
218deb6d01 Update SC2091/SC2092 message and ignore in quotes. 2020-09-08 19:30:13 -07:00
Vidar Holen
c4cc2debb7 Improve compatibility checks 2020-09-07 21:05:49 -07:00
Vidar Holen
cfd68ee0c2 Give each sh/dash compatibility warning its own SC3xxx error code 2020-09-01 16:48:14 -07:00
Vidar Holen
58783ab3cc Allow specifying ranges in disable directives 2020-09-01 16:22:15 -07:00
Vidar Holen
43191fa71d Suppress SC2035 for echo * and printf * (fixes #2036) 2020-09-01 14:19:28 -07:00
Vidar Holen
c9be7ab2eb Parse assignments according to spec (fixes #2022) 2020-08-23 18:46:13 -07:00
Vidar Holen
fb89cdf4ad Merge pull request #2042 from sshine/patch-1
Fix whitespace in README.md
2020-08-23 15:57:57 -07:00
Vidar Holen
9e59bcca91 Upgrade SC2169 (unsupported in dash) from warning to error (fixes #2013) 2020-08-23 15:49:20 -07:00
Vidar Holen
a62d9f10c2 Warn when using &/| between test statements 2020-08-23 15:43:33 -07:00
Simon Shine
e72fbb2640 Fix whitespace in README.md 2020-08-20 13:07:32 +02:00
Vidar Holen
17e591233f Merge branch 'donnerpeter-supportMinusNZ' 2020-08-08 15:02:07 -07:00
Vidar Holen
50067ddf94 Consider variables in -z/-n tests to be checked 2020-08-08 12:32:20 -07:00
Vidar Holen
3fa5b7d3bd Merge branch 'supportMinusNZ' of https://github.com/donnerpeter/shellcheck into donnerpeter-supportMinusNZ 2020-08-08 11:22:00 -07:00
Vidar Holen
5e6d50f493 Merge branch 'Gandalf--issue_1759_mapfile_proc_substition' 2020-08-07 16:41:52 -07:00
Vidar Holen
e779aedac3 Modernize getting mapfile array name 2020-08-07 16:41:18 -07:00
Vidar Holen
3ef1175566 Merge branch 'issue_1759_mapfile_proc_substition' of https://github.com/Gandalf-/shellcheck into Gandalf--issue_1759_mapfile_proc_substition 2020-08-07 15:57:59 -07:00
Vidar Holen
506ffa849b Merge branch 'glenjamin-patch-1' 2020-08-07 15:24:45 -07:00
Vidar Holen
b864242caa Merge branch 'patch-1' of https://github.com/glenjamin/shellcheck into glenjamin-patch-1 2020-08-07 15:24:36 -07:00
Vidar Holen
3e50a2fce8 Suppress SC2216 for du --files0-from or --exclude-from (fixes #1286) 2020-08-07 14:59:34 -07:00
Vidar Holen
10c2d827fa Merge pull request #2028 from Lin-Buo-Ren/github-issue-1643
Fix snap distribution unable to process scripts in Unicode(Chinese) (fixes #1643)
2020-08-07 11:35:50 -07:00
Vidar Holen
e0e2edd525 Merge pull request #2031 from umanwizard/patch-1
Update README.md
2020-08-07 11:34:36 -07:00
Brennan Vincent
c5b6d6f027 Update README.md
Minor typographical fixes ("macOS" everywhere, capitalize Homebrew)
2020-08-05 10:50:14 -04:00
林博仁(Buo-ren, Lin)
beee9b22ca Fix snap distribution unable to process scripts in Unicode(Chinese) (fixes #1643)
The snap runtime only supports C.UTF-8 locale, as other locales don't seem to be used now just hardcode it.

Fixes #1643.

Signed-off-by: 林博仁(Buo-ren, Lin) <Buo.Ren.Lin@gmail.com>
2020-08-03 11:46:20 +08:00
Vidar Holen
1ac2c31728 Warn when shell functions blatantly recurse (fixes #1994) 2020-07-27 21:50:33 -07:00
Vidar Holen
cc81bdee31 Improve SC1033/SC1034 message 2020-07-27 18:44:07 -07:00
Vidar Holen
34885142e7 Handle tilde expansion in pattern matching (fixes #1769) 2020-07-27 18:34:42 -07:00
Vidar Holen
14e6806092 Handle literal linefeeds in printf format strings (fixes #2007) 2020-07-25 17:36:22 -07:00
Vidar Holen
5d753212fb Improve handling of command prefixes like exec/command (fixes #2008) 2020-07-25 13:45:05 -07:00
Vidar Holen
5b86777f9d Warn about non-POSIX case modification expansions (fixes #1977) 2020-07-22 17:32:00 -07:00
Vidar Holen
7a9dbc042b Re-enable Windows job 2020-07-05 20:46:22 -07:00
Vidar Holen
9793d94206 Remove trailing whitespace 2020-07-05 20:30:18 -07:00
Vidar Holen
baab5b53e0 Use TravisCI workspaces 2020-07-01 21:21:38 -07:00
Vidar Holen
210cdcd01a Treat $x/ or $(x)/ as ./ when finding sourced files (fixes #1998) 2020-06-28 17:24:07 -07:00
Vidar Holen
1b884a17ea Merge pull request #2002 from stdedos/patch-1
Autolink https://www.shellcheck.net/
2020-06-28 16:05:24 -07:00
Vidar Holen
b52f58473d Merge pull request #1999 from aureliojargas/patch-1
SC1102: Fix typo in error message: substition
2020-06-28 16:04:56 -07:00
Vidar Holen
376e78b631 Merge branch 'onnozweers-patch-1' 2020-06-28 16:03:56 -07:00
Vidar Holen
40aacc3345 Merge branch 'patch-1' of https://github.com/onnozweers/shellcheck into onnozweers-patch-1 2020-06-28 16:03:44 -07:00
Vidar Holen
739eaadbf5 Warn about extra spaces between ((s in for((;;)) 2020-06-28 16:01:15 -07:00
Stavros Ntentos
6b88a341f3 Autolink https://www.shellcheck.net/ 2020-06-28 01:47:02 +03:00
Aurelio Jargas
a61d8a232c SC1102: Fix typo in error message: substition 2020-06-26 02:13:33 +02:00
Vidar Holen
12d9c1b76d Clarify that SC1090 refers to ShellCheck, not sh 2020-06-24 11:50:27 -07:00
Onno Zweers
a2b5b6a500 Rephrase: *Shellcheck* can't follow non-constant source
The message "Can't follow non-constant source. Use a directive to specify location." set me off on the wrong foot. At first, I thought it meant "In bash, you can't source a script specified by a variable." It was only after reading the wiki page for this message https://github.com/koalaman/shellcheck/wiki/SC1090 that I understood that the problem is that *shellcheck* can't check the sourced file. So I would suggest to rephrase this message so that it is more clear that the problem is in the checking, not in the running of the script.
2020-06-03 11:37:53 +02:00
Vidar Holen
5cf2c00ff7 Warn about defining and using an alias in a single command (fixes #1807) 2020-05-25 23:24:33 -07:00
Vidar Holen
a08ad3bee9 Count $# as an argument reference in SC2120 2020-05-25 23:24:33 -07:00
Vidar Holen
417e13f129 Merge pull request #1950 from geeseven/aur-shellcheck-bin
update dependency free AUR package
2020-05-25 18:06:35 -07:00
geeseven
536cb584f4 update dependency free AUR package
The `shellcheck-static` AUR package has been removed and `shellcheck-bin` is an option for a dependency free AUR package.
2020-05-12 14:58:15 -05:00
Vidar Holen
c2a15ce8e9 Allow disabling SC1072/SC1073 with annotations (fixes #1931) 2020-05-03 21:57:16 -07:00
Vidar Holen
d6adbfde78 Improve SC2259/60/61 messages 2020-05-03 21:46:16 -07:00
Vidar Holen
2030b83607 Warn about duplicate uses of stdin/out/err 2020-05-03 11:54:25 -07:00
Vidar Holen
8aa40c43ed Merge pull request #1926 from scop/spelling
Spelling fixes
2020-05-02 18:39:44 -07:00
Vidar Holen
5a42f4b938 Merge pull request #1927 from scop/sc-prefix
Use SC prefix for disable= in man page
2020-04-25 16:37:55 -07:00
Vidar Holen
a7a406c43c Merge pull request #1925 from josephcsible/nofromright
Revert "Use fromRight instead of reimplementing it"
2020-04-25 16:37:22 -07:00
Ville Skyttä
1d126960f3 Use SC prefix for disable= in man page 2020-04-25 08:33:10 +03:00
Ville Skyttä
60e80e4ce1 Spelling fixes 2020-04-25 08:29:38 +03:00
Joseph C. Sible
e0daa936d2 Revert "Use fromRight instead of reimplementing it"
We still support GHC 8.0, which didn't have fromRight.

This reverts commit 64c31d9142.
2020-04-24 22:14:08 -04:00
Vidar Holen
75863a887e Merge pull request #1918 from josephcsible/getsuspiciousregexwildcard
Clean up and optimize getSuspiciousRegexWildcard
2020-04-12 15:32:00 -07:00
Vidar Holen
413f0048b8 Merge pull request #1917 from josephcsible/thenskip
Simplify thenSkip, and use in another location
2020-04-12 15:26:36 -07:00
Vidar Holen
e7b5fb9742 Merge pull request #1907 from josephcsible/formatters
Clean up formatters
2020-04-12 15:23:17 -07:00
Vidar Holen
30523555af Merge pull request #1906 from josephcsible/shellsupport
Simplify ShellSupport
2020-04-12 15:22:52 -07:00
Vidar Holen
58d3e50f43 Merge pull request #1905 from josephcsible/skiprepeating
Make skipRepeating lazier and faster
2020-04-12 15:21:39 -07:00
Vidar Holen
73cc11fd0a Merge pull request #1901 from josephcsible/bracedstring
Mostly get rid of bracedString
2020-04-12 15:14:50 -07:00
Joseph C. Sible
163c710ba7 Clean up and optimize getSuspiciousRegexWildcard 2020-04-12 16:15:45 -04:00
Vidar Holen
ab1610b004 Merge pull request #1903 from josephcsible/fixer
Only perform the comparisons once
2020-04-11 17:23:51 -07:00
Vidar Holen
148468be70 Merge pull request #1904 from josephcsible/commands
Simplify Commands
2020-04-11 17:23:39 -07:00
Vidar Holen
5eac721fcf Merge pull request #1902 from josephcsible/astlib
Clean up ASTLib
2020-04-11 17:23:08 -07:00
Joseph C. Sible
b58bb4ba9d Move bracedString to be local to its last use site 2020-04-11 19:24:11 -04:00
Joseph C. Sible
999b7e2596 Get rid of bracedString everywhere it's easy to 2020-04-11 19:24:11 -04:00
Joseph C. Sible
a9d564a8bc Combine bracedString into getSingleUnmodifiedVariable
Everywhere we used getSingleUnmodifiedVariable, we just called bracedString on
the result. Move this into that function instead, and rename it accordingly.
2020-04-11 19:23:13 -04:00
Joseph C. Sible
8a7497c4f0 Simplify checkVariableBraces 2020-04-11 19:23:13 -04:00
Vidar Holen
1eac0d7340 Merge pull request #1900 from josephcsible/analyzerlib
Clean up AnalyzerLib
2020-04-11 16:21:20 -07:00
Vidar Holen
f8c1ffb0dc Merge pull request #1898 from josephcsible/nameexpansion
Simplify nameExpansion
2020-04-11 16:18:54 -07:00
Joseph C. Sible
3e17a20965 Simplify thenSkip, and use in another location 2020-04-11 17:29:28 -04:00
Joseph C. Sible
1c6202dba4 Avoid some awkward parentheses with forM_ 2020-04-05 22:25:19 -04:00
Joseph C. Sible
64c31d9142 Use fromRight instead of reimplementing it 2020-04-05 22:23:22 -04:00
Joseph C. Sible
8a6679fd8a Remove unnecessary fromMaybe and when from bashism 2020-04-05 22:03:50 -04:00
Joseph C. Sible
facf0d1e27 Write getLiteralArgs with foldr and without fromMaybe or monads 2020-04-05 21:59:27 -04:00
Joseph C. Sible
cd38afce26 Make it slightly lazier still (and more clear) 2020-04-05 21:46:08 -04:00
Joseph C. Sible
5084ba8d7e Make skipRepeating lazier and faster 2020-04-05 21:39:14 -04:00
Joseph C. Sible
ed331b816b Simplify warnRedundant 2020-04-05 20:32:39 -04:00
Joseph C. Sible
cfa2a663af Simplify checkSetAssignment 2020-04-05 20:30:37 -04:00
Joseph C. Sible
df4928f4e3 Use MultiWayIf instead of case-matching on () 2020-04-05 20:30:37 -04:00
Joseph C. Sible
9747b1d5c3 Simplify checkArg 2020-04-05 20:10:56 -04:00
Joseph C. Sible
fa841cb270 Prefer pattern matching in undirected 2020-04-05 20:08:02 -04:00
Joseph C. Sible
e8501151dd Use a guard instead of unless 2020-04-05 20:04:54 -04:00
Joseph C. Sible
9027a9239f Use pattern matching instead of snd 2020-04-05 20:03:17 -04:00
Joseph C. Sible
773e98868d Use foldr in checkFindNameGlob 2020-04-05 19:53:40 -04:00
Joseph C. Sible
d45ab327b0 Only perform the comparisons once 2020-04-05 19:45:28 -04:00
Joseph C. Sible
0f9b0f18a4 Remove unnecessary cases from wordToPseudoGlob 2020-04-05 19:30:21 -04:00
Joseph C. Sible
322842b57e Remove unnecessary monadicity from wordToPseudoGlob 2020-04-05 19:29:40 -04:00
Joseph C. Sible
b6cff5ea0e Simplify getAssociativeArrays 2020-04-05 19:06:30 -04:00
Joseph C. Sible
8f105074fe Simplify getCommandNameAndToken 2020-04-05 19:01:56 -04:00
Joseph C. Sible
d22e0aa4a7 Simplify process
Note to self: This is a lot like foldr or traverse, and would be trivial to
implement as such if it didn't need to peek ahead when takesArg is true. I
wonder if there's a clean way to implement it in terms of one of them anyway.
2020-04-05 16:45:45 -04:00
Joseph C. Sible
fb55072302 Implement supportsArrays with pattern-matching 2020-04-05 16:30:59 -04:00
Joseph C. Sible
0cc5ed4563 Don't bother with asks if you're just immediately binding the result anyway 2020-04-05 16:25:43 -04:00
Joseph C. Sible
ca41440a67 Simplify getSpecial 2020-04-05 16:21:07 -04:00
Joseph C. Sible
1cf0aa25e9 Simplify dropPrefix 2020-04-05 16:19:18 -04:00
Joseph C. Sible
4604066c37 Use head instead of (!! 0) 2020-04-05 16:16:12 -04:00
Joseph C. Sible
2ebf522a52 Simplify isArrayFlag 2020-04-05 16:13:55 -04:00
Joseph C. Sible
e4eb2d157f Remove an unnecessary operator section 2020-04-05 16:13:55 -04:00
Joseph C. Sible
f109f9ab92 Remove unnecessary as-patterns 2020-04-05 16:13:55 -04:00
Joseph C. Sible
67e091674e Remove unnecessary maybeToList
The functions we use here are polymorphic enough to work in the [] monad,
so there's no point to use them in the Maybe monad and then convert.
2020-04-05 16:13:54 -04:00
Joseph C. Sible
f833ee3d5a Use a list comprehension instead of a concatMap with extra lists 2020-04-05 15:54:12 -04:00
Joseph C. Sible
f55d8c45e5 Simplify causesSubshell 2020-04-05 15:54:12 -04:00
Joseph C. Sible
14ee462ccd Use execState instead of reimplementing it 2020-04-05 15:50:42 -04:00
Joseph C. Sible
b3c04ce3d0 Implement findFirst in terms of foldr 2020-04-05 15:50:42 -04:00
Joseph C. Sible
b0dbc79f69 Remove unnecessary Maybe from isQuoteFreeElement 2020-04-05 15:07:36 -04:00
Joseph C. Sible
2a8170ba05 Use force instead of reimplementing it 2020-04-05 15:01:57 -04:00
Vidar Holen
01f4423465 Disable SC2257 about > $((i=42)) for Dash 2020-04-05 11:38:22 -07:00
Joseph C. Sible
d2fa88dd91 Simplify nameExpansion 2020-04-05 14:04:23 -04:00
Vidar Holen
a30e42ab05 Filter GitHub uploads by tag 2020-04-04 19:30:13 -07:00
Vidar Holen
84d6e53659 Update Changelog with new version 2020-04-04 19:29:49 -07:00
Luke Davis
00574dd1fc Add conda install instructions 2020-03-03 13:23:24 -07:00
Peter Gromov
a82e606e8d Don't trigger SC2154 (unassigned var) in -n/-z expressions #1583 2020-01-31 14:49:25 +01:00
Gandalf-
fdd02c94c0 Issue 1759 mapfile and process substition
https://github.com/koalaman/shellcheck/issues/1759

When a simple process substition is used, this tripped up
the getMapfileArray function by making the last argument
not a variable
2019-12-22 23:19:03 -08:00
Glen Mailer
9423691039 Mention the CircleCI shellcheck orb in the README. 2019-08-12 22:24:47 +01:00
61 changed files with 3511 additions and 1055 deletions

View File

@@ -1,73 +0,0 @@
#!/bin/bash
build_linux() {
# Linux Docker image
name="$DOCKER_BASE"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
docker build -t "$name:current" .
docker run "$name:current" --version
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
id=$(docker create "$name:current")
docker cp "$id:/bin/shellcheck" "shellcheck"
docker rm "$id"
ls -l shellcheck
./shellcheck myscript
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
done
}
build_aarch64() {
# Linux aarch64 static executable
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
done
}
build_armv6hf() {
# Linux armv6hf static executable
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
done
}
build_windows() {
# Windows .exe
docker run -v "$PWD:/appdata" koalaman/winghc cuib
for tag in $TAGS
do
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
done
}
build_osx() {
# Darwin x86_64 executable
brew update
brew install cabal-install pandoc gnu-tar
sudo ln -sf /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
sudo ln -sf /usr/local/bin/gtar /usr/local/bin/tar
export PATH="/usr/local/bin:$PATH"
cabal update
cabal install --dependencies-only
cabal build shellcheck
# Cabal 3 no longer has a predictable output path
path="$(find . -name 'shellcheck' -type f -perm +111)"
[[ -e "$path" ]]
for tag in $TAGS
do
cp "$path" "deploy/shellcheck-$tag.darwin-x86_64";
done
}

View File

@@ -2,10 +2,10 @@
- Rule Id (if any, e.g. SC1000): - Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"): - My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) - [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit - [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
#### For new checks and feature suggestions #### For new checks and feature suggestions
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this - [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related - [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related

131
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,131 @@
name: Build ShellCheck
# Run this workflow every time a new commit pushed to your repository
on: push
jobs:
package_source:
name: Package Source Code
runs-on: ubuntu-latest
steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-mark manual ghc # Don't bother installing ghc just to tar up source
sudo apt-get install cabal-install
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Deduce tags
run: |
mkdir source
echo "latest" > source/tags
if tag=$(git describe --exact-match --tags)
then
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
with:
name: source
path: source/
build_source:
name: Build Source Code
needs: package_source
strategy:
matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v2
- name: Build source
run: |
mkdir -p bin
mkdir -p bin/${{matrix.build}}
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: bin
path: bin/
package_binary:
name: Package Binaries
needs: build_source
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v2
- name: Work around GitHub permissions bug
run: chmod +x bin/*/shellcheck*
- name: Package binaries
run: |
export TAGS="$(cat source/tags)"
mkdir -p deploy
cp -r bin/* deploy
cd deploy
../.prepare_deploy
rm -rf */ README* LICENSE*
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: deploy
path: deploy/
deploy:
name: Deploy binaries
needs: package_binary
runs-on: ubuntu-latest
environment: Deploy
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v2
- name: Upload to GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
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 }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }}
DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }}/shellcheck
run: |
export TAGS="$(cat source/tags)"
( source ./.multi_arch_docker && set -eux && multi_arch_docker::main )

View File

@@ -2,39 +2,10 @@
set -x set -x
shopt -s extglob shopt -s extglob
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
then
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping GitHub deployment."
exit 0
fi
install_deps() {
version="2.7.0" # 2.14.1 fails to overwrite duplicates
case "$(uname)" in
Linux)
sudo apt-get update
sudo apt-get install curl
curl -L "https://github.com/github/hub/releases/download/v$version/hub-linux-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-linux-amd64-$version/bin/hub"
;;
Darwin)
curl -L "https://github.com/github/hub/releases/download/v$version/hub-darwin-amd64-$version.tgz" | tar xvz --strip-components=1 "hub-darwin-amd64-$version/bin/hub"
;;
*)
echo "Unknown: $(uname)"
exit 1
;;
esac
hub_path="$PWD/bin/hub"
hub() {
"$hub_path" "$@"
}
}
install_deps
export EDITOR="touch" export EDITOR="touch"
# Sanity check # Sanity check
gh --version || exit 1
hub release show latest || exit 1 hub release show latest || exit 1
for tag in $TAGS for tag in $TAGS
@@ -50,8 +21,8 @@ do
for file in deploy/* for file in deploy/*
do do
[[ $file == *.@(xz|gz|zip) ]] || continue [[ $file == *.@(xz|gz|zip) ]] || continue
files+=(-a "$file") [[ $file == *"$tag"* ]] || continue
files+=("$file")
done done
hub release edit "${files[@]}" "$tag" || exit 1 gh release upload "$tag" "${files[@]}" --clobber || exit 1
done done

View File

@@ -1,12 +1,6 @@
#!/bin/bash #!/bin/bash
# This script builds and deploys multi-architecture docker images from the # This script builds and deploys multi-architecture docker images from the
# binaries previously built and deployed to GCS by the Travis pipeline. # binaries previously built and deployed to GitHub.
if [[ "$TRAVIS_SECURE_ENV_VARS" != "true" ]]
then
echo >&2 "Missing TRAVIS_SECURE_ENV_VARS. Skipping Docker builds."
exit 0
fi
function multi_arch_docker::install_docker_buildx() { function multi_arch_docker::install_docker_buildx() {
# Install up-to-date version of docker, with buildx support. # Install up-to-date version of docker, with buildx support.
@@ -108,6 +102,5 @@ function multi_arch_docker::main() {
multi_arch_docker::install_docker_buildx multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub multi_arch_docker::login_to_docker_hub
multi_arch_docker::build_and_push_all multi_arch_docker::build_and_push_all
set +x
multi_arch_docker::test_all multi_arch_docker::test_all
} }

View File

@@ -1,8 +1,9 @@
#!/bin/bash #!/bin/bash
# This script packages up Travis compiled binaries # This script packages up compiled binaries
set -ex set -ex
shopt -s nullglob shopt -s nullglob extglob
cd deploy
ls -l
cp ../LICENSE LICENSE.txt cp ../LICENSE LICENSE.txt
sed -e $'s/$/\r/' > README.txt << END sed -e $'s/$/\r/' > README.txt << END
@@ -22,44 +23,32 @@ This binary was compiled on $(date -u).
$(git log -n 3) $(git log -n 3)
END END
for file in ./*.exe for dir in */
do do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file" cp LICENSE.txt README.txt "$dir"
done done
for file in *.linux-x86_64 echo "Tags are $TAGS"
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-aarch64 for tag in $TAGS
do do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.aarch64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-armv6hf for dir in windows.*/
do do
base="${file%.*}" ( cd "$dir" && zip "../shellcheck-$tag.zip" * )
cp "$file" "shellcheck" done
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.darwin-x86_64 for dir in {linux,darwin}.*/
do do
base="${file%.*}" base="${dir%/}"
cp "$file" "shellcheck" ( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * )
tar -cJf "$base.darwin.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck done
rm "shellcheck"
done done
for file in ./* for file in ./*
do do
[[ -f "$file" ]] || continue
sha512sum "$file" > "$file.sha512sum" sha512sum "$file" > "$file.sha512sum"
done done
ls -l

View File

@@ -1,64 +0,0 @@
language: shell
os: linux
services:
- docker
jobs:
include:
- stage: Build
# This must weirdly not have a dash, otherwise an empty job is created
env: BUILD=linux
- env: BUILD=windows
- env: BUILD=armv6hf
- env: BUILD=aarch64
- env: BUILD=osx
os: osx
- stage: Deploy docker image
# Deploy only for pushes to master branch, not other branches, not PRs.
if: type = push
script:
- source ./.multi_arch_docker
- set -ex; multi_arch_docker::main; set +x
# This is in global context and runs for every stage that doesn't override it.
before_install: |
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
DOCKER_BUILDS=""
export TAGS=""
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
echo "Tags are $TAGS"
# This is in global context and runs for every stage that doesn't override it.
script:
- mkdir -p deploy
- source ./.compile_binaries
- ./striptests
- set -ex; build_"$BUILD"; set +x;
- ./.prepare_deploy
- ./.github_deploy
# This is in global context and runs for every stage that doesn't override it.
after_failure: |
id
pwd
df -h
find . -name '*.log' -type f -exec grep "" /dev/null {} +
find . -ls
# This is in global context and runs for every stage that doesn't override it.
deploy:
provider: gcs
skip_cleanup: true
access_key_id: GOOG7MDN7WEH6IIGBDCA
secret_access_key:
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
bucket: shellcheck-private
local_dir: deploy
on:
repo: koalaman/shellcheck
condition: $TRAVIS_BUILD_STAGE_NAME = Build
all_branches: true

View File

@@ -1,3 +1,69 @@
## 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
- SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]`
### Fixed
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
is still purely cosmetic and does not allow ShellCheck to continue.
- Improved error reporting for trailing tokens after ]/]] and compound commands
- `#!/usr/bin/env -S shell` is now handled correctly
- Here docs with \r are now parsed correctly and give better warnings
### Changed
- Assignments are now parsed to spec, without leniency for leading $ or spaces
- 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`
## v0.7.1 - 2020-04-04 ## v0.7.1 - 2020-04-04
### Fixed ### Fixed
- `-f diff` no longer claims that it found more issues when it didn't - `-f diff` no longer claims that it found more issues when it didn't
@@ -138,7 +204,7 @@
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )` - SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[ - SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays 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 - SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements - SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables - SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
@@ -155,7 +221,7 @@
### Fixed ### Fixed
- `-c` no longer suggested when using `grep -o | wc` - `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives - 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` - 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 - `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
- `grep -F` now suppresses regex related suggestions - `grep -F` now suppresses regex related suggestions

View File

@@ -1,29 +0,0 @@
# Build-only image
FROM ubuntu:18.04 AS build
USER root
WORKDIR /opt/shellCheck
# Install OS deps
RUN apt-get update && apt-get install -y ghc cabal-install
# Install Haskell deps
# (This is a separate copy/run so that source changes don't require rebuilding)
COPY ShellCheck.cabal ./
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
# Copy source and build it
COPY LICENSE shellcheck.hs ./
COPY src src
RUN cabal build Paths_ShellCheck && \
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
strip --strip-all shellcheck
RUN mkdir -p /out/bin && \
cp shellcheck /out/bin/
# Resulting ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=build /out /
ENTRYPOINT ["/bin/shellcheck"]

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 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 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 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 # ShellCheck - A shell script static analysis tool
@@ -109,11 +110,8 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/) * [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/) * [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/) * [Code Factor](https://www.codefactor.io/)
* [Github](https://github.com/features/actions)(only Linux) * [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 Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@@ -142,13 +140,13 @@ On systems with Stack (installs to `~/.local/bin`):
On Debian based distros: On Debian based distros:
apt-get install shellcheck sudo apt install shellcheck
On Arch Linux based distros: On Arch Linux based distros:
pacman -S shellcheck pacman -S shellcheck
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR. or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
On Gentoo based distros: On Gentoo based distros:
@@ -156,8 +154,8 @@ On Gentoo based distros:
On EPEL based distros: On EPEL based distros:
yum -y install epel-release sudo yum -y install epel-release
yum install ShellCheck sudo yum install ShellCheck
On Fedora based distros: On Fedora based distros:
@@ -167,10 +165,14 @@ On FreeBSD:
pkg install hs-ShellCheck pkg install hs-ShellCheck
On OS X with homebrew: On macOS (OS X) with Homebrew:
brew install shellcheck brew install shellcheck
Or with MacPorts:
sudo port install shellcheck
On OpenBSD: On OpenBSD:
pkg_add shellcheck pkg_add shellcheck
@@ -197,6 +199,10 @@ Or Windows (via [scoop](http://scoop.sh)):
C:\> scoop install shellcheck C:\> scoop install shellcheck
``` ```
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
conda install -c conda-forge shellcheck
From Snap Store: From Snap Store:
snap install --channel=edge shellcheck snap install --channel=edge shellcheck
@@ -220,7 +226,7 @@ Alternatively, you can download pre-compiled binaries for the latest release her
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
@@ -233,6 +239,19 @@ pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1 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
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
@@ -264,7 +283,7 @@ This section describes how to build ShellCheck from a source directory. ShellChe
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`). ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source. On macOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
$ brew install cabal-install $ brew install cabal-install
@@ -339,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 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
unset var[i] # Array index treated as glob
``` ```
### Conditionals ### Conditionals
@@ -357,6 +377,7 @@ ShellCheck can recognize many types of incorrect test statements.
[ grep -q foo file ] # Command without $(..) [ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed [[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..)) (( 1 -lt 2 )) # Using test operators in ((..))
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
``` ```
### Frequently misused commands ### Frequently misused commands
@@ -428,6 +449,8 @@ echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count 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 ### Robustness
@@ -452,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..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not 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 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 trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files read foo < /dev/tcp/host/22 # Unportable intercepted files
@@ -472,10 +496,15 @@ rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \ echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment 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 echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input 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 while getopts "a" f; do case $f in "b") # Unhandled getopts flags
``` ```

View File

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

13
build/README.md Normal file
View File

@@ -0,0 +1,13 @@
This directory contains Dockerfiles for all builds.
A build image will:
* Run on Linux x86\_64 with vanilla Docker (no exceptions)
* Not contain any software that would restrict easy modification or copying
* Take a `cabal sdist` style tar.gz of the ShellCheck directory on stdin
* Output a tar.gz of artifacts on stdout, in a directory named for the arch
This makes it simple to build any release without exotic hardware or software.
An image can be built and tagged using `build_builder`,
and run on a source tarball using `run_builder`.

12
build/build_builder Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
if [ $# -eq 0 ]
then
echo >&2 "No build image directories specified"
echo >&2 "Example: $0 build/*/"
exit 1
fi
for dir
do
( cd "$dir" && docker build -t "$(cat tag)" . ) || exit 1
done

View File

@@ -0,0 +1,31 @@
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
FROM liushuyu/osxcross:latest
ENV TARGET x86_64-apple-darwin18
ENV TARGETNAME darwin.x86_64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

14
build/darwin.x86_64/build Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -Sx "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
} >&2
tar czv "$TARGETNAME"

1
build/darwin.x86_64/tag Normal file
View File

@@ -0,0 +1 @@
koalaman/scbuilder-darwin-x86_64

View File

@@ -0,0 +1,30 @@
FROM ubuntu:20.04
ENV TARGET aarch64-linux-gnu
ENV TARGETNAME linux.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
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 --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

15
build/linux.aarch64/build Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
qemu-aarch64-static "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.aarch64/tag Normal file
View File

@@ -0,0 +1 @@
koalaman/scbuilder-linux-aarch64

View File

@@ -0,0 +1,59 @@
# I've again spent days trying to get a working armv6hf compiler going.
# God only knows how many recompilations of GCC, GHC, libraries, and
# ShellCheck itself, has gone into it.
#
# I tried Debian's toolchain. I tried my custom one built according to
# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but
# nothing has yielded an armv6hf binary that does not immediately
# segfault on qemu-arm-static or the RPi itself.
#
# I then tried the same but with armv7hf. Same story.
#
# Emulating the entire userspace with balenalib again? Very strange build
# failures where programs would fail to execute with > ~100 arguments.
#
# Finally, creating our own appears to work when using a custom QEmu
# patched to follow execve calls.
#
# PS: $100 bounty for getting a RPi1 compatible static build going
# with cross-compilation, similar to what the aarch64 build does.
#
FROM ubuntu:20.04
ENV TARGETNAME linux.armv6hf
# Build QEmu with execve follow support
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev
WORKDIR /build
RUN git clone --depth 1 https://github.com/koalaman/qemu
RUN cd qemu && ./configure --static && cd build && ninja qemu-arm
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
ENV QEMU_EXECVE 1
# Set up an armv6 userspace
WORKDIR /
RUN apt-get install -y debootstrap qemu-user-static
# We expect this to fail if the host doesn't have binfmt qemu support
RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ]
RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static
RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun
# If the debootstrap process didn't finish, continue it
RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage
# Install deps in the chroot
RUN pirun apt-get update
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 --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
WORKDIR /pi/scratch
COPY build /pi/usr/bin
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]

16
build/linux.armv6hf/build Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -xe
cd /scratch
{
tar xzv --strip-components=1
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 )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
strip -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.armv6hf/tag Normal file
View File

@@ -0,0 +1 @@
koalaman/scbuilder-linux-armv6hf

View File

@@ -0,0 +1,26 @@
FROM ubuntu:20.04
ENV TARGETNAME linux.x86_64
# Install GHC and cabal
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc curl xz-utils
# So we'd like a later version of Cabal that supports --enable-executable-static,
# but we can't use Ubuntu 20.10 because coreutils has switched to new syscalls that
# the TravisCI kernel doesn't support. Download it manually.
RUN curl "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/bin
# Use ld.bfd instead of ld.gold due to
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
# relocation refers to local symbol "" [2], which is defined in a discarded section
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections"
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

15
build/linux.x86_64/build Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
strip -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

1
build/linux.x86_64/tag Normal file
View File

@@ -0,0 +1 @@
koalaman/scbuilder-linux-x86_64

30
build/run_builder Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
if [ $# -lt 2 ]
then
echo >&2 "This script builds a source archive (as produced by cabal sdist)"
echo >&2 "Usage: $0 sourcefile.tar.gz builddir..."
exit 1
fi
file=$(realpath "$1")
shift
if [ ! -e "$file" ]
then
echo >&2 "$file does not exist"
exit 1
fi
set -ex -o pipefail
for dir
do
tagfile="$dir/tag"
if [ ! -e "$tagfile" ]
then
echo >&2 "$tagfile does not exist"
exit 2
fi
docker run -i "$(< "$tagfile")" < "$file" | tar xz
done

View File

@@ -0,0 +1,27 @@
FROM ubuntu:20.04
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 winbind
# Fetch Windows version, will be available under z:\haskell
WORKDIR /haskell
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-7.75.0/curl-7.75.0-win64-mingw.zip" | busybox unzip - && mv curl-7.75.0-win64-mingw/bin/* .
ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue
# 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
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
ENTRYPOINT ["/usr/bin/build"]

19
build/windows.x86_64/build Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
cabal() {
wine /haskell/bin/cabal.exe "$@"
}
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS )
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
wine "/haskell/mingw/bin/strip.exe" -s "$TARGETNAME/shellcheck.exe"
ls -l "$TARGETNAME"
wine "$TARGETNAME/shellcheck.exe" --version
} >&2
tar czv "$TARGETNAME"

1
build/windows.x86_64/tag Normal file
View File

@@ -0,0 +1 @@
koalaman/scbuilder-windows-x86_64

View File

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

View File

@@ -2,4 +2,12 @@
# quickrun runs ShellCheck in an interpreted mode. # quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling. # 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. # This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth. # '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* ]] if [[ $var == *ExitSuccess* ]]
then then
exit 0 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 line (plus `/dev/null`). This option allows following any file the script
may `source`. may `source`.
This option may also be enabled using `external-sources=true` in
`.shellcheckrc`. This flag takes precedence.
**FILES...** **FILES...**
: One or more script files to check, or "-" for standard input. : One or more script files to check, or "-" for standard input.
@@ -232,12 +235,22 @@ Valid keys are:
**disable** **disable**
: Disables a comma separated list of error codes for the following command. : Disables a comma separated list of error codes for the following command.
The command can be a simple command like `echo foo`, or a compound command The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop. 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**
: Enable an optional check by name, as listed with **--list-optional**. : Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered. 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** **source**
: Overrides the filename included by a `source`/`.` statement. This can be : 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 used to tell shellcheck where to look for a file whose name is determined
@@ -254,7 +267,7 @@ Valid keys are:
**shell** **shell**
: Overrides the shell detected from the shebang. This is useful for : Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'. as a more targeted alternative to 'disable=SC2039'.
# RC FILES # RC FILES
@@ -269,6 +282,9 @@ Here is an example `.shellcheckrc`:
source-path=SCRIPTDIR source-path=SCRIPTDIR
source-path=/mnt/chroot 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 # Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables enable=quote-safe-variables
@@ -319,10 +335,32 @@ locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)` Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`. 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 (If nothing in this section makes sense, you are unlikely to be affected by it)
long list of wonderful contributors.
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 # REPORTING BUGS
@@ -330,9 +368,14 @@ Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues 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
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, Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html see https://gnu.org/licenses/gpl.html

View File

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

View File

@@ -31,6 +31,8 @@ apps:
shellcheck: shellcheck:
command: usr/bin/shellcheck command: usr/bin/shellcheck
plugs: [home, removable-media] plugs: [home, removable-media]
environment:
LANG: C.UTF-8
parts: parts:
shellcheck: shellcheck:

View File

@@ -145,11 +145,12 @@ data InnerToken t =
deriving (Show, Eq, Functor, Foldable, Traversable) deriving (Show, Eq, Functor, Foldable, Traversable)
data Annotation = data Annotation =
DisableComment Integer DisableComment Integer Integer -- [from, to)
| EnableComment String | EnableComment String
| SourceOverride String | SourceOverride String
| ShellOverride String | ShellOverride String
| SourcePath String | SourcePath String
| ExternalSources Bool
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket 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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,9 +17,11 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.ASTLib where module ShellCheck.ASTLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.Regex
import Control.Monad.Writer import Control.Monad.Writer
import Control.Monad import Control.Monad
@@ -28,6 +30,12 @@ import Data.Functor
import Data.Functor.Identity import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe 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? -- Is this a type of loop?
isLoop t = case t of isLoop t = case t of
@@ -51,10 +59,28 @@ willSplit x =
T_NormalWord _ l -> any willSplit l T_NormalWord _ l -> any willSplit l
_ -> False _ -> False
isGlob T_Extglob {} = True isGlob t = case t of
isGlob T_Glob {} = True T_Extglob {} -> True
isGlob (T_NormalWord _ l) = any isGlob l T_Glob {} -> True
isGlob _ = False 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? -- Is this shell word a constant?
isConstant token = isConstant token =
@@ -134,26 +160,141 @@ isUnquotedFlag token = fromMaybe False $ do
str <- getLeadingUnquotedString token str <- getLeadingUnquotedString token
return $ "-" `isPrefixOf` str return $ "-" `isPrefixOf` str
-- Given a T_DollarBraced, return a simplified version of the string contents. -- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l -- -re -d : -u 3 bar
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)" -- into
-- Just [("r", (-re, -re)), ("e", (-re, -re)), ("d", (-d,:)), ("u", (-u,3)), ("", (bar,bar))]
--
-- Each string flag maps to a tuple of (flag, argument), where argument=flag if it
-- doesn't take a specific one.
--
-- Any unrecognized flag will result in Nothing. The exception is if arbitraryLongOpts
-- is set, in which case --anything will map to "anything".
getGnuOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getGnuOpts str args = getOpts (True, False) str [] args
-- As above, except the first non-arg string will treat the rest as arguments
getBsdOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getBsdOpts str args = getOpts (False, False) str [] args
-- Tests for this are in Commands.hs where it's more frequently used
getOpts ::
-- Behavioral config: gnu style, allow arbitrary long options
(Bool, Bool)
-- A getopts style string
-> String
-- List of long options and whether they take arguments
-> [(String, Bool)]
-- List of arguments (excluding command)
-> [Token]
-- List of flags to tuple of (optionToken, valueToken)
-> Maybe [(String, (Token, Token))]
getOpts (gnu, arbitraryLongOpts) string longopts args = process args
where
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = longopts
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process (token:rest) = do
case getLiteralStringDef "\0" token of
"--" -> return $ listToArgs rest
'-':'-':word -> do
let (name, arg) = span (/= '=') word
needsArg <-
if arbitraryLongOpts
then return $ Map.findWithDefault False name flagMap
else Map.lookup name flagMap
if needsArg && null arg
then
case rest of
(arg:rest2) -> do
more <- process rest2
return $ (name, (token, arg)) : more
_ -> fail "Missing arg"
else do
more <- process rest
-- Consider splitting up token to get arg
return $ (name, (token, token)) : more
'-':opts -> shortToOpts opts token rest
arg ->
if gnu
then do
more <- process rest
return $ ("", (token, token)):more
else return $ listToArgs (token:rest)
shortToOpts opts token args =
case opts of
c:rest -> do
needsArg <- Map.lookup [c] flagMap
case () of
_ | needsArg && null rest -> do
(next:restArgs) <- return args
more <- process restArgs
return $ ([c], (token, next)):more
_ | needsArg -> do
more <- process args
return $ ([c], (token, token)):more
_ -> do
more <- shortToOpts rest token args
return $ ([c], (token, token)):more
[] -> 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? -- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _ _) = isArrayExpansion (T_DollarBraced _ _ l) =
let string = bracedString t in let string = concat $ oversimplify l in
"@" `isPrefixOf` string || "@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
isArrayExpansion _ = False isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args? -- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
where where
f t@(T_DollarBraced _ _ _) = f quoted (T_DollarBraced _ _ l) =
let string = bracedString t in let string = concat $ oversimplify l in
"!" `isPrefixOf` string not quoted || "!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts f quoted (T_DoubleQuoted _ parts) = any (f True) parts
f (T_NormalWord _ parts) = any f parts f quoted (T_NormalWord _ parts) = any (f quoted) parts
f _ = False f _ _ = False
-- Is it certain that this word will becomes multiple words? -- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t willBecomeMultipleArgs t = willConcatInAssignment t || f t
@@ -161,7 +302,6 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
f T_Extglob {} = True f T_Extglob {} = True
f T_Glob {} = True f T_Glob {} = True
f T_BraceExpansion {} = True f T_BraceExpansion {} = True
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts f (T_NormalWord _ parts) = any f parts
f _ = False f _ = False
@@ -193,6 +333,12 @@ getUnquotedLiteral (T_NormalWord _ list) =
str _ = Nothing str _ = Nothing
getUnquotedLiteral _ = Nothing getUnquotedLiteral _ = Nothing
isQuotes t =
case t of
T_DoubleQuoted {} -> True
T_SingleQuoted {} -> True
_ -> False
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS -- Get the last unquoted T_Literal in a word like "${var}foo"THIS
-- or nothing if the word does not end in an unquoted literal. -- or nothing if the word does not end in an unquoted literal.
getTrailingUnquotedLiteral :: Token -> Maybe Token getTrailingUnquotedLiteral :: Token -> Maybe Token
@@ -211,8 +357,11 @@ getTrailingUnquotedLiteral t =
getLeadingUnquotedString :: Token -> Maybe String getLeadingUnquotedString :: Token -> Maybe String
getLeadingUnquotedString t = getLeadingUnquotedString t =
case t of case t of
T_NormalWord _ ((T_Literal _ s) : _) -> return s T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
_ -> Nothing _ -> Nothing
where
from ((T_Literal _ s):rest) = s ++ from rest
from _ = ""
-- Maybe get the literal string of this token and any globs in it. -- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f getGlobOrLiteralString = getLiteralStringExt f
@@ -273,6 +422,37 @@ getLiteralStringExt more = g
-- Is this token a string literal? -- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t isLiteral t = isJust $ getLiteralString t
-- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage
escapeForMessage :: String -> String
escapeForMessage str = concatMap f str
where
f '\\' = "\\\\"
f '\n' = "\\n"
f '\r' = "\\r"
f '\t' = "\\t"
f '\x1B' = "\\e"
f c =
if shouldEscape c
then
if ord c < 256
then "\\x" ++ (pad0 2 $ toHex c)
else "\\U" ++ (pad0 4 $ toHex c)
else [c]
shouldEscape c =
(not $ isPrint c)
|| (not (isAscii c) && not (isLetter c))
pad0 :: Int -> String -> String
pad0 n s =
let l = length s in
if l < n
then (replicate (n-l) '0') ++ s
else s
toHex :: Char -> String
toHex c = map toUpper $ showHex (ord c) ""
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] -- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
getWordParts (T_NormalWord _ l) = concatMap getWordParts l getWordParts (T_NormalWord _ l) = concatMap getWordParts l
@@ -301,7 +481,7 @@ getCommand t =
-- Maybe get the command name string of a token representing a command -- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken getCommandName = fst . getCommandNameAndToken False
-- Maybe get the name+arguments of a command. -- Maybe get the name+arguments of a command.
getCommandArgv t = do getCommandArgv t = do
@@ -311,20 +491,38 @@ getCommandArgv t = do
-- Get the command name token from a command, i.e. -- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'. -- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token. -- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken getCommandTokenOrThis = snd . getCommandNameAndToken False
getCommandNameAndToken :: Token -> (Maybe String, Token) -- Given a command, get the string and token that represents the command name.
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do -- If direct, return the actual command (e.g. exec in 'exec ls')
(T_SimpleCommand _ _ (w:rest)) <- getCommand t -- If not, return the logical command (e.g. 'ls' in 'exec ls')
getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s return $ fromMaybe (Just s, w) $ do
then guard $ not direct
case rest of actual <- getEffectiveCommandToken s cmd rest
(applet:_) -> return (getLiteralString applet, applet) return (getLiteralString actual, actual)
_ -> return (Just s, w) where
else getEffectiveCommandToken str cmd args =
return (Just s, w) let
firstArg = do
arg <- listToMaybe args
guard . not $ isFlag arg
return arg
in
case str of
"busybox" -> firstArg
"builtin" -> firstArg
"command" -> firstArg
"run" -> firstArg -- Used by bats
"exec" -> do
opts <- getBsdOpts "cla:" args
(_, (t, _)) <- find (null . fst) opts
return t
_ -> fail ""
-- If a command substitution is a single command, get its name. -- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date" -- $(date +%s) = Just "date"
@@ -341,8 +539,8 @@ getCommandNameFromExpansion t =
-- Get the basename of a token representing a command -- Get the basename of a token representing a command
getCommandBasename = fmap basename . getCommandName getCommandBasename = fmap basename . getCommandName
where
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
isAssignment t = isAssignment t =
case t of case t of
@@ -400,10 +598,10 @@ getAssociativeArrays t =
f t@T_SimpleCommand {} = sequence_ $ do f t@T_SimpleCommand {} = sequence_ $ do
name <- getCommandName t name <- getCommandName t
let assocNames = ["declare","local","typeset"] let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames guard $ name `elem` assocNames
let flags = getAllFlags t let flags = getAllFlags t
guard $ elem "A" $ map snd flags guard $ "A" `elem` map snd flags
let args = map fst . filter ((==) "" . snd) $ flags let args = [arg | (arg, "") <- flags]
let names = mapMaybe (getLiteralStringExt nameAssignments) args let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names return $ tell names
f _ = return () f _ = return ()
@@ -421,38 +619,36 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
-- Turn a word into a PG pattern, replacing all unknown/runtime values with -- Turn a word into a PG pattern, replacing all unknown/runtime values with
-- PGMany. -- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob] wordToPseudoGlob :: Token -> [PseudoGlob]
wordToPseudoGlob word = wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_DollarBraced {} -> return [PGMany]
T_DollarExpansion {} -> return [PGMany]
T_Backticked {} -> return [PGMany]
T_Glob _ "?" -> return [PGAny]
T_Glob _ ('[':_) -> return [PGAny]
T_Glob {} -> return [PGMany]
T_Extglob {} -> return [PGMany]
_ -> return [PGMany]
-- Turn a word into a PG pattern, but only if we can preserve -- Turn a word into a PG pattern, but only if we can preserve
-- exact semantics. -- exact semantics.
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob] wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToExactPseudoGlob word = wordToExactPseudoGlob = wordToPseudoGlob' True
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
wordToPseudoGlob' exact word =
simplifyPseudoGlob <$> toGlob word
where where
toGlob :: Token -> Maybe [PseudoGlob]
toGlob word =
case word of
T_NormalWord _ (T_Literal _ ('~':str):rest) -> do
guard $ not exact
let this = (PGMany : (map PGChar $ dropWhile (/= '/') str))
tail <- concat <$> (mapM f $ concatMap getWordParts rest)
return $ this ++ tail
_ -> concat <$> (mapM f $ getWordParts word)
f x = case x of f x = case x of
T_Literal _ s -> return $ map PGChar s T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s T_SingleQuoted _ s -> return $ map PGChar s
T_Glob _ "?" -> return [PGAny] T_Glob _ "?" -> return [PGAny]
T_Glob _ "*" -> return [PGMany] T_Glob _ "*" -> return [PGMany]
_ -> fail "Unknown token type" T_Glob _ ('[':_) | not exact -> return [PGAny]
_ -> if exact then fail "" else return [PGMany]
-- Reorder a PseudoGlob for more efficient matching, e.g. -- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g -- f?*?**g -> f??*g
@@ -502,8 +698,7 @@ pseudoGlobIsSuperSetof = matchable
matchable (PGMany : rest) [] = matchable rest [] matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False matchable _ _ = False
wordsCanBeEqual x y = fromMaybe True $ wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
-- Is this an expansion that can be quoted, -- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})? -- e.g. $(foo) `foo` $foo (but not {foo,})?
@@ -517,6 +712,11 @@ isCommandSubstitution t = case t of
T_Backticked {} -> True T_Backticked {} -> True
_ -> False _ -> False
-- Is this an expansion that results in a simple string?
isStringExpansion t = isCommandSubstitution t || case t of
T_DollarArithmetic {} -> True
T_DollarBraced {} -> not (isArrayExpansion t)
_ -> False
-- Is this a T_Annotation that ignores a specific code? -- Is this a T_Annotation that ignores a specific code?
isAnnotationIgnoringCode code t = isAnnotationIgnoringCode code t =
@@ -524,5 +724,45 @@ isAnnotationIgnoringCode code t =
T_Annotation _ anns _ -> any hasNum anns T_Annotation _ anns _ -> any hasNum anns
_ -> False _ -> False
where where
hasNum (DisableComment ts) = code == ts hasNum (DisableComment from to) = code >= from && code < to
hasNum _ = False 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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -79,8 +79,12 @@ composeAnalyzers f g x = f x >> g x
data Parameters = Parameters { data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default. -- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool, hasLastpipe :: Bool,
-- Whether this script has the 'inherit_errexit' option set/default.
hasInheritErrexit :: Bool,
-- Whether this script has 'set -e' anywhere. -- Whether this script has 'set -e' anywhere.
hasSetE :: Bool, hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- A linear (bad) analysis of data flow -- A linear (bad) analysis of data flow
variableFlow :: [StackData], variableFlow :: [StackData],
-- A map from Id to parent Token -- A map from Id to parent Token
@@ -142,7 +146,7 @@ producesComments c s = do
prRoot pr prRoot pr
let spec = defaultSpec pr let spec = defaultSpec pr
let params = makeParameters spec 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 -> String -> TokenComment
makeComment severity id code note = makeComment severity id code note =
@@ -163,8 +167,12 @@ err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str style id code str = addComment $ makeComment StyleC id code str
errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
errWithFix = addCommentWithFix ErrorC
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
warnWithFix = addCommentWithFix WarningC 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 :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
styleWithFix = addCommentWithFix StyleC styleWithFix = addCommentWithFix StyleC
@@ -176,9 +184,10 @@ makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix = makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str let comment = makeComment severity id code str
withFix = comment { 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 withFix `deepseq` withFix in force withFix
makeParameters spec = makeParameters spec =
let params = Parameters { let params = Parameters {
@@ -191,7 +200,18 @@ makeParameters spec =
Dash -> False Dash -> False
Sh -> False Sh -> False
Ksh -> True, 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), shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root,
@@ -214,18 +234,33 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False _ -> False
re = mkRegex "[[:space:]]-[^-]*e" re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere? containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root
-- Also used as a hack. where
containsLastpipe root = 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 isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where where
isShoptLastPipe t = isShoptLastPipe t =
case t of case t of
T_SimpleCommand {} -> T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" && t `isUnqualifiedCommand` "shopt" &&
("lastpipe" `elem` oversimplify t) (shopt `elem` oversimplify t)
_ -> False _ -> 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_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
@@ -236,6 +271,10 @@ prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh 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 = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@@ -249,22 +288,11 @@ determineShell fallbackShell t = fromMaybe Bash $
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations] headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s 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 = headOrDefault "" (drop 1 $ words s)
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
-- Given a root node, make a map from Id to parent Token. -- Given a root node, make a map from Id to parent Token.
-- This is used to populate parentMap in Parameters -- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token getParentTree :: Token -> Map.Map Id Token
getParentTree t = getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty) snd $ execState (doStackAnalysis pre post t) ([], Map.empty)
where where
pre t = modify (first ((:) t)) pre t = modify (first ((:) t))
post t = do post t = do
@@ -292,16 +320,16 @@ isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFree = isQuoteFreeNode False isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t = isQuoteFreeNode strict shell tree t =
(isQuoteFreeElement t == Just True) || isQuoteFreeElement t ||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t)) (fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t)
where where
-- Is this node self-quoting in itself? -- Is this node self-quoting in itself?
isQuoteFreeElement t = isQuoteFreeElement t =
case t of case t of
T_Assignment {} -> return True T_Assignment {} -> assignmentIsQuoting t
T_FdRedirect {} -> return True T_FdRedirect {} -> True
_ -> Nothing _ -> False
-- Are any subnodes inherently self-quoting? -- Are any subnodes inherently self-quoting?
isQuoteFreeContext t = isQuoteFreeContext t =
@@ -311,7 +339,7 @@ isQuoteFreeNode strict tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True TA_Sequence {} -> return True
T_Arithmetic {} -> return True T_Arithmetic {} -> return True
T_Assignment {} -> return True T_Assignment {} -> return $ assignmentIsQuoting t
T_Redirecting {} -> return False T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True T_DollarDoubleQuoted _ _ -> return True
@@ -323,6 +351,18 @@ isQuoteFreeNode strict tree t =
T_SelectIn {} -> return (not strict) T_SelectIn {} -> return (not strict)
_ -> Nothing _ -> 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: -- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t -- Example: isParamTo (parentMap params) "sed" t
isParamTo :: Map.Map Id Token -> String -> Token -> Bool isParamTo :: Map.Map Id Token -> String -> Token -> Bool
@@ -354,8 +394,8 @@ getClosestCommand tree t =
-- Like above, if koala_man knew Haskell when starting this project. -- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do getClosestCommandM t = do
tree <- asks parentMap params <- ask
return $ getClosestCommand tree t return $ getClosestCommand (parentMap params) t
-- Is the token used as a command name (the first word in a T_SimpleCommand)? -- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
@@ -364,8 +404,8 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
| currentId == getId word = go id rest | currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest) go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest | currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_) go currentId (t@(T_SimpleCommand _ _ (word:_)):_) =
| currentId == getId word = True getId word == currentId || getId (getCommandTokenOrThis t) == currentId
go _ _ = False go _ _ = False
-- A list of the element and all its parents up to the root node. -- A list of the element and all its parents up to the root node.
@@ -377,8 +417,8 @@ getPath tree t = t :
-- Version of the above taking the map from the current context -- Version of the above taking the map from the current context
-- Todo: give this the name "getPath" -- Todo: give this the name "getPath"
getPathM t = do getPathM t = do
map <- asks parentMap params <- ask
return $ getPath map t return $ getPath (parentMap params) t
isParentOf tree parent child = isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child elem (getId parent) . map getId $ getPath tree child
@@ -388,14 +428,13 @@ parents params = getPath (parentMap params)
-- Find the first match in a list where the predicate is Just True. -- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing. -- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
findFirst p l = findFirst p = foldr go Nothing
case l of where
[] -> Nothing go x acc =
(x:xs) -> case p x of
case p x of Just True -> return x
Just True -> return x Just False -> Nothing
Just False -> Nothing Nothing -> acc
Nothing -> findFirst p xs
-- Check whether a word is entirely output from a single command -- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of tokenIsJustCommandOutput t = case t of
@@ -410,8 +449,7 @@ tokenIsJustCommandOutput t = case t of
-- TODO: Replace this with a proper Control Flow Graph -- TODO: Replace this with a proper Control Flow Graph
getVariableFlow params t = getVariableFlow params t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) [] reverse $ execState (doStackAnalysis startScope endScope t) []
in reverse stack
where where
startScope t = startScope t =
let scopeType = leadType params t let scopeType = leadType params t
@@ -462,28 +500,22 @@ leadType params t =
causesSubshell = do causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline (T_Pipeline _ _ list) <- parentPipeline
if length list <= 1 return $ case list of
then return False _:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t
else if not $ hasLastpipe params _ -> False
then return True
else return . not $ (getId . head $ reverse list) == getId t
getModifiedVariables t = getModifiedVariables t =
case t of case t of
T_SimpleCommand _ vars [] -> T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of [(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars]
T_Assignment id _ name _ w -> T_SimpleCommand {} ->
[(x, x, name, dataTypeFrom DataString w)] getModifiedVariableCommand t
_ -> []
) vars
c@T_SimpleCommand {} ->
getModifiedVariableCommand c
TA_Unary _ "++|" v@(TA_Variable _ name _) -> TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])] [(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ "|++" v@(TA_Variable _ name _) -> TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])] [(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do TA_Assignment _ op (TA_Variable _ name _) rhs -> do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString $ SourceFrom [rhs]) return (t, t, name, DataString $ SourceFrom [rhs])
@@ -496,25 +528,23 @@ getModifiedVariables t =
-- Count [[ -v foo ]] as an "assignment". -- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused. -- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do TC_Unary id _ "-v" token -> maybeToList $ do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index str <- getVariableForTestDashV token
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> Nothing
guard . not . null $ str
return (t, token, str, DataString SourceChecked) return (t, token, str, DataString SourceChecked)
TC_Unary _ _ "-n" token -> markAsChecked t token
TC_Unary _ _ "-z" token -> markAsChecked t token
TC_Nullary _ _ token -> markAsChecked t token
T_DollarBraced _ _ l -> maybeToList $ do T_DollarBraced _ _ l -> maybeToList $ do
let string = bracedString t let string = concat $ oversimplify l
let modifier = getBracedModifier string let modifier = getBracedModifier string
guard $ any (`isPrefixOf` modifier) ["=", ":="] guard $ any (`isPrefixOf` modifier) ["=", ":="]
return (t, t, getBracedReference string, DataString $ SourceFrom [l]) return (t, t, getBracedReference string, DataString $ SourceFrom [l])
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) -> T_CoProc _ name _ ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable --Points to 'for' rather than variable
@@ -522,6 +552,14 @@ getModifiedVariables t =
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> [] _ -> []
where
markAsChecked place token = mapMaybe (f place) $ getWordParts token
f place t = case t of
T_DollarBraced _ _ l ->
let str = getBracedReference $ concat $ oversimplify l in do
guard $ isVariableName str
return (place, t, str, DataString SourceChecked)
_ -> Nothing
isClosingFileOp op = isClosingFileOp op =
case op of case op of
@@ -533,12 +571,13 @@ isClosingFileOp op =
-- Consider 'export/declare -x' a reference, since it makes the var available -- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) = getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of case x of
"declare" -> forDeclare
"typeset" -> forDeclare
"export" -> if "f" `elem` flags "export" -> if "f" `elem` flags
then [] then []
else concatMap getReference rest else concatMap getReference rest
"declare" -> if "local" -> if "x" `elem` flags
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest then concatMap getReference rest
else [] else []
"trap" -> "trap" ->
@@ -548,6 +587,13 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token] "alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
_ -> [] _ -> []
where 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_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)] getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = [] getReference _ = []
@@ -569,10 +615,14 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"builtin" -> "builtin" ->
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
"read" -> "read" ->
let params = map getLiteral rest let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
readArrayVars = getReadArrayVariables rest in fromMaybe fallback $ do
in parsed <- getGnuOpts flagsForRead rest
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params case lookup "a" parsed of
Just (_, var) -> (:[]) <$> getLiteralArray var
Nothing -> return $ catMaybes $
map (getLiteral . snd . snd) $ filter (null . fst) parsed
"getopts" -> "getopts" ->
case rest of case rest of
opts:var:_ -> maybeToList $ getLiteral var opts:var:_ -> maybeToList $ getLiteral var
@@ -583,8 +633,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"export" -> "export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars "declare" -> forDeclare
"typeset" -> declaredVars "typeset" -> forDeclare
"local" -> concatMap getModifierParamString rest "local" -> concatMap getModifierParamString rest
"readonly" -> "readonly" ->
@@ -596,6 +646,7 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
return (base, base, "@", DataString $ SourceFrom params) return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest "printf" -> maybeToList $ getPrintfVariable rest
"wait" -> maybeToList $ getWaitVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest "mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest "readarray" -> maybeToList $ getMapfileArray base rest
@@ -615,6 +666,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]] T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t stripEqualsFrom t = t
forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
declaredVars = concatMap (getModifierParam defaultType) rest declaredVars = concatMap (getModifierParam defaultType) rest
where where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
@@ -653,36 +706,39 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
_ -> return (t:fromMaybe [] (getSetParams rest)) _ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list
where getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
where getFlagAssignedVariable str dataSource maybeFlags = do
(varName, varType) = case elemIndex '[' var of flags <- maybeFlags
Just i -> (take i var, DataArray) (_, (flag, value)) <- find ((== str) . fst) flags
Nothing -> (var, DataString) variableName <- getLiteralStringExt (const $ return "!") value
f (_:rest) = f rest let (baseName, index) = span (/= '[') variableName
f [] = fail "not found" return (base, value, baseName, (if null index then DataString else DataArray) dataSource)
-- mapfile has some curious syntax allowing flags plus 0..n variable names -- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. Here we cheat and -- where only the first non-option one is used if any.
-- just get the last one, if it's a variable name. getMapfileArray base rest = parseArgs `mplus` fallback
getMapfileArray base arguments = do where
lastArg <- listToMaybe (reverse arguments) parseArgs :: Maybe (Token, Token, String, DataType)
name <- getLiteralString lastArg parseArgs = do
guard $ isVariableName name args <- getGnuOpts "d:n:O:s:u:C:c:t" rest
return (base, lastArg, name, DataArray SourceExternal) case [y | ("",(_,y)) <- args] of
[] ->
-- get all the array variables used in read, e.g. read -a arr return (base, base, "MAPFILE", DataArray SourceExternal)
getReadArrayVariables args = first:_ -> do
map (getLiteralArray . snd) name <- getLiteralString first
(filter (isArrayFlag . fst) (zip args (tail args))) guard $ isVariableName name
return (base, first, name, DataArray SourceExternal)
isArrayFlag x = fromMaybe False $ do -- If arg parsing fails (due to bad or new flags), get the last variable name
str <- getLiteralString x fallback :: Maybe (Token, Token, String, DataType)
return $ case str of fallback = do
'-':'-':_ -> False (name, token) <- listToMaybe . mapMaybe f $ reverse rest
'-':str -> 'a' `elem` str return (base, token, name, DataArray SourceExternal)
_ -> False f arg = do
name <- getLiteralString arg
guard $ isVariableName name
return (name, arg)
-- get the FLAGS_ variable created by a shflags DEFINE_ call -- get the FLAGS_ variable created by a shflags DEFINE_ call
getFlagVariable (n:v:_) = do getFlagVariable (n:v:_) = do
@@ -699,6 +755,20 @@ getIndexReferences s = fromMaybe [] $ do
where where
re = mkRegex "(\\[.*\\])" 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_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"] prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"] prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
@@ -713,7 +783,7 @@ getOffsetReferences mods = fromMaybe [] $ do
getReferencedVariables parents t = getReferencedVariables parents t =
case t of case t of
T_DollarBraced id _ l -> let str = bracedString t in T_DollarBraced id _ l -> let str = concat $ oversimplify l in
(t, t, getBracedReference str) : (t, t, getBracedReference str) :
map (\x -> (l, l, x)) ( map (\x -> (l, l, x)) (
getIndexReferences str getIndexReferences str
@@ -728,7 +798,7 @@ getReferencedVariables parents t =
TC_Unary id _ "-v" token -> getIfReference t token TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs -> TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op if isDereferencingBinaryOp op
then concatMap (getIfReference t) [lhs, rhs] then concatMap (getIfReference t) [lhs, rhs]
else [] else []
@@ -738,7 +808,7 @@ getReferencedVariables parents t =
(t, t, "output") (t, t, "output")
] ]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op] [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x x -> getReferencedVariableCommand x
where where
@@ -755,19 +825,18 @@ getReferencedVariables parents t =
literalizer t = case t of literalizer t = case t of
T_Glob _ s -> return s -- Also when parsed as globs T_Glob _ s -> return s -- Also when parsed as globs
_ -> Nothing _ -> []
getIfReference context token = maybeToList $ do getIfReference context token = maybeToList $ do
str@(h:_) <- getLiteralStringExt literalizer token str <- getVariableForTestDashV token
when (isDigit h) $ fail "is a number"
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False _ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v] dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
@@ -792,6 +861,7 @@ isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x isVariableChar x = isVariableStartChar x || isDigit x
isSpecialVariableChar = (`elem` "*@#?-$!")
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*" variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123" prop_isVariableName1 = isVariableName "_fo123"
@@ -808,7 +878,7 @@ getVariablesFromLiteralToken token =
prop_getVariablesFromLiteral1 = prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"] getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string = getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string map head $ matchAllSubgroups variableRegex string
where where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
@@ -824,33 +894,34 @@ prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo" prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo" prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == "" prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference11b= getBracedReference "!os@" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == "" prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo" prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $ getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where where
noPrefix = dropPrefix s noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest dropPrefix (c:rest) | c `elem` "!#" = rest
dropPrefix "" = "" dropPrefix cs = cs
takeName s = do takeName s = do
let name = takeWhile isVariableChar s let name = takeWhile isVariableChar s
guard . not $ null name guard . not $ null name
return name return name
getSpecial (c:_) = getSpecial (c:_) | isSpecialVariableChar c = return [c]
if c `elem` "*@#?-$!" then return [c] else fail "not special" getSpecial _ = fail "empty or not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*} nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest guard $ isVariableChar next -- e.g. ${!@}
guard $ suffix /= rest -- e.g. ${!@} first <- find (not . isVariableChar) rest
first <- suffix !!! 0 guard $ first `elem` "*?@"
guard $ first `elem` "*?"
return "" return ""
nameExpansion _ = Nothing nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz" prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo" prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]" prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
getBracedModifier s = headOrDefault "" $ do getBracedModifier s = headOrDefault "" $ do
let var = getBracedReference s let var = getBracedReference s
a <- dropModifier s a <- dropModifier s
@@ -869,6 +940,10 @@ getBracedModifier s = headOrDefault "" $ do
headOrDefault _ (a:_) = a headOrDefault _ (a:_) = a
headOrDefault def _ = def 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. --- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i = (!!!) list i =
case drop i list of case drop i list of
@@ -877,8 +952,8 @@ headOrDefault def _ = def
-- Run a command if the shell is in the given list -- Run a command if the shell is in the given list
whenShell l c = do whenShell l c = do
shell <- asks shellType params <- ask
when (shell `elem` l ) c when (shellType params `elem` l ) c
filterByAnnotation asSpec params = filterByAnnotation asSpec params =
@@ -907,45 +982,15 @@ isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"} -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t = isQuotedAlternativeReference t =
case t of case t of
T_DollarBraced _ _ _ -> T_DollarBraced _ _ l ->
getBracedModifier (bracedString t) `matches` re getBracedModifier (concat $ oversimplify l) `matches` re
_ -> False _ -> False
where where
re = mkRegex "(^|\\]):?\\+" re = mkRegex "(^|\\]):?\\+"
-- getGnuOpts "erd:u:" will parse a SimpleCommand like supportsArrays Bash = True
-- read -re -d : -u 3 bar supportsArrays Ksh = True
-- into supportsArrays _ = False
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
-- where flags with arguments map to arguments, while others map to themselves.
-- Any unrecognized flag will result in Nothing.
getGnuOpts str t = getOpts str $ getAllFlags t
getBsdOpts str t = getOpts str $ getLeadingFlags t
getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
getOpts string flags = process flags
where
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = []
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process [(token, flag)] = do
takesArg <- Map.lookup flag flagMap
guard $ not takesArg
return [(flag, token)]
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
takesArg <- Map.lookup flag1 flagMap
if takesArg
then do
guard $ null flag2
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh) -- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool isBashLike :: Parameters -> Bool
@@ -956,5 +1001,34 @@ isBashLike params =
Dash -> False Dash -> False
Sh -> 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 [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) 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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -156,6 +156,11 @@ checkWithIncludesAndSourcePath includes mapper = getErrors
siFindSource = mapper siFindSource = mapper
} }
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
(mockRcFile rc $ mockedSystemInterface includes) {
siFindSource = mapper
}
prop_findsParseIssue = check "echo \"$12\"" == [1037] prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 = prop_commentDisablesParseIssue1 =
@@ -229,6 +234,12 @@ prop_cantSourceDynamic =
prop_cantSourceDynamic2 = prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo" [1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canStripPrefixAndSource =
null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
prop_canStripPrefixAndSource2 =
null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
prop_canSourceDynamicWhenRedirected = prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
@@ -270,7 +281,7 @@ prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1" check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source' prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh" 3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n" prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
@@ -295,6 +306,20 @@ prop_canDisableShebangWarning = null $ result
csScript = "#shellcheck disable=SC2148\nfoo" 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 {
csFilename = "file.sh",
csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
}
prop_shExtensionDoesntMatter = result == [2148] prop_shExtensionDoesntMatter = result == [2148]
where where
result = checkWithSpec [] emptyCheckSpec { result = checkWithSpec [] emptyCheckSpec {
@@ -371,7 +396,7 @@ prop_canEnableOptionalsWithRc = result == [2244]
prop_sourcePathRedirectsName = result == [2086] prop_sourcePathRedirectsName = result == [2086]
where where
f "dir/myscript" _ "lib" = return "foo/lib" f "dir/myscript" _ _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib", csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript", csFilename = "dir/myscript",
@@ -380,7 +405,7 @@ prop_sourcePathRedirectsName = result == [2086]
prop_sourcePathAddsAnnotation = result == [2086] prop_sourcePathAddsAnnotation = result == [2086]
where 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 { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib", csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript", csFilename = "dir/myscript",
@@ -389,13 +414,75 @@ prop_sourcePathAddsAnnotation = result == [2086]
prop_sourcePathRedirectsDirective = result == [2086] prop_sourcePathRedirectsDirective = result == [2086]
where where
f "dir/myscript" _ "lib" = return "foo/lib" f "dir/myscript" _ _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null" f _ _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens", csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript", csFilename = "dir/myscript",
csCheckSourced = True 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 [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -19,6 +19,7 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
@@ -52,13 +53,11 @@ verify :: CommandCheck -> String -> Bool
verify f s = producesComments (getChecker [f]) s == Just True verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments (getChecker [f]) s == Just False verifyNot f s = producesComments (getChecker [f]) s == Just False
arguments (T_SimpleCommand _ _ (cmd:args)) = args
commandChecks :: [CommandCheck] commandChecks :: [CommandCheck]
commandChecks = [ commandChecks = [
checkTr checkTr
,checkFindNameGlob ,checkFindNameGlob
,checkNeedlessExpr ,checkExpr
,checkGrepRe ,checkGrepRe
,checkTrapQuotes ,checkTrapQuotes
,checkReturn ,checkReturn
@@ -95,7 +94,13 @@ commandChecks = [
,checkSudoArgs ,checkSudoArgs
,checkSourceArgs ,checkSourceArgs
,checkChmodDashr ,checkChmodDashr
,checkXargsDashi
,checkUnquotedEchoSpaces
,checkEvalArray
] ]
++ map checkArgComparison declaringCommands
++ map checkMaskedReturns declaringCommands
optionalChecks = map fst optionalCommandChecks optionalChecks = map fst optionalCommandChecks
optionalCommandChecks :: [(CheckDescription, CommandCheck)] optionalCommandChecks :: [(CheckDescription, CommandCheck)]
@@ -115,6 +120,47 @@ prop_verifyOptionalExamples = all check optionalCommandChecks
verify check (cdPositive desc) verify check (cdPositive desc)
&& verifyNot check (cdNegative desc) && verifyNot check (cdNegative desc)
-- Run a check against the getopt parser. If it fails, the lists are empty.
checkGetOpts str flags args f =
flags == actualFlags && args == actualArgs
where
toTokens = map (T_Literal (Id 0)) . words
opts = fromMaybe [] $ f (toTokens str)
actualFlags = filter (not . null) $ map fst opts
actualArgs = [onlyLiteralString x | ("", (_, x)) <- opts]
-- Short options
prop_checkGetOptsS1 = checkGetOpts "-f x" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS2 = checkGetOpts "-fx" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True) "fx" []
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 :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty buildCommandMap = foldl' addCheck Map.empty
where where
@@ -199,27 +245,81 @@ prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ] acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
f [] = return () f [] = return ()
f (x:xs) = g x xs f (x:xs) = foldr g (const $ return ()) xs x
g _ [] = return () g b acc a = do
g a (b:r) = do
forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $ forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it." warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
g b r acc b
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)" prop_checkExpr = verify checkExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``" prop_checkExpr2 = verify checkExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)" prop_checkExpr3 = verifyNot checkExpr "foo=$(expr foo : regex)"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)" prop_checkExpr4 = verifyNot checkExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where prop_checkExpr5 = verify checkExpr "# shellcheck disable=SC2003\nexpr match foo bar"
f t = 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)) $ when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId $ getCommandTokenOrThis t) 2003 style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]." "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 -- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ] exceptions = [ ":", "<", ">", "<=", ">=",
-- We can offer better suggestions for these
"match", "length", "substr", "index"]
words = mapMaybe getLiteralString 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_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3" prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
@@ -283,17 +383,11 @@ checkGrepRe = CommandCheck (Basename "grep") check where
candidates = candidates =
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
getSuspiciousRegexWildcard str = getSuspiciousRegexWildcard str = case matchRegex suspicious str of
if not $ str `matches` contra Just [[c]] | not (str `matches` contra) -> Just c
then do _ -> fail "looks good"
match <- matchRegex suspicious str suspicious = mkRegex "([A-Za-z1-9])\\*"
str <- match !!! 0 contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
str !!! 0
else
fail "looks good"
where
suspicious = mkRegex "([A-Za-z1-9])\\*"
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
@@ -462,8 +556,8 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where where
check t = sequence_ $ do check t = sequence_ $ do
let flags = getAllFlags t let flags = getAllFlags t
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags dashP <- find (\(_,f) -> f == "p" || f == "parents") flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags dashM <- find (\(_,f) -> f == "m" || f == "mode") flags
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not. -- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
guard $ any couldHaveSubdirs (drop 1 $ arguments t) guard $ any couldHaveSubdirs (drop 1 $ arguments t)
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory." return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
@@ -483,7 +577,7 @@ prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' i
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where where
f args = case args of f args = case args of
first:rest -> unless (isFlag first) $ mapM_ check rest first:rest | not $ isFlag first -> mapM_ check rest
_ -> return () _ -> return ()
check param = sequence_ $ do check param = sequence_ $ do
@@ -520,9 +614,9 @@ checkInteractiveSu = CommandCheck (Basename "su") f
info (getId cmd) 2117 info (getId cmd) 2117
"To run commands as another user, use su -c or sudo." "To run commands as another user, use su -c or sudo."
undirected (T_Pipeline _ _ l) = length l <= 1 undirected (T_Pipeline _ _ (_:_:_)) = False
-- This should really just be modifications to stdin, but meh -- This should really just be modifications to stdin, but meh
undirected (T_Redirecting _ list _) = null list undirected (T_Redirecting _ (_:_) _) = False
undirected _ = True undirected _ = True
@@ -539,9 +633,8 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
([], hostport:r@(_:_)) -> checkArg $ last r ([], hostport:r@(_:_)) -> checkArg $ last r
_ -> return () _ -> return ()
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) = checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
case filter (not . isConstant) parts of forM_ (find (not . isConstant) parts) $
[] -> return () \x -> info (getId x) 2029
(x:_) -> info (getId x) 2029
"Note that, unescaped, this expands on the client side." "Note that, unescaped, this expands on the client side."
checkArg _ = return () checkArg _ = return ()
@@ -567,6 +660,8 @@ prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'" prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42" prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'" prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@@ -580,22 +675,21 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
let formatCount = length formats let formatCount = length formats
let argCount = length more let argCount = length more
return $ return $ if
case () of | argCount == 0 && formatCount == 0 ->
() | argCount == 0 && formatCount == 0 -> return () -- This is fine
return () -- This is fine | formatCount == 0 && argCount > 0 ->
() | formatCount == 0 && argCount > 0 -> err (getId format) 2182
err (getId format) 2182 "This printf format string has no variables. Other arguments are ignored."
"This printf format string has no variables. Other arguments are ignored." | any mayBecomeMultipleArgs more ->
() | any mayBecomeMultipleArgs more -> return () -- We don't know so trust the user
return () -- We don't know so trust the user | argCount < formatCount && onlyTrailingTs formats argCount ->
() | argCount < formatCount && onlyTrailingTs formats argCount -> return () -- Allow trailing %()Ts since they use the current time
return () -- Allow trailing %()Ts since they use the current time | argCount > 0 && argCount `mod` formatCount == 0 ->
() | argCount > 0 && argCount `mod` formatCount == 0 -> return () -- Great: a suitable number of arguments
return () -- Great: a suitable number of arguments | otherwise ->
() -> warn (getId format) 2183 $
warn (getId format) 2183 $ "This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $ unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
info (getId format) 2059 info (getId format) 2059
@@ -610,6 +704,8 @@ prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T" prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT" prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb" prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss"
prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss"
getPrintfFormats = getFormats getPrintfFormats = getFormats
where where
-- Get the arguments in the string as a string of type characters, -- Get the arguments in the string as a string of type characters,
@@ -628,17 +724,17 @@ getPrintfFormats = getFormats
regexBasedGetFormats rest = regexBasedGetFormats rest =
case matchRegex re rest of case matchRegex re rest of
Just [width, precision, typ, rest] -> Just [width, precision, typ, rest, _] ->
(if width == "*" then "*" else "") ++ (if width == "*" then "*" else "") ++
(if precision == "*" then "*" else "") ++ (if precision == "*" then "*" else "") ++
typ ++ getFormats rest typ ++ getFormats rest
Nothing -> take 1 rest ++ getFormats rest Nothing -> take 1 rest ++ getFormats rest
where where
-- constructed based on specifications in "man printf" -- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)" re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \ / -- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
-- V V V V V -- V V V V V
-- flags field width precision format character rest -- flags field width precision format character rest
-- field width and precision can be specified with a '*' instead of a digit, -- field width and precision can be specified with a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used -- in which case printf will accept one more argument for each '*' used
@@ -663,17 +759,12 @@ prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set" prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments) checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where where
f (var:value:rest) = f (var:rest)
let str = literal var in | (not (null rest) && isVariableName str) || isAssignment str =
when (isVariableName str || isAssignment str) $ warn (getId var) 2121 "To assign a variable, use just 'var=value', no 'set ..'."
msg (getId var) where str = literal var
f (var:_) =
when (isAssignment $ literal var) $
msg (getId var)
f _ = return () f _ = return ()
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
isAssignment str = '=' `elem` str isAssignment str = '=' `elem` str
literal (T_NormalWord _ l) = concatMap literal l literal (T_NormalWord _ l) = concatMap literal l
literal (T_Literal _ str) = str literal (T_Literal _ str) = str
@@ -687,8 +778,7 @@ prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments) checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where where
check t = sequence_ $ do check t = sequence_ $ do
var <- getSingleUnmodifiedVariable t name <- getSingleUnmodifiedBracedString t
let name = bracedString var
return . warn (getId t) 2163 $ return . warn (getId t) 2163 $
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
@@ -700,30 +790,43 @@ prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var" prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1" prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}" prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
prop_checkReadExpansions9 = verify checkReadExpansions "read arr[val]"
checkReadExpansions = CommandCheck (Exactly "read") check checkReadExpansions = CommandCheck (Exactly "read") check
where where
options = getGnuOpts flagsForRead options = getGnuOpts flagsForRead
getVars cmd = fromMaybe [] $ do getVars cmd = fromMaybe [] $ do
opts <- options cmd opts <- options $ arguments cmd
return [y | (x,y) <- opts, null x || x == "a"] return [y | (x,(_, y)) <- opts, null x || x == "a"]
check cmd = mapM_ warning $ getVars cmd check cmd = do
warning t = sequence_ $ do mapM_ dollarWarning $ getVars cmd
var <- getSingleUnmodifiedVariable t mapM_ arrayWarning $ arguments cmd
let name = bracedString var
dollarWarning t = sequence_ $ do
name <- getSingleUnmodifiedBracedString t
guard $ isVariableName name -- e.g. not $1 guard $ isVariableName name -- e.g. not $1
return . warn (getId t) 2229 $ return . warn (getId t) 2229 $
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "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. -- Return the single variable expansion that makes up this word, if any.
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing -- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
getSingleUnmodifiedVariable :: Token -> Maybe Token getSingleUnmodifiedBracedString :: Token -> Maybe String
getSingleUnmodifiedVariable word = getSingleUnmodifiedBracedString word =
case getWordParts word of case getWordParts word of
[t@(T_DollarBraced {})] -> [T_DollarBraced _ _ l] ->
let contents = bracedString t let contents = concat $ oversimplify l
name = getBracedReference contents name = getBracedReference contents
in guard (contents == name) >> return t in guard (contents == name) >> return contents
_ -> Nothing _ -> Nothing
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'" prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
@@ -754,6 +857,9 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]" prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo" 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) checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where where
check arg = check arg =
@@ -853,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_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_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_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 checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
where where
f :: Token -> Analysis f :: Token -> Analysis
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do f t@(T_SimpleCommand _ _ (cmd:arg1:name:_)) = do
path <- getPathM t path <- getPathM t
params <- ask
sequence_ $ do sequence_ $ do
options <- getLiteralString arg1 options <- getLiteralString arg1
getoptsVar <- getLiteralString name
(T_WhileExpression _ _ body) <- findFirst whileLoop path (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 return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
f _ = return () f _ = return ()
@@ -883,12 +1003,12 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
notRequested = Map.difference handledMap requestedMap notRequested = Map.difference handledMap requestedMap
warnUnhandled optId caseId str = warnUnhandled optId caseId str =
warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'." warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
warnRedundant (key, expr) = sequence_ $ do warnRedundant (Just str, expr)
str <- key | str `notElem` ["*", ":", "?"] =
guard $ str `notElem` ["*", ":", "?"] warn (getId expr) 2214 "This case is not specified by getopts."
return $ warn (getId expr) 2214 "This case is not specified by getopts." warnRedundant _ = return ()
getHandledStrings (_, globs, _) = getHandledStrings (_, globs, _) =
map (\x -> (literal x, x)) globs map (\x -> (literal x, x)) globs
@@ -899,7 +1019,7 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
fromGlob t = fromGlob t =
case t of case t of
T_Glob _ ('[':c:']':[]) -> return [c] T_Glob _ ['[', c, ']'] -> return [c]
T_Glob _ "*" -> return "*" T_Glob _ "*" -> return "*"
T_Glob _ "?" -> return "?" T_Glob _ "?" -> return "?"
_ -> Nothing _ -> Nothing
@@ -934,7 +1054,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
when (isRecursive t) $ when (isRecursive t) $
mapM_ (mapM_ checkWord . braceExpand) $ arguments t mapM_ (mapM_ checkWord . braceExpand) $ arguments t
where where
isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags
checkWord token = checkWord token =
case getLiteralString token of case getLiteralString token of
@@ -966,9 +1086,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
f _ = return "" f _ = return ""
stripTrailing c = reverse . dropWhile (== c) . reverse stripTrailing c = reverse . dropWhile (== c) . reverse
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest) skipRepeating c = foldr go []
skipRepeating c (a:r) = a:skipRepeating c r where
skipRepeating _ [] = [] go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r
paths = [ paths = [
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local", "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
@@ -1039,7 +1159,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
prop_checkWhich = verify checkWhich "which '.+'" prop_checkWhich = verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "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_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input" prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
@@ -1081,8 +1201,8 @@ prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = sequence_ $ do f t = sequence_ $ do
opts <- parseOpts t opts <- parseOpts $ arguments t
let nonFlags = [x | ("",x) <- opts] let nonFlags = [x | ("",(x, _)) <- opts]
commandArg <- nonFlags !!! 0 commandArg <- nonFlags !!! 0
command <- getLiteralString commandArg command <- getLiteralString commandArg
guard $ command `elem` builtins guard $ command `elem` builtins
@@ -1113,5 +1233,158 @@ checkChmodDashr = CommandCheck (Basename "chmod") f
guard $ flag == "-r" guard $ flag == "-r"
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions." return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
prop_checkXargsDashi1 = verify checkXargsDashi "xargs -i{} echo {}"
prop_checkXargsDashi2 = verifyNot checkXargsDashi "xargs -I{} echo {}"
prop_checkXargsDashi3 = verifyNot checkXargsDashi "xargs sed -i -e foo"
prop_checkXargsDashi4 = verify checkXargsDashi "xargs -e sed -i foo"
prop_checkXargsDashi5 = verifyNot checkXargsDashi "xargs -x sed -i foo"
checkXargsDashi = CommandCheck (Basename "xargs") f
where
f t = sequence_ $ do
opts <- parseOpts $ arguments t
(option, value) <- lookup "i" opts
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 [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) 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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -178,6 +178,9 @@ prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m)
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]" prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_" prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\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 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
checkBashisms = ForShell [Sh, Dash] $ \t -> do checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
@@ -186,102 +189,102 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
kludge params = bashism kludge params = bashism
where where
isDash = shellType params == Dash isDash = shellType params == Dash
warnMsg id s = warnMsg id code s =
if isDash if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported." then err id code $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined." else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is" bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id "extglob is" bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is" bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is" bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are" bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is" bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is" bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are" bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is" bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is" bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is" unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _) bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-ot", "-nt", "-ef" ] = | op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id $ op ++ " is" unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) = bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is" warnMsg id 3014 "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) = bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is" warnMsg id 3015 "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) = bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is" warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) = bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is" warnMsg id 3017 "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] = | op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id $ filter (/= '|') op ++ " is" warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is" bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are" bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
bashism (T_FdRedirect id num _) bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are" | all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) = bashism (T_Assignment id Append _ _ _) =
warnMsg id "+= is" warnMsg id 3024 "+= is"
bashism (T_IoFile id _ word) | isNetworked = bashism (T_IoFile id _ word) | isNetworked =
warnMsg id "/dev/{tcp,udp} is" warnMsg id 3025 "/dev/{tcp,udp} is"
where where
file = onlyLiteralString word file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"] isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | "[^" `isInfixOf` str = bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id "^ in place of ! in glob bracket expressions is" warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str = bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is" warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion mapM_ check expansion
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id $ var ++ " is" warnMsg id 3028 $ var ++ " is"
where where
str = bracedString t str = concat $ oversimplify token
var = getBracedReference str var = getBracedReference str
check (regex, feature) = check (regex, code, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id feature when (isJust $ matchRegex regex str) $ warnMsg id code feature
bashism t@(T_Pipe id "|&") = bashism t@(T_Pipe id "|&") =
warnMsg id "|& in place of 2>&1 | is" warnMsg id 3029 "|& in place of 2>&1 | is"
bashism (T_Array id _) = bashism (T_Array id _) =
warnMsg id "arrays are" warnMsg id 3030 "arrays are"
bashism (T_IoFile id _ t) | isGlob t = bashism (T_IoFile id _ t) | isGlob t =
warnMsg id "redirecting to/from globs is" warnMsg id 3031 "redirecting to/from globs is"
bashism (T_CoProc id _ _) = bashism (T_CoProc id _ _) =
warnMsg id "coproc is" warnMsg id 3032 "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) = bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is" warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x = bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is" warnMsg id 3034 "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x = bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is" warnMsg id 3035 "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex = | t `isCommand` "echo" && argString `matches` flagRegex =
if isDash if isDash
then then
when (argString /= "-n") $ when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n" warnMsg (getId arg) 3036 "echo flags besides -n"
else else
warnMsg (getId arg) "echo flags are" warnMsg (getId arg) 3037 "echo flags are"
where where
argString = concat $ oversimplify arg argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$" flagRegex = mkRegex "^-[eEsn]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are" warnMsg (getId arg) 3038 "exec flags are"
bashism t@(T_SimpleCommand id _ _) bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'let' is" | t `isCommand` "let" = warnMsg id 3039 "'let' is"
bashism t@(T_SimpleCommand _ _ (cmd:args)) bashism t@(T_SimpleCommand _ _ (cmd:args))
| t `isCommand` "set" = unless isDash $ | t `isCommand` "set" = unless isDash $
checkOptions $ getLiteralArgs args checkOptions $ getLiteralArgs args
@@ -289,16 +292,17 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
-- Get the literal options from a list of arguments, -- Get the literal options from a list of arguments,
-- up until the first non-literal one -- up until the first non-literal one
getLiteralArgs :: [Token] -> [(Id, String)] getLiteralArgs :: [Token] -> [(Id, String)]
getLiteralArgs (first:rest) = fromMaybe [] $ do getLiteralArgs = foldr go []
str <- getLiteralString first where
return $ (getId first, str) : getLiteralArgs rest go first rest = case getLiteralString first of
getLiteralArgs [] = [] Just str -> (getId first, str) : rest
Nothing -> []
-- Check a flag-option pair (such as -o errexit) -- Check a flag-option pair (such as -o errexit)
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest) checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
| flag' `matches` oFlagRegex = do | flag' `matches` oFlagRegex = do
when (opt' `notElem` longOptions) $ when (opt' `notElem` longOptions) $
warnMsg oid $ "set option " <> opt' <> " is" warnMsg oid 3040 $ "set option " <> opt' <> " is"
checkFlags (flag:rest) checkFlags (flag:rest)
| otherwise = checkFlags (flag:opt:rest) | otherwise = checkFlags (flag:opt:rest)
checkOptions (flag:rest) = checkFlags (flag:rest) checkOptions (flag:rest) = checkFlags (flag:rest)
@@ -311,10 +315,10 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
unless (flag' `matches` validFlagsRegex) $ unless (flag' `matches` validFlagsRegex) $
forM_ (tail flag') $ \letter -> forM_ (tail flag') $ \letter ->
when (letter `notElem` optionsSet) $ when (letter `notElem` optionsSet) $
warnMsg fid $ "set flag " <> ('-':letter:" is") warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
checkOptions rest checkOptions rest
| beginsWithDoubleDash flag' = do | beginsWithDoubleDash flag' = do
warnMsg fid $ "set flag " <> flag' <> " is" warnMsg fid 3042 $ "set flag " <> flag' <> " is"
checkOptions rest checkOptions rest
-- Either a word that doesn't start with a dash, or simply '--', -- Either a word that doesn't start with a dash, or simply '--',
-- so stop checking. -- so stop checking.
@@ -336,16 +340,19 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
let name = fromMaybe "" $ getCommandName t let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t flags = getLeadingFlags t
in do in do
when (name == "local" && not isDash) $
-- This is so commonly accepted that we'll make it a special case
warnMsg id 3043 $ "'local' is"
when (name `elem` unsupportedCommands) $ when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is" warnMsg id 3044 $ "'" ++ name ++ "' is"
sequence_ $ do sequence_ $ do
allowed' <- Map.lookup name allowedFlags allowed' <- Map.lookup name allowedFlags
allowed <- allowed' allowed <- allowed'
(word, flag) <- find (word, flag) <- find
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is" when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = sequence_ $ do check token = sequence_ $ do
@@ -353,12 +360,12 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
let upper = map toUpper str let upper = map toUpper str
return $ do return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is" warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $ when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token) warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is" "prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $ when (not isDash && upper /= str) $
warnMsg (getId token) warnMsg (getId token) 3049
"using lower/mixed case for signal names is" "using lower/mixed case for signal names is"
in in
mapM_ check (drop 1 rest) mapM_ check (drop 1 rest)
@@ -367,13 +374,13 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
format <- rest !!! 0 -- flags are covered by allowedFlags format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "printf %q is" return $ warnMsg (getId format) 3050 "printf %q is"
where where
unsupportedCommands = [ unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset" "typeset"
] ++ if not isDash then ["local"] else [] ]
allowedFlags = Map.fromList [ allowedFlags = Map.fromList [
("cd", Just ["L", "P"]), ("cd", Just ["L", "P"]),
("exec", Just []), ("exec", Just []),
@@ -390,29 +397,35 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
("unset", Just ["f", "v"]), ("unset", Just ["f", "v"]),
("wait", Just []) ("wait", Just [])
] ]
bashism t@(T_SourceCommand id src _) = bashism t@(T_SourceCommand id src _)
let name = fromMaybe "" $ getCommandName src | getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
in when (name == "source") $ warnMsg id "'source' in place of '.' is" bashism (TA_Expansion _ (T_Literal id str : _))
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix = | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
where where
radix = mkRegex "^[0-9]+#" radix = mkRegex "^[0-9]+#"
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [ expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"), (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"), (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"), (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is") (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
] ]
bashVars = [ bashVars = [
-- This list deliberately excludes $BASH_VERSION as it's often used
-- for shell identification.
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS", "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
"_" "_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE",
"BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS",
"FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE"
] ]
bashDynamicVars = [ "RANDOM", "SECONDS" ] bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "_" ] dashVars = [ "_" ]
@@ -506,13 +519,13 @@ checkMultiDimensionalArrays = ForShell [Bash] f
case token of case token of
T_Assignment _ _ name (first:second:_) _ -> about second T_Assignment _ _ name (first:second:_) _ -> about second
T_IndexedElement _ (first:second:_) _ -> about second T_IndexedElement _ (first:second:_) _ -> about second
T_DollarBraced {} -> T_DollarBraced _ _ l ->
when (isMultiDim token) $ about token when (isMultiDim l) $ about token
_ -> return () _ -> return ()
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays." about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"

View File

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

View File

@@ -273,10 +273,10 @@ getPrefixSum = f 0
where where
f sum _ PSLeaf = sum f sum _ PSLeaf = sum
f sum target (PSBranch pivot left right cumulative) = f sum target (PSBranch pivot left right cumulative) =
case () of case target `compare` pivot of
_ | target < pivot -> f sum target left LT -> f sum target left
_ | target > pivot -> f (sum+cumulative) target right GT -> f (sum+cumulative) target right
_ -> sum+cumulative EQ -> sum+cumulative
-- Add a value to the Prefix Sum tree at the given index. -- Add a value to the Prefix Sum tree at the given index.
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5 -- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
@@ -285,10 +285,10 @@ addPSValue key value tree = if value == 0 then tree else f tree
where where
f PSLeaf = PSBranch key PSLeaf PSLeaf value f PSLeaf = PSBranch key PSLeaf PSLeaf value
f (PSBranch pivot left right sum) = f (PSBranch pivot left right sum) =
case () of case key `compare` pivot of
_ | key < pivot -> PSBranch pivot (f left) right (sum + value) LT -> PSBranch pivot (f left) right (sum + value)
_ | key > pivot -> PSBranch pivot left (f right) sum GT -> PSBranch pivot left (f right) sum
_ -> PSBranch pivot left right (sum + value) EQ -> PSBranch pivot left right (sum + value)
prop_pstreeSumsCorrectly kvs targets = prop_pstreeSumsCorrectly kvs targets =
let let

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import Data.Array
import Data.List import Data.List
import System.IO import System.IO
import System.Info import System.Info
import System.Environment
-- A formatter that carries along an arbitrary piece of data -- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter { data Formatter = Formatter {
@@ -68,12 +69,14 @@ makeNonVirtual comments contents =
shouldOutputColor :: ColorOption -> IO Bool shouldOutputColor :: ColorOption -> IO Bool
shouldOutputColor colorOption = do shouldOutputColor colorOption =
term <- hIsTerminalDevice stdout case colorOption of
let windows = "mingw" `isPrefixOf` os ColorAlways -> return True
let isUsableTty = term && not windows ColorNever -> return False
let useColor = case colorOption of ColorAuto -> do
ColorAlways -> True isTerminal <- hIsTerminalDevice stdout
ColorNever -> False term <- lookupEnv "TERM"
ColorAuto -> isUsableTty let windows = "mingw" `isPrefixOf` os
return useColor 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 :: [PositionedComment] -> IO ()
f group = do f group = do
let filename = sourceFile (head group) let filename = sourceFile (head group)
result <- (siReadFile sys) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
outputResult filename contents group outputResult filename contents group

View File

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

View File

@@ -121,13 +121,13 @@ outputResult options ref result sys = do
outputForFile color sys comments = do outputForFile color sys comments = do
let fileName = sourceFile (head comments) let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName result <- siReadFile sys (Just True) fileName
let contents = either (const "") id result let contents = either (const "") id result
let fileLinesList = lines contents let fileLinesList = lines contents
let lineCount = length fileLinesList let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do forM_ groups $ \commentsForLine -> do
let lineNum = fromIntegral $ lineNo (head commentsForLine) let lineNum = fromIntegral $ lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
@@ -136,10 +136,9 @@ outputForFile color sys comments = do
putStrLn $ color "message" $ putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":" "In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line) putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
putStrLn "" putStrLn ""
showFixedString color commentsForLine (fromIntegral lineNum) fileLines showFixedString color commentsForLine (fromIntegral lineNum) fileLines
) groups
-- Pick out only the lines necessary to show a fix in action -- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
@@ -175,7 +174,7 @@ showFixedString color comments lineNum fileLines =
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String
cuteIndent comment = cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++ replicate (fromIntegral $ colNo comment - 1) ' ' ++
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment
where where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^" arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow = makeArrow =

View File

@@ -73,15 +73,19 @@ import qualified Data.Map as Map
data SystemInterface m = SystemInterface { data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error -- | Given:
siReadFile :: String -> m (Either ErrorMessage String), -- What annotations say about including external files (if anything)
-- Given: -- A resolved filename from siFindSource
-- Read the file or return an error
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
-- | Given:
-- the current script, -- the current script,
-- what annotations say about including external files (if anything)
-- a list of source-path annotations in effect, -- a list of source-path annotations in effect,
-- and a sourced file, -- and a sourced file,
-- find the sourced file -- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath, siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
-- Get the configuration file (name, contents) for a filename -- | Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String)) siGetConfig :: String -> m (Maybe (FilePath, String))
} }
@@ -313,11 +317,11 @@ mockedSystemInterface files = SystemInterface {
siGetConfig = const $ return Nothing siGetConfig = const $ return Nothing
} }
where where
rf file = return $ rf _ file = return $
case find ((== file) . fst) files of case find ((== file) . fst) files of
Nothing -> Left "File not included in mock." Nothing -> Left "File not included in mock."
Just (_, contents) -> Right contents Just (_, contents) -> Right contents
fs _ _ file = return file fs _ _ _ file = return file
mockRcFile rcfile mock = mock { mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile) 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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -24,7 +24,7 @@
module ShellCheck.Parser (parseScript, runTests) where module ShellCheck.Parser (parseScript, runTests) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib hiding (runTests)
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
@@ -37,7 +37,7 @@ import Data.Functor
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find) import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
import Data.Maybe import Data.Maybe
import Data.Monoid import Data.Monoid
import Debug.Trace import Debug.Trace -- STRIP
import GHC.Exts (sortWith) import GHC.Exts (sortWith)
import Prelude hiding (readList) import Prelude hiding (readList)
import System.IO import System.IO
@@ -66,7 +66,7 @@ doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_" variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_" variableChars = upper <|> lower <|> digit <|> oneOf "_"
-- Chars to allow in function names -- Chars to allow in function names
functionChars = variableChars <|> oneOf ":+?-./^@" functionChars = variableChars <|> oneOf ":+?-./^@,"
-- Chars to allow in functions using the 'function' keyword -- Chars to allow in functions using the 'function' keyword
extendedFunctionChars = functionChars <|> oneOf "[]*=!" extendedFunctionChars = functionChars <|> oneOf "[]*=!"
specialVariable = oneOf (concat specialVariables) specialVariable = oneOf (concat specialVariables)
@@ -87,11 +87,23 @@ extglobStart = oneOf extglobStartChars
unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036" unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
unicodeSingleQuotes = "\x2018\x2019" 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 spacing = do
x <- many (many1 linewhitespace <|> try (string "\\\n" >> return "")) x <- many (many1 linewhitespace <|> continuation)
optional readComment optional readComment
return $ concat x 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 spacing1 = do
spacing <- spacing spacing <- spacing
@@ -123,8 +135,10 @@ readUnicodeQuote = do
return $ T_Literal id [c] return $ T_Literal id [c]
carriageReturn = do carriageReturn = do
parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." pos <- getPosition
char '\r' char '\r'
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
return '\r'
almostSpace = almostSpace =
choice [ choice [
@@ -209,8 +223,13 @@ startSpan = IncompleteInterval <$> getPosition
endSpan (IncompleteInterval start) = do endSpan (IncompleteInterval start) = do
endPos <- getPosition endPos <- getPosition
id <- getNextIdBetween start endPos getNextIdBetween start endPos
return id
getSpanPositionsFor m = do
start <- getPosition
m
end <- getPosition
return (start, end)
addToHereDocMap id list = do addToHereDocMap id list = do
state <- getState state <- getState
@@ -253,16 +272,22 @@ ignoreProblemsOf p = do
shouldIgnoreCode code = do shouldIgnoreCode code = do
context <- getCurrentContexts context <- getCurrentContexts
checkSourced <- Mr.asks checkSourced checkSourced <- Mr.asks checkSourced
return $ any (disabling checkSourced) context return $ any (contextItemDisablesCode checkSourced code) context
-- Does this item on the context stack disable warnings for 'code'?
contextItemDisablesCode :: Bool -> Integer -> Context -> Bool
contextItemDisablesCode alsoCheckSourced code = disabling alsoCheckSourced
where where
disabling checkSourced item = disabling checkSourced item =
case item of case item of
ContextAnnotation list -> any disabling' list ContextAnnotation list -> any disabling' list
ContextSource _ -> not $ checkSourced ContextSource _ -> not $ checkSourced
_ -> False _ -> False
disabling' (DisableComment n) = code == n disabling' (DisableComment n m) = code >= n && code < m
disabling' _ = False disabling' _ = False
getCurrentAnnotations includeSource = getCurrentAnnotations includeSource =
concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts
where where
@@ -373,16 +398,15 @@ parseNoteAtId id c l a = do
parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
--------- Convenient combinators --------- Convenient combinators
thenSkip main follow = do thenSkip main follow = main <* optional follow
r <- main
optional follow
return r
unexpecting s p = try $ unexpecting s p = try $
(try p >> fail ("Unexpected " ++ s)) <|> return () (try p >> fail ("Unexpected " ++ s)) <|> return ()
notFollowedBy2 = unexpecting "" notFollowedBy2 = unexpecting ""
isFollowedBy p = (lookAhead . try $ p $> True) <|> return False
reluctantlyTill p end = reluctantlyTill p end =
(lookAhead (void (try end) <|> eof) >> return []) <|> do (lookAhead (void (try end) <|> eof) >> return []) <|> do
x <- p x <- p
@@ -420,7 +444,7 @@ acceptButWarn parser level code note =
parsecBracket before after op = do parsecBracket before after op = do
val <- before val <- before
(op val <* optional (after val)) <|> (after val *> fail "") op val `thenSkip` after val <|> (after val *> fail "")
swapContext contexts p = swapContext contexts p =
parsecBracket (getCurrentContexts <* setCurrentContexts contexts) parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
@@ -914,8 +938,9 @@ prop_readCondition20 = isOk readCondition "[[ echo_rc -eq 0 ]]"
prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]" prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]" prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]" prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
prop_readCondition24 = isWarning readCondition "[[ 1 == 2 ]]]"
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]" prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
readCondition = called "test expression" $ do readCondition = called "test expression" $ do
opos <- getPosition opos <- getPosition
start <- startSpan start <- startSpan
@@ -942,15 +967,9 @@ readCondition = called "test expression" $ do
cpos <- getPosition cpos <- getPosition
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])" close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
id <- endSpan start id <- endSpan start
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?" when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Test expression was opened with double [[ but closed with single ]. Make sure they match."
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?" when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Test expression was opened with single [ but closed with double ]]. Make sure they match."
optional $ lookAhead $ do
pos <- getPosition
notFollowedBy2 readCmdWord <|>
parseProblemAt pos ErrorC 1136
("Unexpected characters after terminating " ++ close ++ ". Missing semicolon/linefeed?")
spacing spacing
many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
return $ T_Condition id typ condition return $ T_Condition id typ condition
readAnnotationPrefix = do readAnnotationPrefix = do
@@ -964,12 +983,14 @@ prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n" prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n" prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\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 readAnnotation = called "shellcheck directive" $ do
try readAnnotationPrefix try readAnnotationPrefix
many1 linewhitespace many1 linewhitespace
readAnnotationWithoutPrefix readAnnotationWithoutPrefix True
readAnnotationWithoutPrefix = do readAnnotationWithoutPrefix sandboxed = do
values <- many1 readKey values <- many1 readKey
optional readAnyComment optional readAnyComment
void linefeed <|> eof <|> do void linefeed <|> eof <|> do
@@ -984,12 +1005,20 @@ readAnnotationWithoutPrefix = do
key <- many1 (letter <|> char '-') key <- many1 (letter <|> char '-')
char '=' <|> fail "Expected '=' after directive key" char '=' <|> fail "Expected '=' after directive key"
annotations <- case key of annotations <- case key of
"disable" -> readCode `sepBy` char ',' "disable" -> readElement `sepBy` char ','
where where
readElement = readRange <|> readAll
readAll = do
string "all"
return $ DisableComment 0 1000000
readRange = do
from <- readCode
to <- choice [ char '-' *> readCode, return $ from+1 ]
return $ DisableComment from to
readCode = do readCode = do
optional $ string "SC" optional $ string "SC"
int <- many1 digit int <- many1 digit
return $ DisableComment (read int) return $ read int
"enable" -> readName `sepBy` char ',' "enable" -> readName `sepBy` char ','
where where
@@ -1011,6 +1040,21 @@ readAnnotationWithoutPrefix = do
"This shell type is unknown. Use e.g. sh or bash." "This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell] return [ShellOverride shell]
"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 _ -> do
parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored." parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored."
anyChar `reluctantlyTill` whitespace anyChar `reluctantlyTill` whitespace
@@ -1027,6 +1071,7 @@ readComment = do
unexpecting "shellcheck annotation" readAnnotationPrefix unexpecting "shellcheck annotation" readAnnotationPrefix
readAnyComment readAnyComment
prop_readAnyComment = isOk readAnyComment "# Comment"
readAnyComment = do readAnyComment = do
char '#' char '#'
many $ noneOf "\r\n" many $ noneOf "\r\n"
@@ -1347,6 +1392,8 @@ prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"
prop_readGlob6 = isOk readGlob "[\\|]" prop_readGlob6 = isOk readGlob "[\\|]"
prop_readGlob7 = isOk readGlob "[^[]" prop_readGlob7 = isOk readGlob "[^[]"
prop_readGlob8 = isOk readGlob "[*?]" prop_readGlob8 = isOk readGlob "[*?]"
prop_readGlob9 = isOk readGlob "[!]^]"
prop_readGlob10 = isOk readGlob "[]]"
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
where where
readSimple = do readSimple = do
@@ -1354,22 +1401,25 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
c <- oneOf "*?" c <- oneOf "*?"
id <- endSpan start id <- endSpan start
return $ T_Glob id [c] return $ T_Glob id [c]
-- Doesn't handle weird things like [^]a] and [$foo]. fixme?
readClass = try $ do readClass = try $ do
start <- startSpan start <- startSpan
char '[' 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 ']' char ']'
id <- endSpan start id <- endSpan start
return $ T_Glob id $ "[" ++ concat s ++ "]" return $ T_Glob id $ "[" ++ concat (negation:leadingBracket:s) ++ "]"
where where
globchars = fmap return . oneOf $ "!$[" ++ extglobStartChars globchars = charToString $ oneOf $ "![" ++ extglobStartChars
predefined = do predefined = do
try $ string "[:" try $ string "[:"
s <- many1 letter s <- many1 letter
string ":]" string ":]"
return $ "[:" ++ s ++ ":]" return $ "[:" ++ s ++ ":]"
charToString = fmap return
readGlobbyLiteral = do readGlobbyLiteral = do
start <- startSpan start <- startSpan
c <- extglobStart <|> char '[' c <- extglobStart <|> char '['
@@ -1392,6 +1442,8 @@ readNormalEscaped = called "escaped char" $ do
do do
next <- quotable <|> oneOf "?*@!+[]{}.,~#" next <- quotable <|> oneOf "?*@!+[]{}.,~#"
when (next == ' ') $ checkTrailingSpaces pos <|> return () 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] return $ if next == '\n' then "" else [next]
<|> <|>
do do
@@ -1459,7 +1511,6 @@ readSingleEscaped = do
case x of case x of
'\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'."; '\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'.";
'\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line."
_ -> return () _ -> return ()
return [s] return [s]
@@ -1554,7 +1605,7 @@ readDollarExpression = do
readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
where where
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos -> arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.") parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substitution), add space after $( . For $((arithmetics)), fix parsing errors.")
prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'" prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
readDollarSingleQuote = called "$'..' expression" $ do readDollarSingleQuote = called "$'..' expression" $ do
@@ -1603,6 +1654,7 @@ readArithmeticExpression = called "((..)) command" $ do
c <- readArithmeticContents c <- readArithmeticContents
string "))" string "))"
id <- endSpan start id <- endSpan start
spacing
return (T_Arithmetic id c) return (T_Arithmetic id c)
-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used -- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used
@@ -1749,6 +1801,8 @@ prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n" prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
prop_readHereDoc20= isWarning readScript "cat << foo\n foo\n()\nfoo\n" prop_readHereDoc20= isWarning readScript "cat << foo\n foo\n()\nfoo\n"
prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n" prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
prop_readHereDoc22 = isWarning readScript "cat << foo\r\ncow\r\nfoo\r\n"
prop_readHereDoc23 = isNotOk readScript "cat << foo \r\ncow\r\nfoo\r\n"
readHereDoc = called "here document" $ do readHereDoc = called "here document" $ do
pos <- getPosition pos <- getPosition
try $ string "<<" try $ string "<<"
@@ -1777,7 +1831,9 @@ readHereDoc = called "here document" $ do
-- Fun fact: bash considers << foo"" quoted, but not << <("foo"). -- Fun fact: bash considers << foo"" quoted, but not << <("foo").
readToken = do readToken = do
str <- readStringForParser readNormalWord str <- readStringForParser readNormalWord
return $ unquote str -- A here doc actually works with \r\n because the \r becomes part of the token
crstr <- (carriageReturn >> (return $ str ++ "\r")) <|> return str
return $ unquote crstr
readPendingHereDocs = do readPendingHereDocs = do
docs <- popPendingHereDocs docs <- popPendingHereDocs
@@ -1892,14 +1948,14 @@ readPendingHereDocs = do
debugHereDoc tokenId endToken doc debugHereDoc tokenId endToken doc
| endToken `isInfixOf` doc = | endToken `isInfixOf` doc =
let lookAt line = when (endToken `isInfixOf` line) $ let lookAt line = when (endToken `isInfixOf` line) $
parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ line ++ "' (!= '" ++ endToken ++ "').") parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ (e4m line) ++ "' (!= '" ++ (e4m endToken) ++ "').")
in do in do
parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ endToken ++ "' further down, but not on a separate line.") parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ (e4m endToken) ++ "' further down, but not on a separate line.")
mapM_ lookAt (lines doc) mapM_ lookAt (lines doc)
| map toLower endToken `isInfixOf` map toLower doc = | map toLower endToken `isInfixOf` map toLower doc =
parseProblemAtId tokenId ErrorC 1043 ("Found " ++ endToken ++ " further down, but with wrong casing.") parseProblemAtId tokenId ErrorC 1043 ("Found " ++ (e4m endToken) ++ " further down, but with wrong casing.")
| otherwise = | otherwise =
parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ endToken ++ "' in the here document.") parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ (e4m endToken) ++ "' in the here document.")
readFilename = readNormalWord readFilename = readNormalWord
@@ -1955,8 +2011,6 @@ readIoRedirect = do
spacing spacing
return $ T_FdRedirect id n redir return $ T_FdRedirect id n redir
readRedirectList = many1 readIoRedirect
prop_readHereString = isOk readHereString "<<< \"Hello $world\"" prop_readHereString = isOk readHereString "<<< \"Hello $world\""
readHereString = called "here string" $ do readHereString = called "here string" $ do
start <- startSpan start <- startSpan
@@ -1966,12 +2020,14 @@ readHereString = called "here string" $ do
word <- readNormalWord word <- readNormalWord
return $ T_HereString id word return $ T_HereString id word
prop_readNewlineList1 = isOk readScript "&> /dev/null echo foo"
readNewlineList = readNewlineList =
many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak
where where
checkBadBreak = optional $ do checkBadBreak = optional $ do
pos <- getPosition pos <- getPosition
try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or && try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or &&
notFollowedBy2 (string "&>") -- Except &> or &>> which is valid
parseProblemAt pos ErrorC 1133 parseProblemAt pos ErrorC 1133
"Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one." "Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one."
readLineBreak = optional readNewlineList readLineBreak = optional readNewlineList
@@ -2031,6 +2087,7 @@ prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)"
prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi" prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi"
prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )" prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"
prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls" prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls"
prop_readSimpleCommand7b = isOk readSimpleCommand "\\:"
prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol" prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol"
prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */" prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */"
prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */" prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */"
@@ -2079,10 +2136,6 @@ readSimpleCommand = called "simple command" $ do
then action then action
else getParser def cmd rest else getParser def cmd rest
cStyleComment cmd =
case cmd of
_ -> False
validateCommand cmd = validateCommand cmd =
case cmd of case cmd of
(T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd) (T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd)
@@ -2119,14 +2172,14 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
let file = getFile file' rest' let file = getFile file' rest'
override <- getSourceOverride override <- getSourceOverride
let literalFile = do let literalFile = do
name <- override `mplus` getLiteralString file name <- override `mplus` getLiteralString file `mplus` stripDynamicPrefix file
-- Hack to avoid 'source ~/foo' trying to read from literal tilde -- Hack to avoid 'source ~/foo' trying to read from literal tilde
guard . not $ "~/" `isPrefixOf` name guard . not $ "~/" `isPrefixOf` name
return name return name
case literalFile of case literalFile of
Nothing -> do Nothing -> do
parseNoteAtId (getId file) WarningC 1090 parseNoteAtId (getId file) WarningC 1090
"Can't follow non-constant source. Use a directive to specify location." "ShellCheck can't follow non-constant source. Use a directive to specify location."
return t return t
Just filename -> do Just filename -> do
proceed <- shouldFollow filename proceed <- shouldFollow filename
@@ -2142,10 +2195,12 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
if filename == "/dev/null" -- always allow /dev/null if filename == "/dev/null" -- always allow /dev/null
then return (Right "", filename) then return (Right "", filename)
else do else do
allAnnotations <- getCurrentAnnotations True
currentScript <- Mr.asks currentFilename currentScript <- Mr.asks currentFilename
paths <- mapMaybe getSourcePath <$> getCurrentAnnotations True let paths = mapMaybe getSourcePath allAnnotations
resolved <- system $ siFindSource sys currentScript paths filename let externalSources = listToMaybe $ mapMaybe getExternalSources allAnnotations
contents <- system $ siReadFile sys resolved resolved <- system $ siFindSource sys currentScript externalSources paths filename
contents <- system $ siReadFile sys externalSources resolved
return (contents, resolved) return (contents, resolved)
case input of case input of
Left err -> do Left err -> do
@@ -2179,6 +2234,21 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
SourcePath x -> Just x SourcePath x -> Just x
_ -> Nothing _ -> 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 =
case getWordParts word of
exp : rest | isStringExpansion exp -> do
str <- getLiteralString (T_NormalWord (Id 0) rest)
guard $ "/" `isPrefixOf` str
return $ "." ++ str
_ -> Nothing
subRead name script = subRead name script =
withContext (ContextSource name) $ withContext (ContextSource name) $
inSeparateContext $ inSeparateContext $
@@ -2276,6 +2346,7 @@ readPipe = do
readCommand = choice [ readCommand = choice [
readCompoundCommand, readCompoundCommand,
readConditionCommand,
readCoProc, readCoProc,
readSimpleCommand readSimpleCommand
] ]
@@ -2284,7 +2355,7 @@ readCmdName = do
-- Ignore alias suppression -- Ignore alias suppression
optional . try $ do optional . try $ do
char '\\' char '\\'
lookAhead $ variableChars lookAhead $ variableChars <|> oneOf ":."
readCmdWord readCmdWord
readCmdWord = do readCmdWord = do
@@ -2388,6 +2459,7 @@ readSubshell = called "explicit subshell" $ do
allspacing allspacing
char ')' <|> fail "Expected ) closing the subshell" char ')' <|> fail "Expected ) closing the subshell"
id <- endSpan start id <- endSpan start
spacing
return $ T_Subshell id list return $ T_Subshell id list
prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }" prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }"
@@ -2408,6 +2480,7 @@ readBraceGroup = called "brace group" $ do
parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it." parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it."
fail "Missing '}'" fail "Missing '}'"
id <- endSpan start id <- endSpan start
spacing
return $ T_BraceGroup id list return $ T_BraceGroup id list
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}" prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
@@ -2460,6 +2533,11 @@ readDoGroup kwId = do
parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'." parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'."
parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'." parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'."
return "Expected 'done'" return "Expected 'done'"
optional . lookAhead $ do
pos <- getPosition
try $ string "<("
parseProblemAt pos ErrorC 1142 "Use 'done < <(cmd)' to redirect from process substitution (currently missing one '<')."
return commands return commands
@@ -2482,19 +2560,31 @@ readForClause = called "for loop" $ do
readArithmetic id <|> readRegular id readArithmetic id <|> readRegular id
where where
readArithmetic id = called "arithmetic for condition" $ do readArithmetic id = called "arithmetic for condition" $ do
try $ string "((" readArithmeticDelimiter '(' "Missing second '(' to start arithmetic for ((;;)) loop"
x <- readArithmeticContents x <- readArithmeticContents
char ';' >> spacing char ';' >> spacing
y <- readArithmeticContents y <- readArithmeticContents
char ';' >> spacing char ';' >> spacing
z <- readArithmeticContents z <- readArithmeticContents
spacing spacing
string "))" readArithmeticDelimiter ')' "Missing second ')' to terminate 'for ((;;))' loop condition"
spacing spacing
optional $ readSequentialSep >> spacing optional $ readSequentialSep >> spacing
group <- readBraced <|> readDoGroup id group <- readBraced <|> readDoGroup id
return $ T_ForArithmetic id x y z group return $ T_ForArithmetic id x y z group
-- For c='(' read "((" and be lenient about spaces
readArithmeticDelimiter c msg = do
char c
startPos <- getPosition
sp <- spacing
endPos <- getPosition
char c <|> do
parseProblemAt startPos ErrorC 1137 msg
fail ""
unless (null sp) $
parseProblemAtWithEnd startPos endPos ErrorC 1138 $ "Remove spaces between " ++ [c,c] ++ " in arithmetic for loop."
readBraced = do readBraced = do
(T_BraceGroup _ list) <- readBraceGroup (T_BraceGroup _ list) <- readBraceGroup
return list return list
@@ -2625,7 +2715,7 @@ readFunctionDefinition = called "function" $ do
readWithoutFunction = try $ do readWithoutFunction = try $ do
name <- many1 functionChars name <- many1 functionChars
guard $ name /= "time" -- Interfers with time ( foo ) guard $ name /= "time" -- Interferes with time ( foo )
spacing spacing
readParens readParens
return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name
@@ -2665,9 +2755,38 @@ readCoProc = called "coproc" $ do
id <- endSpan start id <- endSpan start
return $ T_CoProcBody id body return $ T_CoProcBody id body
readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing) readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
prop_readConditionCommand = isOk readConditionCommand "[[ x ]] > foo 2>&1"
readConditionCommand = do
cmd <- readCondition
redirs <- many readIoRedirect
id <- getNextIdSpanningTokenList (cmd:redirs)
pos <- getPosition
hasDashAo <- isFollowedBy $ do
c <- choice $ try . string <$> ["-o", "-a", "or", "and"]
posEnd <- getPosition
parseProblemAtWithEnd pos posEnd ErrorC 1139 $
"Use " ++ alt c ++ " instead of '" ++ c ++ "' between test commands."
-- If the next word is a keyword, readNormalWord will trigger a warning
hasKeyword <- isFollowedBy readKeyword
hasWord <- isFollowedBy readNormalWord
when (hasWord && not (hasKeyword || hasDashAo)) $ do
-- We have other words following, and no error has been emitted.
posEnd <- getPosition
parseProblemAtWithEnd pos posEnd ErrorC 1140 "Unexpected parameters after condition. Missing &&/||, or bad expression?"
return $ T_Redirecting id redirs cmd
where
alt "or" = "||"
alt "-o" = "||"
alt "and" = "&&"
alt "-a" = "&&"
alt _ = "|| or &&"
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null" prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
readCompoundCommand = do readCompoundCommand = do
cmd <- choice [ cmd <- choice [
@@ -2675,7 +2794,6 @@ readCompoundCommand = do
readAmbiguous "((" readArithmeticExpression readSubshell (\pos -> readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."), parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
readSubshell, readSubshell,
readCondition,
readWhileClause, readWhileClause,
readUntilClause, readUntilClause,
readIfClause, readIfClause,
@@ -2685,15 +2803,15 @@ readCompoundCommand = do
readBatsTest, readBatsTest,
readFunctionDefinition readFunctionDefinition
] ]
spacing
redirs <- many readIoRedirect redirs <- many readIoRedirect
id <- getNextIdSpanningTokenList (cmd:redirs) id <- getNextIdSpanningTokenList (cmd:redirs)
unless (null redirs) $ optional $ do optional . lookAhead $ do
lookAhead $ try (spacing >> needsSeparator) notFollowedBy2 $ choice [readKeyword, g_Lbrace]
parseProblem WarningC 1013 "Bash requires ; or \\n here, after redirecting nested compound commands." pos <- getPosition
many1 readNormalWord
posEnd <- getPosition
parseProblemAtWithEnd pos posEnd ErrorC 1141 "Unexpected tokens after compound command. Bad redirection or missing ;/&&/||/|?"
return $ T_Redirecting id redirs cmd return $ T_Redirecting id redirs cmd
where
needsSeparator = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace ]
readCompoundList = readTerm readCompoundList = readTerm
@@ -2723,7 +2841,7 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
startPos <- getPosition startPos <- getPosition
expression <- readStringForParser readCmdWord expression <- readStringForParser readCmdWord
let (unQuoted, newPos) = kludgeAwayQuotes expression startPos let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
subParse newPos readArithmeticContents unQuoted subParse newPos (readArithmeticContents <* eof) unQuoted
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos) kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p = kludgeAwayQuotes s p =
@@ -2763,17 +2881,13 @@ readLiteralForParser parser = do
prop_readAssignmentWord = isOk readAssignmentWord "a=42" prop_readAssignmentWord = isOk readAssignmentWord "a=42"
prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)" prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)"
prop_readAssignmentWord3 = isWarning readAssignmentWord "$b = 13"
prop_readAssignmentWord4 = isWarning readAssignmentWord "b = $(lol)"
prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol" prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
prop_readAssignmentWord6 = isWarning readAssignmentWord "b += (1 2 3)"
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42" prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= " prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
prop_readAssignmentWord9a= isOk readAssignmentWord "foo=" prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= " prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar" prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )" prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
@@ -2781,52 +2895,63 @@ prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))" prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
readAssignmentWord = readAssignmentWordExt True readAssignmentWord = readAssignmentWordExt True
readWellFormedAssignment = readAssignmentWordExt False readWellFormedAssignment = readAssignmentWordExt False
readAssignmentWordExt lenient = try $ do readAssignmentWordExt lenient = called "variable assignment" $ do
start <- startSpan -- Parse up to and including the = in a 'try'
pos <- getPosition (id, variable, op, indices) <- try $ do
when lenient $ start <- startSpan
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.") pos <- getPosition
variable <- readVariableName -- Check for a leading $ at parse time, to warn for $foo=(bar) which
when lenient $ -- would otherwise cause a parse failure so it can't be checked later.
optional (readNormalDollar >> parseNoteAt pos ErrorC leadingDollarPos <-
1067 "For indirection, use arrays, declare \"var$n=value\", or (for sh) read/eval.") if lenient
indices <- many readArrayIndex then optionMaybe $ getSpanPositionsFor (char '$')
hasLeftSpace <- fmap (not . null) spacing else return Nothing
pos <- getPosition variable <- readVariableName
id <- endSpan start indices <- many readArrayIndex
op <- readAssignmentOp hasLeftSpace <- fmap (not . null) spacing
opStart <- getPosition
id <- endSpan start
op <- readAssignmentOp
opEnd <- getPosition
when (isJust leadingDollarPos || hasLeftSpace) $ do
hasParen <- isFollowedBy (spacing >> char '(')
when hasParen $
sequence_ $ do
(l, r) <- leadingDollarPos
return $ parseProblemAtWithEnd l r ErrorC 1066 "Don't use $ on the left side of assignments."
-- Fail so that this is not parsed as an assignment.
fail ""
-- At this point we know for sure.
return (id, variable, op, indices)
rightPosStart <- getPosition
hasRightSpace <- fmap (not . null) spacing hasRightSpace <- fmap (not . null) spacing
rightPosEnd <- getPosition
isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof)) isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof))
if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
if hasRightSpace || isEndOfCommand
then do then do
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ do
parseNoteAt pos WarningC 1007 parseProblemAtWithEnd rightPosStart rightPosEnd WarningC 1007
"Remove space after = if trying to assign a value (for empty string, use var='' ... )." "Remove space after = if trying to assign a value (for empty string, use var='' ... )."
value <- readEmptyLiteral value <- readEmptyLiteral
return $ T_Assignment id op variable indices value return $ T_Assignment id op variable indices value
else do else do
when (hasLeftSpace || hasRightSpace) $ optional $ do
parseNoteAt pos ErrorC 1068 $ lookAhead $ char '='
"Don't put spaces around the " parseProblem ErrorC 1097 "Unexpected ==. For assignment, use =. For comparison, use [/[[. Or quote for literal string."
++ (if op == Append
then "+= when appending"
else "= in assignments")
++ " (or quote to make it literal)."
value <- readArray <|> readNormalWord value <- readArray <|> readNormalWord
spacing spacing
return $ T_Assignment id op variable indices value return $ T_Assignment id op variable indices value
where where
readAssignmentOp = do readAssignmentOp = do
pos <- getPosition -- This is probably some kind of ascii art border
unexpecting "" $ string "===" unexpecting "===" (string "===")
choice [ choice [
string "+=" >> return Append, string "+=" >> return Append,
do
try (string "==")
parseProblemAt pos ErrorC 1097
"Unexpected ==. For assignment, use =. For comparison, use [/[[."
return Assign,
string "=" >> return Assign string "=" >> return Assign
] ]
@@ -3093,7 +3218,7 @@ readConfigFile filename = do
let line = "line " ++ (show . sourceLine $ errorPos err) let line = "line " ++ (show . sourceLine $ errorPos err)
suggestion = getStringFromParsec $ errorMessages err suggestion = getStringFromParsec $ errorMessages err
in in
"Failed to process " ++ filename ++ ", " ++ line ++ ": " "Failed to process " ++ (e4m filename) ++ ", " ++ line ++ ": "
++ suggestion ++ suggestion
prop_readConfigKVs1 = isOk readConfigKVs "disable=1234" prop_readConfigKVs1 = isOk readConfigKVs "disable=1234"
@@ -3103,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" prop_readConfigKVs5 = isOk readConfigKVs "# shellcheck accepts annotation-like comments in rc files\ndisable=1234"
readConfigKVs = do readConfigKVs = do
anySpacingOrComment anySpacingOrComment
annotations <- many (readAnnotationWithoutPrefix <* anySpacingOrComment) annotations <- many (readAnnotationWithoutPrefix False <* anySpacingOrComment)
eof eof
return $ concat annotations return $ concat annotations
anySpacingOrComment = anySpacingOrComment =
@@ -3114,6 +3239,7 @@ prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"
prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world" prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=(" prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n" prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"
readScriptFile sourced = do readScriptFile sourced = do
start <- startSpan start <- startSpan
pos <- getPosition pos <- getPosition
@@ -3139,8 +3265,8 @@ readScriptFile sourced = do
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
unless ignoreShebang $ unless ignoreShebang $
verifyShebang pos (getShell shebangString) verifyShebang pos (executableFromShebang shebangString)
if ignoreShebang || isValidShell (getShell shebangString) /= Just False if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
then do then do
commands <- withAnnotations annotations readCompoundListOrEmpty commands <- withAnnotations annotations readCompoundListOrEmpty
id <- endSpan start id <- endSpan start
@@ -3154,16 +3280,6 @@ readScriptFile sourced = do
return $ T_Script id shebang [] return $ T_Script id shebang []
where where
basename s = reverse . takeWhile (/= '/') . reverse $ s
getShell sb =
case words sb of
[] -> ""
[x] -> basename x
(first:second:_) ->
if basename first == "env"
then second
else basename first
verifyShebang pos s = do verifyShebang pos s = do
case isValidShell s of case isValidShell s of
Just True -> return () Just True -> return ()
@@ -3256,13 +3372,13 @@ parsesCleanly parser string = runIdentity $ do
-- For printf debugging: print the value of an expression -- For printf debugging: print the value of an expression
-- Example: return $ dump $ T_Literal id [c] -- Example: return $ dump $ T_Literal id [c]
dump :: Show a => a -> a dump :: Show a => a -> a -- STRIP
dump x = trace (show x) x dump x = trace (show x) x -- STRIP
-- Like above, but print a specific expression: -- Like above, but print a specific expression:
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c] -- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
dumps :: Show x => x -> a -> a dumps :: Show x => x -> a -> a -- STRIP
dumps t = trace (show t) dumps t = trace (show t) -- STRIP
parseWithNotes parser = do parseWithNotes parser = do
item <- parser item <- parser
@@ -3316,16 +3432,21 @@ parseShell env name contents = do
prRoot = Just $ prRoot = Just $
reattachHereDocs script (hereDocMap userstate) reattachHereDocs script (hereDocMap userstate)
} }
Left err -> Left err -> do
let context = contextStack state
return newParseResult { return newParseResult {
prComments = prComments =
map toPositionedComment $ map toPositionedComment $
notesForContext (contextStack state) (filter (not . isIgnored context) $
++ [makeErrorFor err] notesForContext context
++ [makeErrorFor err])
++ parseProblems state, ++ parseProblems state,
prTokenPositions = Map.empty, prTokenPositions = Map.empty,
prRoot = Nothing prRoot = Nothing
} }
where
-- A final pass for ignoring parse errors after failed parsing
isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
notesForContext list = zipWith ($) [first, second] $ filter isName list notesForContext list = zipWith ($) [first, second] $ filter isName list
where where

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ fail() {
if git diff | grep -q "" if git diff | grep -q ""
then then
fail "There are uncommited changes" fail "There are uncommitted changes"
fi fi
current=$(git tag --points-at) current=$(git tag --points-at)
@@ -56,19 +56,19 @@ cat << EOF
Manual Checklist Manual Checklist
$((i++)). Make sure none of the automated checks above failed $((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++)). 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++)). 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++)). 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 Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package $((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit $((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for Travis to build $((j++)). Wait for GitHub Actions to build.
$((j++)). Verify release: $((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 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++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md $((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 haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils 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 LTS
ubuntu:18.04 apt-get update && apt-get install -y cabal-install ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build # Stack on Ubuntu LTS
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 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 EOF
exit "$final" exit "$final"

View File

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

View File

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