150 Commits

Author SHA1 Message Date
Vidar Holen
aac0823e6b Stable version v0.11.0
This release is dedicated to Satisfactory, even though my giant
3D ball of rat's nest conveyor belt spaghetti is anything but.

  CHANGELOG

  ## v0.11.0 - 2025-08-03
  ### Added
  - SC2327/SC2328: Warn about capturing the output of redirected commands.
  - SC2329: Warn when (non-escaping) functions are never invoked.
  - SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
  - SC2331: Suggest using standard -e instead of unary -a in tests.
  - SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
  - SC3062: Warn about bashism `[ -o opt ]`.
  - Optional `avoid-negated-conditions`: suggest replacing `[ ! a -eq b ]`
    with `[ a -ne b ]`, and similar for -ge/-lt/=/!=/etc (SC2335).
  - Precompiled binaries for Linux riscv64 (linux.riscv64)

  ### Changed
  - SC2002 about Useless Use Of Cat is now disabled by default. It can be
    re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
  - SC2236/SC2237 about replacing `[ ! -n .. ]` with `[ -z ]` and vice versa
    is now optional under `avoid-negated-conditions`.
  - SC2015 about `A && B || C` no longer triggers when B is a test command.
  - SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
  - Diff output now uses / as path separator on Windows

  ### Fixed
  - SC2218 about function use-before-define is now more accurate.
  - SC2317 about unreachable commands is now less spammy for nested ones.
  - SC2292, optional suggestion for [[ ]], now triggers for Busybox.
  - Updates for Bash 5.3, including `${| cmd; }` and `source -p`

  ### Removed
  - SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
2025-08-03 16:19:11 -07:00
Vidar Holen
1857608dc3 Update release checklist 2025-08-03 16:16:04 -07:00
Vidar Holen
abf3c0ca66 Prevent cabal.project.freeze from interfering with distro tests 2025-08-02 14:39:12 -07:00
Vidar Holen
3f3c2cd94b Add snap docker support for distro testing 2025-08-02 14:39:01 -07:00
Vidar Holen
109b847c75 Git ignore cabal.project.freeze 2025-08-02 12:40:46 -07:00
Vidar Holen
8a9bed1bbd Update linux.x86_64 image 2025-07-30 19:00:38 -07:00
Vidar Holen
29b1bf3a52 Merge pull request #3076 from kuraian/patch-1
Update README.md
2025-07-30 10:02:36 -07:00
Vidar Holen
6c1542a0e3 Merge branch 'Flu-add-bang-to-function-chars' 2025-07-30 09:55:37 -07:00
Vidar Holen
b014ce13da Add unit tests for function names with "#" 2025-07-30 09:55:00 -07:00
Vidar Holen
20fd67da07 Merge branch 'add-bang-to-function-chars' of github.com:Flu/shellcheck into Flu-add-bang-to-function-chars 2025-07-28 15:49:30 -07:00
Vidar Holen
b6c0673edf Merge branch 'e-kwsm-fix-3164' 2025-07-28 15:19:03 -07:00
Vidar Holen
5e6383578d Make SC2335 and friends optional (avoid-negated-conditions) 2025-07-28 15:16:22 -07:00
Vidar Holen
60c0be98b6 Merge branch 'fix-3164' of github.com:e-kwsm/shellcheck into e-kwsm-fix-3164 2025-07-28 14:08:25 -07:00
Vidar Holen
bbd5d211cf Merge pull request #3256 from e-kwsm/SC2143
feat(SC2143): add grep variants for compressed data
2025-07-28 14:06:08 -07:00
Vidar Holen
ed081f8f43 Merge pull request #3253 from e-kwsm/docker
build: simplify Dockerfile
2025-07-27 11:25:54 -07:00
Vidar Holen
1e679444d7 Merge pull request #3257 from e-kwsm/SC2232
feat(SC2232): add more shell builtins
2025-07-24 09:54:13 -07:00
Vidar Holen
5b40fde630 Merge pull request #3258 from e-kwsm/doas-run0
feat(SC2016,SC2032,SC2033): check doas and run0 similarly to sudo
2025-07-24 09:53:45 -07:00
Vidar Holen
317507b8cd Update CI with new builders/ directory name 2025-07-23 16:00:20 -07:00
Vidar Holen
7a768a4b0f Update builder images. Yay for GHC's improved cross-compiler support! 2025-07-23 14:57:41 -07:00
Vidar Holen
89806b96fa Update dependencies 2025-07-22 14:40:25 -07:00
Vidar Holen
d92b0fdd43 Rename build/ to builders/ to avoid looking like build output 2025-07-22 14:38:10 -07:00
Vidar Holen
95ddc900fe Merge pull request #3251 from juhp/patch-1
allow QuickCheck-2.16
2025-07-19 19:43:11 -07:00
Eisuke Kawashima
aba0ffb8d3 feat(SC2016,SC2032,SC2033): check doas and run0 similarly to sudo
partially address #3255
2025-07-20 08:13:16 +09:00
Eisuke Kawashima
c0f1265fa0 feat(SC2232): add more shell builtins 2025-07-20 08:02:13 +09:00
Eisuke Kawashima
6e65eb7136 feat(SC2143): add grep variants for compressed data 2025-07-20 04:53:02 +09:00
Eisuke Kawashima
34cdbaa5e0 feat: avoid double negative of a binary operator in test
suggest `[ a != b ]` over `[ ! a = b ]` and `! [ a = b ]`, and so forth.
c.f. SC2236 and SC2237 (unary operations)

close #3164
2025-07-20 03:18:38 +09:00
Eisuke Kawashima
6c2cb4d009 build: simplify Dockerfile 2025-07-20 02:59:43 +09:00
Jens Petersen
ce6f18cfb7 allow QuickCheck-2.16
see also commercialhaskell/stackage#7787
2025-07-18 19:27:05 +08:00
Vidar Holen
7dc4214149 Normalize \ to / in diff output on Windows (fixes #3240) 2025-07-17 13:40:33 -07:00
Vidar Holen
9b8e0b6f8c Update issue templates 2025-07-17 12:48:56 -07:00
Vidar Holen
c3a597e6dd Update changelog with Bash 5.3 features 2025-07-17 12:32:55 -07:00
Vidar Holen
0c26fb405d Add support for Bash 5.3 source -p .. file (just ignores the path) 2025-07-17 12:27:43 -07:00
Vidar Holen
23097320a4 Add support for ${| ..} expansion (fixes #3243) 2025-07-17 12:01:54 -07:00
Vidar Holen
6a758d5dc7 Update with new Bash 5.3 printf formats 2025-07-17 10:53:05 -07:00
Vidar Holen
64b172e090 Merge pull request #3223 from simondeziel/snap-core24-base
snap: switch to `core24` base
2025-07-17 09:23:17 -07:00
Vidar Holen
947a0ebc7f Merge pull request #3238 from polluks/patch-1
Update shellcheck.1.md
2025-07-17 08:44:00 -07:00
Vidar Holen
2ae0aeaff9 Merge branch 'slycordinator-leading_X' 2025-07-17 08:41:07 -07:00
Vidar Holen
c592abb984 Add unit test for #2689 2025-07-17 08:37:58 -07:00
Christopher Slycord
08329b0698 x-prefix: add support for "!=" and X (capital x)
Changes checkComparisonWithLeadingX to:
1) Work with the "!=" operator in addition to "=" and "==".
2) Support prefixing with "x" and "X". This is helpful since some scripts have comparisons like [ "X$var" = "X" ] and the like
2025-07-17 13:14:04 +09:00
Stefan
24891542c2 Update shellcheck.1.md 2025-07-05 12:13:32 +02:00
Simon Deziel
9477e26858 snap: strip executable during installation
Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
2025-06-18 16:34:35 -04:00
Simon Deziel
638eb88a5a snap: remove now unneeded libatomic1 stage package
Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
2025-06-18 16:01:24 -04:00
Simon Deziel
34f582c81c snap: don't abort on swapon failures
This is needed when building snap on LXD containers.

Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
2025-06-18 15:24:14 -04:00
Simon Deziel
310932be8e snap: modern cabal no longer have sandbox subcommand
Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
2025-06-18 15:24:14 -04:00
Simon Deziel
fa99cfd355 snap: replace dd by fallocate (faster)
Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
2025-06-18 12:50:29 -04:00
Simon Deziel
133bc8a543 snap: newer mkswap require stricter perms
Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
2025-06-18 12:50:07 -04:00
Simon Deziel
677243d5aa snap: switch to core24 base
Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
2025-06-18 12:40:34 -04:00
Vidar Holen
20d11c1c33 Merge branch 'e-kwsm-tautologically-false' 2025-05-17 00:56:52 +00:00
Vidar Holen
47d358c1d4 Tighten SC2333/SC2334 to only trigger against literals. 2025-05-17 00:55:50 +00:00
Vidar Holen
ad58768563 Merge branch 'tautologically-false' of github.com:e-kwsm/shellcheck into e-kwsm-tautologically-false 2025-05-12 17:04:34 +00:00
Vidar Holen
62a8ecf9bf Merge branch 'e-kwsm-SC3013' 2025-04-27 16:16:49 -07:00
Vidar Holen
0b5410d759 Merge pull request #3193 from iehrenwald/master
Add python3 to the list of badShells
2025-04-27 16:09:19 -07:00
iehrenwald
975cfeee50 Merge pull request #1 from iehrenwald/add_python3_badshell
Add python3 to the list of badShells
2025-04-25 14:20:01 -04:00
Ian Ehrenwald
b381658dbc Add python3 to the list of badShells 2025-04-25 14:11:07 -04:00
Vidar Holen
950578ae0e Merge branch 'Flu-ignore-sc2015-true' 2025-04-11 19:15:34 -07:00
Vidar Holen
f78714e0f6 Add ":" alongside "true" for SC2015 2025-04-11 19:14:53 -07:00
Vidar Holen
de07ec1c56 Merge branch 'ignore-sc2015-true' of github.com:Flu/shellcheck into Flu-ignore-sc2015-true 2025-04-11 19:14:15 -07:00
Vidar Holen
85066dd805 Merge remote-tracking branch 'refs/remotes/origin/master' 2025-04-11 14:17:29 -07:00
Vidar Holen
140274b810 Merge branch 'e-kwsm-SC3013-unary' 2025-04-11 14:14:36 -07:00
Vidar Holen
dc41f0cc5b Refactor checks for POSIX test flags 2025-04-11 14:14:09 -07:00
Vidar Holen
fbb8386797 Merge pull request #3170 from e-kwsm/SC3012
fix(SC3012)!: do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
2025-04-09 10:51:44 -07:00
Eisuke Kawashima
efb5a5a274 fix(SC3013): check POSIX-compliant unary operators for test and [
fix #2125
2025-04-09 19:21:53 +09:00
Vidar Holen
553a80f77a Also ignore SC2119 for :? and :+. 2025-04-08 21:21:50 -07:00
Vidar Holen
7fc992d0dc Suppress SC2119/SC2120 for ${1:-default} (fixes #2023) 2025-04-08 20:52:52 -07:00
Vidar Holen
c553288085 Merge pull request #3106 from larryv/updatevars-bash-5.3
Recognize internal variables new in bash 5.3
2025-04-08 20:09:29 -07:00
Vidar Holen
1be41dd652 Merge pull request #3082 from silby/oksh
Recognize "oksh" executable name as ksh
2025-04-08 20:08:53 -07:00
Vidar Holen
2eddec86d3 Merge pull request #3185 from e-kwsm/man
doc: update man
2025-04-08 20:08:05 -07:00
Vidar Holen
c41f3a4b8a Warn about [ ! -o opt ] (and -a) being unconditionally true (fixes #3174) 2025-04-08 10:53:52 -07:00
Vidar Holen
574c6d18fb Suggest using test -e instead of -a (fixes #3174). 2025-04-08 10:23:10 -07:00
Eisuke Kawashima
e4853af5b0 doc: update man 2025-04-08 19:31:58 +09:00
Vidar Holen
72af76f443 Supress SC2093 when execfail is set (fixes #3178) 2025-04-06 19:58:13 -07:00
Vidar Holen
8ff0c5be7a Suppress SC2216 when piping to cp/mv/rm -i (fixes #3141). 2025-04-06 19:27:29 -07:00
Eisuke Kawashima
4f628cbe2a feat: check tautologically-false conditionals
- fix #3179 — negation of SC2055, `[ x = y -a x = z]`
- fix #3181 — negation of SC2056, `(( x == y && x == z ))`
- fix #3180 — negation of SC2252, `[ x = y ] && [ x = z ]`
2025-04-04 18:21:35 +09:00
Eisuke Kawashima
bc60607f9e fix(SC3012)!: do not warn about \< and \> in test/[] as specified in POSIX.1-2024
https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html
fix #3168
2025-03-24 06:32:32 +09:00
Eisuke Kawashima
3a9ddae06b fix(SC3013)!: remove SC3013 since the operators are specified by POSIX.1-2024
https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html
fix #3167
2025-03-24 06:24:12 +09:00
Adrian Fluturel
cbf0b33463 Skip SC2015 when the last command is true 2025-01-07 03:24:29 +01:00
Adrian Fluturel
ad1d5fa64f Fix extendedFunction definition 2024-12-31 05:01:18 +01:00
Adrian Fluturel
34b03040d9 Allow pound symbol only inside the function name 2024-12-31 04:48:19 +01:00
Adrian Fluturel
0d504f44d9 Add bang as a valid char for function names 2024-12-31 03:40:47 +01:00
Lawrence Velázquez
fe315a25c4 Recognize internal variables new in bash 5.3
From the bug-bash@gnu.org announcement "Bash-5.3-beta available":

    q. GLOBSORT: new variable to specify how to sort the results of
       pathname expansion (name, size, blocks, mtime, atime, ctime,
       none) in ascending or descending order.

    w. BASH_MONOSECONDS: new dynamic variable that returns the value of
       the system's monotonic clock, if one is available.

    x. BASH_TRAPSIG: new variable, set to the numeric signal number of
       the trap being executed while it's running.

https://lists.gnu.org/archive/html/bug-bash/2024-12/msg00120.html
2024-12-28 03:20:19 -05:00
Joseph C. Sible
d3001f337a Simplify getParseOutput 2024-12-13 23:57:50 -05:00
Joseph C. Sible
7deb7e853b Use mapM_ instead of sequence_ and <$> 2024-12-13 23:47:55 -05:00
Joseph C. Sible
26b949b9b0 Use mapM_ instead of isJust and fromJust 2024-12-13 23:45:32 -05:00
Joseph C. Sible
5adfea21ee Use the result of the comparison directly instead of an if/else 2024-12-13 23:20:48 -05:00
Joseph C. Sible
0ecaf2b5f1 Use foldr instead of explicit recursion 2024-12-13 23:19:36 -05:00
Joseph C. Sible
195b70db8c Use unless instead of when and not 2024-12-13 23:06:49 -05:00
Vidar Holen
3c75d82db5 Fix stacktest complaining about permissions on /mnt 2024-11-29 13:00:36 -08:00
Vidar Holen
7f3f014d49 Allow latest QuickCheck 2024-11-28 11:51:22 -08:00
Evan Silberman
944d87915a Recognize "oksh" executable name as ksh
A portable version of OpenBSD's ksh is distributed with the executable
name oksh [1]. It's a descendant of pdksh and can be shellchecked as
ksh.

[1]: https://github.com/ibara/oksh
2024-11-11 11:24:21 -08:00
kuraian
f8a3f1922f Update README.md
hyperlink correction: product name and URL updated (I work at trunk.io)
2024-11-04 15:59:12 -08:00
Vidar Holen
47bff1d5fd Add 24.04 to distrotest LTS 2024-11-03 16:54:45 -08:00
Vidar Holen
0ee46a0f33 Update filepath dependency 2024-11-03 14:19:08 -08:00
Vidar Holen
792466bc22 Update Diff dependency (fixes #3075) 2024-11-03 13:56:51 -08:00
Vidar Holen
097018754b Mention that SC2002 (UUOC) is now no longer enabled by default. 2024-10-27 18:10:00 -07:00
Vidar Holen
f2932ebcdc Remember to add changelog to release messages (fixes #3051) 2024-10-27 16:02:56 -07:00
Vidar Holen
5e3e98bcb0 Use CFG to determine use-before-define for SC2218 (fixes #3070) 2024-10-27 15:43:30 -07:00
Vidar Holen
68bc17b8ea Merge pull request #3056 from random1223/patch-1
Update README.md and add Codety into the tool list
2024-10-27 12:47:16 -07:00
Tony
5c2be767ab Update README.md
Add Codety Scanner into the static analysis solution list. 
Here are the examples of the result:
* Codety's pull request code review example: https://github.com/codetyio/codety-scanner/pull/66#issuecomment-2339438925
* Codety's GitHub code scan result example : https://github.com/codetyio/codety-scanner/runs/29907371258

Codety Scanner is open source: https://github.com/codetyio/codety-scanner
2024-09-09 18:56:18 -07:00
Vidar Holen
79e43c4550 Allow parsing arbitrary coproc names (fixes #3048) 2024-09-07 17:14:52 -07:00
Vidar Holen
ca65071d77 Run unit tests in GitHub actions 2024-09-01 14:08:15 -07:00
Vidar Holen
8a1b24c7af Fix paths for CI binary packaging after upgrade 2024-09-01 13:56:44 -07:00
Vidar Holen
88e441453b Make SC2002 optional (useless-use-of-cat) 2024-08-31 18:31:47 -07:00
Vidar Holen
1487e57a46 Suppress unused warnings about stderr and stderr_lines from bats tests, fixing tests. 2024-08-31 18:27:18 -07:00
Vidar Holen
68e6f02267 Expand list of recognized unicode spaces (and rewrite for performance) 2024-08-31 18:00:49 -07:00
Vidar Holen
c7611dfcc6 Use dynamic artifact name to work around issue with v4 uploader 2024-08-19 18:37:29 -07:00
Vidar Holen
15f132e167 Merge pull request #2972 from s1204IT/master
Upgrade build workflow dependencies
2024-08-19 17:48:54 -07:00
Vidar Holen
4e69767b03 Merge pull request #2988 from bryanhonof/bryanhonof.add-flox
Add Flox to list of installation methods
2024-08-19 17:46:55 -07:00
Vidar Holen
8bf8cf5cc7 Merge pull request #3018 from hasit/patch-1
Update README.md to add CodeRabbit to the list of services that use ShellCheck
2024-08-19 17:46:34 -07:00
Vidar Holen
17ebc3dda0 Merge pull request #2973 from jandubois/bats-stderr
Add new bats variables stderr and stderr_lines
2024-08-04 16:52:59 -07:00
Vidar Holen
4cd76283da Merge pull request #3011 from sertonix/busybox-3003
Fix SC3003, SC3036 and SC3045 for busybox shell
2024-08-04 16:52:15 -07:00
Vidar Holen
cd6fdee99b Merge pull request #3034 from dereckson/SC2016-oc
Whitelist oc to avoid SC2016 false positive
2024-08-04 16:50:30 -07:00
Vidar Holen
c831616f3a Merge pull request #3037 from ember91/master
Fix typos and trailing whitespace
2024-08-04 16:49:38 -07:00
Emil Berg
38c5ba7c79 Fix typos and trailing whitespace 2024-08-03 08:49:40 +02:00
Sébastien Santoro
2696c6472d Whitelist oc to avoid SC2016 false positive
Fixes #3033.
2024-07-31 13:33:25 +00:00
Hasit Mistry
d590a35ff8 Update README.md 2024-07-09 14:22:19 -07:00
Sertonix
6d2f3d8628 Allow 'echo -e' in busybox shell 2024-07-09 16:58:50 +02:00
Sertonix
4c85274921 Fix SC3045 for busybox shell 2024-07-09 16:57:44 +02:00
Sertonix
6593096ba0 Allow SC3003 on busybox shell 2024-07-09 16:56:59 +02:00
Joseph C. Sible
98b8dc0720 Use fromList instead of reimplementing it in terms of foldl 2024-07-07 01:28:06 -04:00
Joseph C. Sible
95c0cc2e4b Simplify removeUnnecessaryStructuralNodes 2024-07-07 01:28:06 -04:00
Joseph C. Sible
e5fdec970a Swap the order of the tuple returned by orderEdge 2024-07-07 01:28:06 -04:00
Joseph C. Sible
8746c6e7f2 Switch the order of the maps to avoid unnecessary unionWith instead of union 2024-07-07 01:28:06 -04:00
Joseph C. Sible
61b7e66f80 Use sets instead of maps that never use their values 2024-07-07 01:28:06 -04:00
Joseph C. Sible
b408f54620 Simplify invokedNodes 2024-07-07 01:28:00 -04:00
Vidar Holen
3946cbd4a0 Upgrade docker build images 2024-06-24 05:12:21 +00:00
Vidar Holen
c4b7b79b8b Merge branch 'mengzhuo-main' 2024-06-18 01:53:21 +00:00
Vidar Holen
23e76de4f2 Allow riscv64 image to run without binfmt_misc 2024-06-18 01:52:56 +00:00
Meng Zhuo
15de97e33f Add linux.riscv64 precompiled support 2024-05-30 19:20:21 +08:00
Bryan Honof
78d1ee0222 Add Flox to list of installation methods 2024-05-24 17:15:09 +02:00
Vidar Holen
ac8fb00504 Account for BusyBox support of [[ ]] (fixes #2967) 2024-05-04 16:45:52 -07:00
Vidar Holen
a13cb85f49 Fixed broken test due to bad build cache 2024-05-04 16:34:21 -07:00
Vidar Holen
a7a906e2cb Allow SC2154 to trigger in arrays (fixes #2970) 2024-05-04 16:29:51 -07:00
Vidar Holen
d705716dc4 Account for annotations in SC2215. Fixes #2975. 2024-05-04 15:22:09 -07:00
Vidar Holen
76ff702e93 Supress SC2015 about A && B || C when B is a test. 2024-05-04 15:12:13 -07:00
Vidar Holen
4f81dbe839 Add warning about uninvoked functions, reduce repeated triggering of SC2317 (fixes #2966) 2024-05-04 14:35:26 -07:00
Jan Dubois
796c6bd848 Add new bats variables stderr and stderr_lines
These are being set by `run --separate-stderr` and have been introduced
in https://github.com/bats-core/bats-core/releases/tag/v1.5.0
2024-04-24 19:07:57 -07:00
Syuugo
69fe4e1306 Upgrade build workflow dependencies 2024-04-25 10:35:43 +09:00
Vidar Holen
2c5155e43d Warn about capturing the output of redirected commands. 2024-04-14 18:47:19 -07:00
Vidar Holen
04a86245a1 Remove trailing space in output (fixes #2961) 2024-04-08 20:24:28 -07:00
Vidar Holen
79491db9f6 Merge pull request #2938 from larryv/reword-SC2324
Recommend `typeset` instead of `declare` in SC2324
2024-04-07 13:27:14 -07:00
Vidar Holen
5241878e59 Update Windows build image with new cURL URL 2024-04-05 17:15:04 -07:00
Vidar Holen
30b32af873 Add updating build images to release checks 2024-04-05 17:14:59 -07:00
Vidar Holen
da8854cac6 Merge pull request #2942 from jansorg/fix-builders
Fix builders for Linux
2024-04-04 19:40:13 -07:00
Vidar Holen
39a035793c Merge pull request #2960 from hugos99/patch-1
Update README.md to add macOS Arm64 pre-compiled binaries link
2024-04-04 19:23:28 -07:00
Hugo Sousa
0a7bb1822e Update README.md to add macOS Arm64 pre-compiled binaries link 2024-04-04 12:26:20 +01:00
Joachim Ansorg
c4123375e0 build smaller ShellCheck binary for Linux x86_64 2024-03-12 18:00:36 +01:00
Joachim Ansorg
52dc66349b fix build of linux.aarch64 2024-03-12 17:36:20 +01:00
Lawrence Velázquez
9cb21c8557 Recommend typeset instead of declare in SC2324
Bash has both `typeset` and `declare`, but ksh has `typeset` only.
Recommend the more portable alternative to users.
2024-03-08 18:24:08 -05:00
Vidar Holen
50db9a29c4 Check source details before git details 2024-03-07 19:11:32 -08:00
Vidar Holen
94214ee725 Post-release CHANGELOG 2024-03-07 19:11:12 -08:00
61 changed files with 1129 additions and 461 deletions

View File

@@ -1,6 +1,6 @@
#### For bugs
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a new bug report
title: ''
labels: ''
assignees: ''
---
#### For bugs with existing features
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
#### Here's a snippet or screenshot that shows the problem:
```sh
#!/bin/sh
your script here
```
#### Here's what shellcheck currently says:
#### Here's what I wanted or expected to see:

View File

@@ -0,0 +1,25 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
#### For new checks and feature suggestions
- [ ] 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
#### Here's a snippet or screenshot that shows a potential problem:
```sh
#!/bin/sh
your script here
```
#### Here's what shellcheck currently says:
#### Here's what I wanted to see:

View File

@@ -15,7 +15,7 @@ jobs:
sudo apt-get install cabal-install
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -37,35 +37,58 @@ jobs:
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: source
path: source/
run_tests:
name: Run tests
needs: package_source
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Install dependencies
run: |
sudo apt-get update && sudo apt-get install ghc cabal-install
cabal update
- name: Unpack source
run: |
cd source
tar xvf source.tar.gz --strip-components=1
- name: Build and run tests
run: |
cd source
cabal test
build_source:
name: Build Source Code
name: Build
needs: package_source
strategy:
matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, darwin.aarch64, windows.x86_64]
build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Build source
run: |
mkdir -p bin
mkdir -p bin/${{matrix.build}}
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
( cd bin && ../builders/run_builder ../source/source.tar.gz ../builders/${{matrix.build}} )
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: bin
name: ${{matrix.build}}.bin
path: bin/
package_binary:
@@ -74,25 +97,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Work around GitHub permissions bug
run: chmod +x bin/*/shellcheck*
run: chmod +x *.bin/*/shellcheck*
- name: Package binaries
run: |
export TAGS="$(cat source/tags)"
mkdir -p deploy
cp -r bin/* deploy
cp -r *.bin/* deploy
cd deploy
../.prepare_deploy
rm -rf */ README* LICENSE*
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: deploy
path: deploy/
@@ -109,10 +132,10 @@ jobs:
sudo apt-get install hub
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Upload to GitHub
env:

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ cabal-dev
.cabal-sandbox/
cabal.sandbox.config
cabal.config
cabal.project.freeze
.stack-work
### Snap ###

View File

@@ -80,6 +80,7 @@ function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6'
DOCKER_PLATFORMS+=' linux/riscv64'
multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub

View File

@@ -1,3 +1,34 @@
## v0.11.0 - 2025-08-03
### Added
- SC2327/SC2328: Warn about capturing the output of redirected commands.
- SC2329: Warn when (non-escaping) functions are never invoked.
- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
- SC2331: Suggest using standard -e instead of unary -a in tests.
- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
- SC3062: Warn about bashism `[ -o opt ]`.
- Optional `avoid-negated-conditions`: suggest replacing `[ ! a -eq b ]`
with `[ a -ne b ]`, and similar for -ge/-lt/=/!=/etc (SC2335).
- Precompiled binaries for Linux riscv64 (linux.riscv64)
### Changed
- SC2002 about Useless Use Of Cat is now disabled by default. It can be
re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
- SC2236/SC2237 about replacing `[ ! -n .. ]` with `[ -z ]` and vice versa
is now optional under `avoid-negated-conditions`.
- SC2015 about `A && B || C` no longer triggers when B is a test command.
- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
- Diff output now uses / as path separator on Windows
### Fixed
- SC2218 about function use-before-define is now more accurate.
- SC2317 about unreachable commands is now less spammy for nested ones.
- SC2292, optional suggestion for [[ ]], now triggers for Busybox.
- Updates for Bash 5.3, including `${| cmd; }` and `source -p`
### Removed
- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
## v0.10.0 - 2024-03-07
### Added
- Precompiled binaries for macOS ARM64 (darwin.aarch64)

View File

@@ -5,17 +5,15 @@ ARG tag
# Put the right binary for each architecture into place for the
# multi-architecture docker image.
ARG url_base="https://github.com/koalaman/shellcheck/releases/download/"
RUN set -x; \
arch="$(uname -m)"; \
echo "arch is $arch"; \
if [ "${arch}" = 'armv7l' ]; then \
arch='armv6hf'; \
fi; \
url_base='https://github.com/koalaman/shellcheck/releases/download/'; \
tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \
wget "${url_base}${tar_file}" -O - | tar xJf -; \
mv "shellcheck-${tag}/shellcheck" /bin/; \
rm -rf "shellcheck-${tag}"; \
wget "${url_base}${tar_file}" -O - | tar -C /bin --strip-components=1 -xJf - "shellcheck-${tag}/shellcheck" && \
ls -laF /bin/shellcheck
# ShellCheck image

View File

@@ -110,9 +110,11 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/)
* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner)
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux)
* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
* [Trunk Code Quality](https://trunk.io/code-quality) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
* [CodeRabbit](https://coderabbit.ai/)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@@ -228,11 +230,17 @@ Using the [nix package manager](https://nixos.org/nix):
nix-env -iA nixpkgs.shellcheck
```
Using the [Flox package manager](https://flox.dev/)
```sh
flox install shellcheck
```
Alternatively, you can download pre-compiled binaries for the latest release here:
* [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, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.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)

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.10.0
Version: 0.11.0
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -46,19 +46,19 @@ library
semigroups
build-depends:
-- The lower bounds are based on GHC 7.10.3
-- The upper bounds are based on GHC 9.8.1
-- The upper bounds are based on GHC 9.12.1
aeson >= 1.4.0 && < 2.3,
array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5,
bytestring >= 0.10.6 && < 0.13,
containers >= 0.5.6 && < 0.8,
containers >= 0.5.6 && < 0.9,
deepseq >= 1.4.1 && < 1.6,
Diff >= 0.4.0 && < 0.6,
Diff >= 0.4.0 && < 1.1,
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
filepath >= 1.4.0 && < 1.5,
filepath >= 1.4.0 && < 1.6,
mtl >= 2.2.2 && < 2.4,
parsec >= 3.1.14 && < 3.2,
QuickCheck >= 2.14.2 && < 2.15,
QuickCheck >= 2.14.2 && < 2.17,
regex-tdfa >= 1.2.0 && < 1.4,
transformers >= 0.4.2 && < 0.7,

View File

@@ -1,36 +0,0 @@
FROM ubuntu:20.04
ENV TARGET aarch64-linux-gnu
ENV TARGETNAME linux.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
# The rest are from 22.10
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-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.9.0.0/cabal-install-3.9-x86_64-linux-alpine.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 -optc-fPIC;--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"]

View File

@@ -1,60 +0,0 @@
# 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
RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl
# Copy the build script
WORKDIR /pi/scratch
COPY build /pi/usr/bin
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]

View File

@@ -1,20 +0,0 @@
FROM alpine:latest
ENV TARGETNAME linux.x86_64
# Install GHC and cabal
USER root
RUN apk add ghc cabal g++ libffi-dev curl bash
# 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"]

View File

@@ -1,27 +0,0 @@
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.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.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"]

View File

@@ -11,3 +11,7 @@ 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`.
Tip: Are you developing an image that relies on QEmu usermode emulation?
It's easy to accidentally depend on binfmt\_misc on the host OS.
Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing.

View File

@@ -1,12 +1,12 @@
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest
FROM ghcr.io/shepherdjerred/macos-cross-compiler@sha256:7d40c5e179d5d15453cf2a6b1bba3392bb1448b8257ee6b86021fc905c59dad6
ENV TARGET aarch64-apple-darwin22
ENV TARGETNAME darwin.aarch64
ENV TARGET=aarch64-apple-darwin22
ENV TARGETNAME=darwin.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
ENV LC_ALL C.utf8
ENV DEBIAN_FRONTEND=noninteractive
ENV LC_ALL=C.utf8
# Install basic deps
RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static
@@ -27,7 +27,7 @@ RUN make install
# 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;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0"
ENV CABALOPTS="--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0"
# Prebuild the dependencies
RUN cabal update

View File

@@ -4,7 +4,6 @@ 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"

View File

@@ -1,19 +1,19 @@
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
ENV TARGET x86_64-apple-darwin18
ENV TARGETNAME darwin.x86_64
ENV TARGET=x86_64-apple-darwin18
ENV TARGETNAME=darwin.x86_64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
ENV DEBIAN_FRONTEND=noninteractive
RUN sed -e 's/focal/kinetic/g' -e 's/archive\|security/old-releases/' -i /etc/apt/sources.list
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt-get install -y ghc automake autoconf llvm curl alex happy
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1
RUN ./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
@@ -21,7 +21,7 @@ RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-in
# 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"
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

View File

@@ -4,7 +4,6 @@ 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"

View File

@@ -0,0 +1,35 @@
FROM ubuntu:25.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 llvm-20 "gcc-$TARGET" "g++-$TARGET" ghc alex happy automake autoconf build-essential curl qemu-user-static
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-linux-alpine3_20.tar.xz" | tar xJv -C /usr/local/bin && cabal update
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot.source && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
# GHC fails to build if it can't encode non-ascii
ENV LC_CTYPE=C.utf8
# We have to do a binary-dist instead of a direct install, otherwise the targest won't have
# cross compilation prefixes in /usr/local/lib/aarch64-linux-gnu-ghc-*/lib/settings
RUN ./hadrian/build --flavour=quickest --bignum=native -V -j --prefix=/usr/local install
# Hadrian just outputs "gcc" as the name of gcc, without accounting for $TARGET. Manually fix up the paths:
RUN sed -e 's/"\(gcc\|g++\|ld\)"/"'"$TARGET"'-\1"/g' -i /usr/local/lib/$TARGET-ghc-*/lib/settings
# 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 -optc-fPIC;--with-compiler=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
# 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"]

View File

@@ -4,12 +4,11 @@ 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
"qemu-${TARGET%%-*}-static" "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

View File

@@ -0,0 +1,42 @@
# This Docker file uses a custom QEmu fork with patches to follow execve
# to build all of ShellCheck emulated.
FROM ubuntu:25.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 --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap
WORKDIR /qemu
RUN git clone --depth 1 https://github.com/koalaman/qemu .
RUN ./configure --static --disable-werror && cd build && ninja qemu-arm
ENV QEMU_EXECVE 1
# Convenience utility
COPY scutil /bin/scutil
COPY scutil /chroot/bin/scutil
RUN chmod +x /bin/scutil /chroot/bin/scutil
# Set up an armv6 userspace
WORKDIR /
RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian
RUN cp /qemu/build/qemu-arm /chroot/bin/qemu
RUN scutil emu /debootstrap/debootstrap --second-stage
# Install deps in the chroot
RUN scutil emu apt-get update
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
RUN scutil emu cabal update
# 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"
# Generated with `cabal freeze --constraint 'hashable -arch-native'`
COPY cabal.project.freeze /chroot/etc
RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS
# Copy the build script
COPY build /chroot/bin
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]

View File

@@ -1,8 +1,9 @@
#!/bin/sh
set -xe
cd /scratch
mkdir /scratch && cd /scratch
{
tar xzv --strip-components=1
cp /etc/cabal.project.freeze .
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
# This script does not cabal update because compiling anything new is slow

View File

@@ -0,0 +1,105 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Diff ==1.0.2,
any.OneTuple ==0.4.2,
any.QuickCheck ==2.16.0.0,
QuickCheck -old-random +templatehaskell,
any.StateVar ==1.2.2,
any.aeson ==2.2.3.0,
aeson +ordered-keymap,
any.ansi-terminal ==1.1.3,
ansi-terminal -example,
any.ansi-terminal-types ==1.1.3,
any.array ==0.5.4.0,
any.assoc ==1.1.1,
assoc -tagged,
any.base ==4.15.1.0,
any.base-orphans ==0.9.3,
any.bifunctors ==5.6.2,
bifunctors +tagged,
any.binary ==0.8.8.0,
any.bytestring ==0.10.12.1,
any.character-ps ==0.1,
any.colour ==2.3.6,
any.comonad ==5.0.9,
comonad +containers +distributive +indexed-traversable,
any.containers ==0.6.4.1,
any.contravariant ==1.5.5,
contravariant +semigroups +statevar +tagged,
any.data-array-byte ==0.1.0.1,
any.data-fix ==0.3.4,
any.deepseq ==1.4.5.0,
any.directory ==1.3.6.2,
any.distributive ==0.6.2.1,
distributive +semigroups +tagged,
any.dlist ==1.0,
dlist -werror,
any.exceptions ==0.10.4,
any.fgl ==5.8.3.0,
fgl +containers042,
any.filepath ==1.4.2.1,
any.foldable1-classes-compat ==0.1.2,
foldable1-classes-compat +tagged,
any.generically ==0.1.1,
any.ghc-bignum ==1.1,
any.ghc-boot-th ==9.0.2,
any.ghc-prim ==0.7.0,
any.hashable ==1.4.7.0,
hashable -arch-native +integer-gmp -random-initial-seed,
any.indexed-traversable ==0.1.4,
any.indexed-traversable-instances ==0.1.2,
any.integer-conversion ==0.1.1,
any.integer-logarithms ==1.0.4,
integer-logarithms -check-bounds +integer-gmp,
any.mtl ==2.2.2,
any.network-uri ==2.6.4.2,
any.optparse-applicative ==0.19.0.0,
optparse-applicative +process,
any.parsec ==3.1.14.0,
any.pretty ==1.1.3.6,
any.prettyprinter ==1.7.1,
prettyprinter -buildreadme +text,
any.prettyprinter-ansi-terminal ==1.1.3,
any.primitive ==0.9.1.0,
any.process ==1.6.13.2,
any.random ==1.3.1,
any.regex-base ==0.94.0.3,
any.regex-tdfa ==1.3.2.4,
regex-tdfa +doctest -force-o2,
any.rts ==1.0.2,
any.scientific ==0.3.8.0,
scientific -integer-simple,
any.semialign ==1.3.1,
semialign +semigroupoids,
any.semigroupoids ==6.0.1,
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
any.splitmix ==0.1.3.1,
splitmix -optimised-mixer,
any.stm ==2.5.0.0,
any.strict ==0.5.1,
any.tagged ==0.8.9,
tagged +deepseq +transformers,
any.tasty ==1.5.3,
tasty +unix,
any.template-haskell ==2.17.0.0,
any.text ==1.2.5.0,
any.text-iso8601 ==0.1.1,
any.text-short ==0.1.6,
text-short -asserts,
any.th-abstraction ==0.7.1.0,
any.th-compat ==0.1.6,
any.these ==1.2.1,
any.time ==1.9.3,
any.time-compat ==1.9.8,
any.transformers ==0.5.6.2,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
any.unbounded-delays ==0.1.1.1,
any.unix ==2.7.2.2,
any.unordered-containers ==0.2.20,
unordered-containers -debug,
any.uuid-types ==1.0.6,
any.vector ==0.13.2.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-stream ==0.1.0.1,
any.witherable ==0.5
index-state: hackage.haskell.org 2025-07-22T18:12:16Z

View File

@@ -0,0 +1,48 @@
#!/bin/dash
# Various ShellCheck build utility functions
# Generally set a ulimit to avoid QEmu using too much memory
ulimit -v "$((10*1024*1024))"
# If we happen to invoke or run under QEmu, make sure to follow execve.
# This requires a patched QEmu.
export QEMU_EXECVE=1
# Retry a command until it succeeds
# Usage: scutil retry 3 mycmd
retry() {
n="$1"
ret=1
shift
while [ "$n" -gt 0 ]
do
"$@"
ret=$?
[ "$ret" = 0 ] && break
n=$((n-1))
done
return "$ret"
}
# Install all dependencies from a freeze file
# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install
install_from_freeze() {
linefeed=$(printf '\nx')
linefeed=${linefeed%x}
flags=$(
sed 's/constraints:/&\n /' "$1" |
grep -vw -e rts -e base -e ghc |
sed -n -e 's/^ *\([^,]*\).*/\1/p' |
sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e')
shift
# shellcheck disable=SC2086
( IFS=$linefeed; set -x; "$@" $flags )
}
# Run a command under emulation.
# This assumes the correct emulator is named 'qemu' and the chroot is /chroot
# Usage: scutil emu echo "Hello World"
emu() {
chroot /chroot /bin/qemu /usr/bin/env "$@"
}
"$@"

View File

@@ -0,0 +1,35 @@
FROM ubuntu:25.04
ENV TARGETNAME=linux.riscv64
ENV TARGET=riscv64-linux-gnu
# Build dependencies
USER root
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y llvm-20 "gcc-$TARGET" "g++-$TARGET" ghc alex happy automake autoconf build-essential curl qemu-user-static
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-linux-alpine3_20.tar.xz" | tar xJv -C /usr/local/bin && cabal update
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot.source && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
# GHC fails to build if it can't encode non-ascii
ENV LC_CTYPE=C.utf8
# We have to do a binary-dist instead of a direct install, otherwise the targest won't have
# cross compilation prefixes in /usr/local/lib/aarch64-linux-gnu-ghc-*/lib/settings
RUN ./hadrian/build --flavour=quickest --bignum=native -V -j --prefix=/usr/local install
# Hadrian just outputs "gcc" as the name of gcc, without accounting for $TARGET. Manually fix up the paths:
RUN sed -e 's/"\(gcc\|g++\|ld\)"/"'"$TARGET"'-\1"/g' -i /usr/local/lib/$TARGET-ghc-*/lib/settings
# 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 -optc-fPIC;--with-compiler=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
# 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
builders/linux.riscv64/build Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
( 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-${TARGET%%-*}-static" "$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

View File

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

View File

@@ -0,0 +1,30 @@
FROM alpine:3.22
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes (certs expired)
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes (certs expired)
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes (certs expired)
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes (certs expired)
# alpine:3.20 (GHC 9.8.2): 16.0 megabytes
# alpine:3.21 (GHC 9.8.2): 16.0 megabytes
# alpine:3.22 (GHC 9.8.2): 16.0 megabytes
ENV TARGETNAME=linux.x86_64
# Install GHC and cabal
USER root
RUN apk add ghc cabal g++ libffi-dev curl bash gmp gmp-static
# Cabal has failed to cache if options are not specified on the command line,
# so do that explicitly.
ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Verify that we have the certificates in place to successfully update cabal
RUN cabal update && rm -rf ~/.cabal
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
RUN true
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

@@ -0,0 +1,34 @@
FROM ubuntu:25.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 xz-utils
# Fetch Windows version, will be available under z:\haskell
WORKDIR /haskell
# 9.12.2 produces a 37M binary
# 9.0.2 produces a 28M binary
# 8.10.4 produces a 16M binary
# We don't want to be stuck on old versions forever though, so just go with the latest version
RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
# Fetch dependencies
WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-windows.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-8.15.0_2/curl-8.15.0_2-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
RUN wine /haskell/bin/cabal.exe update
ENV WINEPATH=/haskell/bin:/haskell/mingw/bin
# None of these actually seem to have an effect on GHC on Windows anymore,
# but we'll leave them in place anyways.
ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds
RUN IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

@@ -8,7 +8,6 @@ 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"

View File

@@ -78,7 +78,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Don't try to look for .shellcheckrc configuration files.
--rcfile\ RCFILE
**--rcfile** *RCFILE*
: Prefer the specified configuration file over searching for one
in the default locations.
@@ -317,7 +317,7 @@ Here is an example `.shellcheckrc`:
disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the XDG config directory
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME`
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used.
@@ -397,10 +397,10 @@ long list of wonderful contributors.
# COPYRIGHT
Copyright 2012-2024, Vidar Holen and contributors.
Copyright 2012-2025, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html
# SEE ALSO
sh(1) bash(1)
sh(1), bash(1), dash(1), ksh(1)

View File

@@ -23,7 +23,7 @@ description: |
# snap connect shellcheck:removable-media
version: git
base: core20
base: core24
grade: stable
confinement: strict
@@ -40,17 +40,18 @@ parts:
source: .
build-packages:
- cabal-install
stage-packages:
- libatomic1
override-build: |
# Give ourselves enough memory to build
dd if=/dev/zero of=/tmp/swap bs=1M count=2000
fallocate -l 2G /tmp/swap
chmod 0600 /tmp/swap
mkswap /tmp/swap
swapon /tmp/swap
if ! swapon /tmp/swap; then
echo "Could not enable swap file, continuing anyway"
rm /tmp/swap
fi
cabal sandbox init
cabal update
cabal install -j
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin
install -d "${CRAFT_PART_INSTALL}/usr/bin"
install --strip ~/.cabal/bin/shellcheck "${CRAFT_PART_INSTALL}/usr/bin"

View File

@@ -31,6 +31,7 @@ newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq)
data Piped = Piped | Unpiped deriving (Show, Eq)
data AssignmentMode = Assign | Append deriving (Show, Eq)
newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
@@ -84,7 +85,7 @@ data InnerToken t =
| Inner_T_DollarDoubleQuoted [t]
| Inner_T_DollarExpansion [t]
| Inner_T_DollarSingleQuoted String
| Inner_T_DollarBraceCommandExpansion [t]
| Inner_T_DollarBraceCommandExpansion Piped [t]
| Inner_T_Done
| Inner_T_DoubleQuoted [t]
| Inner_T_EOF
@@ -138,7 +139,7 @@ data InnerToken t =
| Inner_T_WhileExpression [t] [t]
| Inner_T_Annotation [Annotation] t
| Inner_T_Pipe String
| Inner_T_CoProc (Maybe String) t
| Inner_T_CoProc (Maybe Token) t
| Inner_T_CoProcBody t
| Inner_T_Include t
| Inner_T_SourceCommand t t
@@ -206,7 +207,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t)
pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t)
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
@@ -228,7 +229,7 @@ pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)
pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)
pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)
pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)
pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)
pattern T_DollarBraceCommandExpansion id pipe list = OuterToken id (Inner_T_DollarBraceCommandExpansion pipe list)
pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)
pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)
pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)
@@ -259,7 +260,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parentesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parenthesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
instance Eq Token where
OuterToken _ a == OuterToken _ b = a == b

View File

@@ -446,6 +446,12 @@ getLiteralStringExt more = g
-- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t
-- Is this token a string literal number?
isLiteralNumber t = fromMaybe False $ do
s <- getLiteralString t
guard $ all isDigit s
return True
-- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage
@@ -555,7 +561,7 @@ getCommandNameFromExpansion t =
case t of
T_DollarExpansion _ [c] -> extract c
T_Backticked _ [c] -> extract c
T_DollarBraceCommandExpansion _ [c] -> extract c
T_DollarBraceCommandExpansion _ _ [c] -> extract c
_ -> Nothing
where
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
@@ -610,7 +616,7 @@ getCommandSequences t =
T_Annotation _ _ t -> getCommandSequences t
T_DollarExpansion _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ _ cmds -> [cmds]
T_Backticked _ cmds -> [cmds]
_ -> []

View File

@@ -103,8 +103,7 @@ nodeChecksToTreeCheck checkList =
nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()]
nodeChecks = [
checkUuoc
,checkPipePitfalls
checkPipePitfalls
,checkForInQuoted
,checkForInLs
,checkShorthandIf
@@ -124,6 +123,7 @@ nodeChecks = [
,checkCaseAgainstGlob
,checkCommarrays
,checkOrNeq
,checkAndEq
,checkEchoWc
,checkConstantIfs
,checkPipedAssignment
@@ -183,7 +183,6 @@ nodeChecks = [
,checkPipeToNowhere
,checkForLoopGlobVariables
,checkSubshelledTests
,checkInvertedStringTest
,checkRedirectionToCommand
,checkDollarQuoteParen
,checkUselessBang
@@ -204,6 +203,8 @@ nodeChecks = [
,checkUnnecessaryArithmeticExpansionIndex
,checkUnnecessaryParens
,checkPlusEqualsNumber
,checkExpansionWithRedirection
,checkUnaryTestA
]
optionalChecks = map fst optionalTreeChecks
@@ -231,6 +232,13 @@ optionalTreeChecks = [
cdNegative = "[ -n \"$var\" ]"
}, nodeChecksToTreeCheck [checkNullaryExpansionTest])
,(newCheckDescription {
cdName = "avoid-negated-conditions",
cdDescription = "Suggest removing unnecessary comparison negations",
cdPositive = "[ ! \"$var\" -eq 1 ]",
cdNegative = "[ \"$var\" -ne 1 ]"
}, nodeChecksToTreeCheck [checkUnnecessarilyInvertedTest])
,(newCheckDescription {
cdName = "add-default-case",
cdDescription = "Suggest adding a default case in `case` statements",
@@ -272,6 +280,13 @@ optionalTreeChecks = [
cdPositive = "rm -r \"$(get_chroot_dir)/home\"",
cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\""
}, checkExtraMaskedReturns)
,(newCheckDescription {
cdName = "useless-use-of-cat",
cdDescription = "Check for Useless Use Of Cat (UUOC)",
cdPositive = "cat foo | grep bar",
cdNegative = "grep bar foo"
}, nodeChecksToTreeCheck [checkUuoc])
]
optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment])
@@ -490,15 +505,12 @@ checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _
sequence_ $ do
str <- getNormalString val
var:op:_ <- matchRegex regex str
Map.lookup var references
guard $ S.member var references
return . warn (getId val) 2100 $
"Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))"
where
regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$"
references = foldl (flip ($)) Map.empty (map insertRef $ variableFlow params)
insertRef (Assignment (_, _, name, _)) =
Map.insert name ()
insertRef _ = Prelude.id
references = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params]
getNormalString (T_NormalWord _ words) = do
parts <- mapM getLiterals words
@@ -794,7 +806,7 @@ checkUnquotedExpansions params =
where
check t@(T_DollarExpansion _ c) = examine t c
check t@(T_Backticked _ c) = examine t c
check t@(T_DollarBraceCommandExpansion _ c) = examine t c
check t@(T_DollarBraceCommandExpansion _ _ c) = examine t c
check _ = return ()
tree = parentMap params
examine t contents =
@@ -876,13 +888,16 @@ prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b"
prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi"
prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done"
prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi"
checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ _) (T_Pipeline _ _ t))
| not (isOk t || inCondition) =
prop_checkShorthandIf9 = verifyNot checkShorthandIf "foo && [ -x /file ] || bar"
prop_checkShorthandIf10 = verifyNot checkShorthandIf "foo && bar || true"
prop_checkShorthandIf11 = verify checkShorthandIf "foo && bar || false"
checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ b) (T_Pipeline _ _ t))
| not (isOk t || inCondition) && not (isTestCommand b) =
info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true."
where
isOk [t] = isAssignment t || fromMaybe False (do
name <- getCommandBasename t
return $ name `elem` ["echo", "exit", "return", "printf"])
return $ name `elem` ["echo", "exit", "return", "printf", "true", ":"])
isOk _ = False
inCondition = isCondition $ getPath (parentMap params) x
checkShorthandIf _ _ = return ()
@@ -972,32 +987,32 @@ prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr
prop_checkArrayWithoutIndex10 = verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\""
prop_checkArrayWithoutIndex11 = verifyNotTree checkArrayWithoutIndex "read -rpfoobar r; r=42"
checkArrayWithoutIndex params _ =
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
doVariableFlowAnalysis readF writeF defaultSet (variableFlow params)
where
defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables
defaultSet = S.fromList arrayVariables
readF _ (T_DollarBraced id _ token) _ = do
map <- get
s <- get
return . maybeToList $ do
name <- getLiteralString token
assigned <- Map.lookup name map
guard $ S.member name s
return $ makeComment WarningC id 2128
"Expanding an array without an index only gives the first element."
readF _ _ _ = return []
writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do
isArray <- gets (Map.member name)
isArray <- gets (S.member name)
return $ if not isArray then [] else
case mode of
Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."]
Append -> [makeComment WarningC id 2179 "Use array+=(\"item\") to append items to an array."]
writeF _ t name (DataArray _) = do
modify (Map.insert name ())
modify (S.insert name)
return []
writeF _ expr name _ = do
if isIndexed expr
then modify (Map.insert name ())
else modify (Map.delete name)
then modify (S.insert name)
else modify (S.delete name)
return []
isIndexed expr =
@@ -1096,8 +1111,11 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
,"xprop"
,"alias"
,"sudo" -- covering "sudo sh" and such
,"doas" -- same as sudo
,"run0" -- same as sudo
,"docker" -- like above
,"podman"
,"oc"
,"dpkg-query"
,"jq" -- could also check that user provides --arg
,"rename"
@@ -1523,6 +1541,7 @@ prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
prop_checkComparisonAgainstGlob7 = verify checkComparisonAgainstGlob "#!/bin/busybox sh\n[[ $f == *foo* ]]"
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _ _]))
| op `elem` ["=", "==", "!="] =
warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
@@ -1530,10 +1549,14 @@ checkComparisonAgainstGlob params (TC_Binary _ SingleBracket op _ word)
| op `elem` ["=", "==", "!="] && isGlob word =
err (getId word) 2081 msg
where
msg = if isBashLike params
msg = if (shellType params) `elem` [Bash, Ksh] -- Busybox does not support glob matching
then "[ .. ] can't match globs. Use [[ .. ]] or case statement."
else "[ .. ] can't match globs. Use a case statement."
checkComparisonAgainstGlob params (TC_Binary _ DoubleBracket op _ word)
| shellType params == BusyboxSh && op `elem` ["=", "==", "!="] && isGlob word =
err (getId word) 2330 "BusyBox [[ .. ]] does not support glob matching. Use a case statement."
checkComparisonAgainstGlob _ _ = return ()
prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac"
@@ -1620,6 +1643,64 @@ checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do
checkOrNeq _ _ = return ()
prop_checkAndEq1 = verifyNot checkAndEq "cow=0; foo=0; if [[ $lol -eq cow && $lol -eq foo ]]; then echo foo; fi"
prop_checkAndEq2 = verifyNot checkAndEq "lol=0 foo=0; (( a==lol && a==foo ))"
prop_checkAndEq3 = verify checkAndEq "[ \"$a\" = lol && \"$a\" = foo ]"
prop_checkAndEq4 = verifyNot checkAndEq "[ a = $cow && b = $foo ]"
prop_checkAndEq5 = verifyNot checkAndEq "[[ $a = /home && $a = */public_html/* ]]"
prop_checkAndEq6 = verify checkAndEq "[ $a = a ] && [ $a = b ]"
prop_checkAndEq7 = verify checkAndEq "[ $a = a ] && [ $a = b ] || true"
prop_checkAndEq8 = verifyNot checkAndEq "[[ $a == x && $a == x ]]"
prop_checkAndEq9 = verifyNot checkAndEq "[ 0 -eq $FOO ] && [ 0 -eq $BAR ]"
prop_checkAndEq10 = verify checkAndEq "(( a == 1 && a == 2 ))"
prop_checkAndEq11 = verify checkAndEq "[ $x -eq 1 ] && [ $x -eq 2 ]"
prop_checkAndEq12 = verify checkAndEq "[ 1 -eq $x ] && [ $x -eq 2 ]"
prop_checkAndEq13 = verifyNot checkAndEq "[ 1 -eq $x ] && [ $x -eq 1 ]"
prop_checkAndEq14 = verifyNot checkAndEq "[ $a = $b ] && [ $a = $c ]"
checkAndEqOperands "-eq" rhs1 rhs2 = isLiteralNumber rhs1 && isLiteralNumber rhs2
checkAndEqOperands op rhs1 rhs2 | op == "=" || op == "==" = isLiteral rhs1 && isLiteral rhs2
checkAndEqOperands _ _ _ = False
-- For test-level "and": [ x = y -a x = z ]
checkAndEq _ (TC_And id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2))
| op1 == op2 && lhs1 == lhs2 && rhs1 /= rhs2 && checkAndEqOperands op1 rhs1 rhs2 =
warn id 2333 $ "You probably wanted " ++ (if typ == SingleBracket then "-o" else "||") ++ " here, otherwise it's always false."
-- For arithmetic context "and"
checkAndEq _ (TA_Binary id "&&" (TA_Binary _ "==" lhs1 rhs1) (TA_Binary _ "==" lhs2 rhs2))
| lhs1 == lhs2 && isLiteralNumber rhs1 && isLiteralNumber rhs2 =
warn id 2334 "You probably wanted || here, otherwise it's always false."
-- For command level "and": [ x = y ] && [ x = z ]
checkAndEq _ (T_AndIf id lhs rhs) = sequence_ $ do
(lhs1, op1, rhs1) <- getExpr lhs
(lhs2, op2, rhs2) <- getExpr rhs
guard $ op1 == op2
guard $ lhs1 == lhs2 && rhs1 /= rhs2
guard $ checkAndEqOperands op1 rhs1 rhs2
return $ warn id 2333 "You probably wanted || here, otherwise it's always false."
where
getExpr x =
case x of
T_AndIf _ lhs _ -> getExpr lhs -- Fetches x and y in `T_AndIf x (T_AndIf y z)`
T_Pipeline _ _ [x] -> getExpr x
T_Redirecting _ _ c -> getExpr c
T_Condition _ _ c -> getExpr c
TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs)
_ -> Nothing
-- Swap items so that the constant side is rhs (or Nothing if both/neither is constant)
orient (lhs, op, rhs) =
case (isConstant lhs, isConstant rhs) of
(True, False) -> return (rhs, op, lhs)
(False, True) -> return (lhs, op, rhs)
_ -> Nothing
checkAndEq _ _ = return ()
prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]"
prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]"
prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]"
@@ -1885,7 +1966,9 @@ prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.l
prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done"
prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r"
prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :"
checkSpuriousExec _ = doLists
prop_checkSpuriousExec12 = verifyNot checkSpuriousExec "#!/bin/bash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;"
prop_checkSpuriousExec13 = verify checkSpuriousExec "#!/bin/dash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;"
checkSpuriousExec params t = when (not $ hasExecfail params) $ doLists t
where
doLists (T_Script _ _ cmds) = doList cmds False
doLists (T_BraceGroup _ cmds) = doList cmds False
@@ -2255,7 +2338,7 @@ prop_checkFunctionsUsedExternally2c =
prop_checkFunctionsUsedExternally3 =
verifyNotTree checkFunctionsUsedExternally "f() { :; }; echo f"
prop_checkFunctionsUsedExternally4 =
verifyNotTree checkFunctionsUsedExternally "foo() { :; }; sudo \"foo\""
verifyNotTree checkFunctionsUsedExternally "foo() { :; }; run0 \"foo\""
prop_checkFunctionsUsedExternally5 =
verifyTree checkFunctionsUsedExternally "foo() { :; }; ssh host foo"
prop_checkFunctionsUsedExternally6 =
@@ -2265,7 +2348,7 @@ prop_checkFunctionsUsedExternally7 =
prop_checkFunctionsUsedExternally8 =
verifyTree checkFunctionsUsedExternally "foo() { :; }; command sudo foo"
prop_checkFunctionsUsedExternally9 =
verifyTree checkFunctionsUsedExternally "foo() { :; }; exec -c sudo foo"
verifyTree checkFunctionsUsedExternally "foo() { :; }; exec -c doas foo"
checkFunctionsUsedExternally params t =
runNodeAnalysis checkCommand params t
where
@@ -2289,6 +2372,8 @@ checkFunctionsUsedExternally params t =
"chroot" -> firstNonFlag
"screen" -> firstNonFlag
"sudo" -> firstNonFlag
"doas" -> firstNonFlag
"run0" -> firstNonFlag
"xargs" -> firstNonFlag
"tmux" -> firstNonFlag
"ssh" -> take 1 $ drop 1 $ dropFlags argAndString
@@ -2373,15 +2458,9 @@ prop_checkUnused51 = verifyTree checkUnusedAssignments "x[y[z=1]]=1; echo ${x[@]
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
where
flow = variableFlow params
references = foldl (flip ($)) defaultMap (map insertRef flow)
insertRef (Reference (base, token, name)) =
Map.insert (stripSuffix name) ()
insertRef _ = id
references = Map.union (Map.fromList [(stripSuffix name, ()) | Reference (base, token, name) <- flow]) defaultMap
assignments = foldl (flip ($)) Map.empty (map insertAssignment flow)
insertAssignment (Assignment (_, token, name, _)) | isVariableName name =
Map.insert name token
insertAssignment _ = id
assignments = Map.fromList [(name, token) | Assignment (_, token, name, _) <- flow, isVariableName name]
unused = Map.assocs $ Map.difference assignments references
@@ -2445,6 +2524,7 @@ prop_checkUnassignedReferences_minusZDefault = verifyNotTree checkUnassignedRefe
prop_checkUnassignedReferences50 = verifyNotTree checkUnassignedReferences "echo ${foo:+bar}"
prop_checkUnassignedReferences51 = verifyNotTree checkUnassignedReferences "echo ${foo:+$foo}"
prop_checkUnassignedReferences52 = verifyNotTree checkUnassignedReferences "wait -p pid; echo $pid"
prop_checkUnassignedReferences53 = verifyTree checkUnassignedReferences "x=($foo)"
checkUnassignedReferences = checkUnassignedReferences' False
checkUnassignedReferences' includeGlobals params t = warnings
@@ -2500,14 +2580,12 @@ checkUnassignedReferences' includeGlobals params t = warnings
warnings = execWriter . sequence $ mapMaybe warningFor unassigned
-- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.
-- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.
-- ${foo[bar baz]} may not be referencing bar/baz. Just skip these.
-- We can also have ${foo:+$foo} should be treated like [[ -n $foo ]] && echo $foo
isException var t = any shouldExclude $ getPath (parentMap params) t
where
shouldExclude t =
case t of
T_Array {} -> True
(T_DollarBraced _ _ l) ->
let str = concat $ oversimplify l
ref = getBracedReference str
@@ -2818,6 +2896,10 @@ prop_checkUnpassedInFunctions11 = verifyNotTree checkUnpassedInFunctions "foo()
prop_checkUnpassedInFunctions12 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;"
prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n"
prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo"
prop_checkUnpassedInFunctions15 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1-x}; }; foo"
prop_checkUnpassedInFunctions16 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1:-x}; }; foo"
prop_checkUnpassedInFunctions17 = verifyNotTree checkUnpassedInFunctions "foo() { mycommand ${1+--verbose}; }; foo"
prop_checkUnpassedInFunctions18 = verifyNotTree checkUnpassedInFunctions "foo() { if mycheck; then foo ${1?Missing}; fi; }; foo"
checkUnpassedInFunctions params root =
execWriter $ mapM_ warnForGroup referenceGroups
where
@@ -2834,9 +2916,10 @@ checkUnpassedInFunctions params root =
case x of
Assignment (_, _, str, _) -> isPositional str
_ -> False
isPositionalReference function x =
case x of
Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function
Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function && not (hasDefaultValue t)
_ -> False
isDirectChildOf child parent = fromMaybe False $ do
@@ -2850,6 +2933,7 @@ checkUnpassedInFunctions params root =
referenceList :: [(String, Bool, Token)]
referenceList = execWriter $
doAnalysis (sequence_ . checkCommand) root
checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())
checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do
str <- getLiteralString cmd
@@ -2860,6 +2944,22 @@ checkUnpassedInFunctions params root =
isPositional str = str == "*" || str == "@" || str == "#"
|| (all isDigit str && str /= "0" && str /= "")
-- True if t is a variable that specifies a default value,
-- such as ${1-x} or ${1:-x}.
hasDefaultValue t =
case t of
T_DollarBraced _ True l ->
let str = concat $ oversimplify l
in isDefaultValueModifier $ getBracedModifier str
_ -> False
isDefaultValueModifier str =
case str of
':':c:_ -> c `elem` handlesDefault
c:_ -> c `elem` handlesDefault
_ -> False
where handlesDefault = "-+?"
isArgumentless (_, b, _) = b
referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList
updateWith x@(name, _, _) = Map.insertWith (++) name [x]
@@ -2922,7 +3022,8 @@ checkTildeInPath _ _ = return ()
prop_checkUnsupported3 = verify checkUnsupported "#!/bin/sh\ncase foo in bar) baz ;& esac"
prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac"
prop_checkUnsupported5 = verify checkUnsupported "#!/bin/bash\necho \"${ ls; }\""
prop_checkUnsupported5 = verifyNot checkUnsupported "#!/bin/bash\necho \"${ ls; }\""
prop_checkUnsupported6 = verify checkUnsupported "#!/bin/ash\necho \"${ ls; }\""
checkUnsupported params t =
unless (null support || (shellType params `elem` support)) $
report name
@@ -2936,7 +3037,7 @@ checkUnsupported params t =
shellSupport t =
case t of
T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list)
T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Ksh])
T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Bash, Ksh])
_ -> ("", [])
where
forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash])
@@ -3024,7 +3125,7 @@ checkShouldUseGrepQ params t =
T_DollarExpansion _ [x] -> getPipeline x
T_Pipeline _ _ cmds -> return cmds
_ -> fail "unknown"
isGrep = (`elem` ["grep", "egrep", "fgrep", "zgrep"])
isGrep = (`elem` ["grep", "egrep", "fgrep", "bz3grep", "bzgrep", "xzgrep", "zgrep", "zipgrep", "zstdgrep"])
prop_checkTestArgumentSplitting1 = verify checkTestArgumentSplitting "[ -e *.mp3 ]"
prop_checkTestArgumentSplitting2 = verifyNot checkTestArgumentSplitting "[[ $a == *b* ]]"
@@ -3297,7 +3398,7 @@ checkReturnAgainstZero params token =
next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next
next@(TC_Group {}):_ -> isOnlyTestInCommand next
next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next
next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next
next@(TA_Parenthesis _ _):_ -> isOnlyTestInCommand next
_ -> False
-- TODO: Do better $? tracking and filter on whether
@@ -3516,7 +3617,7 @@ checkSplittingInArrays params t =
_ -> return ()
checkPart part = case part of
T_DollarExpansion id _ -> forCommand id
T_DollarBraceCommandExpansion id _ -> forCommand id
T_DollarBraceCommandExpansion id _ _ -> forCommand id
T_Backticked id _ -> forCommand id
T_DollarBraced id _ str |
not (isCountingReference part)
@@ -3592,6 +3693,8 @@ prop_checkPipeToNowhere17 = verify checkPipeToNowhere "echo World | cat << 'EOF'
prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l"
prop_checkPipeToNowhere19 = verifyNot checkPipeToNowhere "find . -print0 | du --files0-from=/dev/stdin"
prop_checkPipeToNowhere20 = verifyNot checkPipeToNowhere "find . | du --exclude-from=/dev/fd/0"
prop_checkPipeToNowhere21 = verifyNot checkPipeToNowhere "yes | cp -ri foo/* bar"
prop_checkPipeToNowhere22 = verifyNot checkPipeToNowhere "yes | rm --interactive *"
data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq)
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
@@ -3657,6 +3760,7 @@ checkPipeToNowhere params t =
commandSpecificException name cmd =
case name of
"du" -> any ((`elem` ["exclude-from", "files0-from"]) . snd) $ getAllFlags cmd
_ | name `elem` interactiveFlagCmds -> hasInteractiveFlag cmd
_ -> False
warnAboutDupes (n, list@(_:_:_)) =
@@ -3680,7 +3784,7 @@ checkPipeToNowhere params t =
name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd
guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i"
guard . not $ name `elem` interactiveFlagCmds && hasInteractiveFlag cmd
let suggestion =
if name == "echo"
then "Did you want 'cat' instead?"
@@ -3695,6 +3799,9 @@ checkPipeToNowhere params t =
treeContains pred t = isNothing $
doAnalysis (guard . not . pred) t
interactiveFlagCmds = [ "cp", "mv", "rm" ]
hasInteractiveFlag cmd = cmd `hasFlag` "i" || cmd `hasFlag` "interactive"
mayConsume t =
case t of
T_ProcSub _ "<" _ -> True
@@ -3761,32 +3868,32 @@ prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { t
prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f"
prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi"
prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }"
checkUseBeforeDefinition _ t =
execWriter $ evalStateT (mapM_ examine $ revCommands) Map.empty
prop_checkUseBeforeDefinition5 = verifyTree checkUseBeforeDefinition "false || mycmd; mycmd() { f; }"
prop_checkUseBeforeDefinition6 = verifyNotTree checkUseBeforeDefinition "f() { one; }; f; f() { two; }; f"
checkUseBeforeDefinition :: Parameters -> Token -> [TokenComment]
checkUseBeforeDefinition params t = fromMaybe [] $ do
cfga <- cfgAnalysis params
let funcs = execState (doAnalysis findFunction t) Map.empty
-- Green cut: no point enumerating commands if there are no functions.
guard . not $ Map.null funcs
return $ execWriter $ doAnalysis (findInvocation cfga funcs) t
where
examine t = case t of
T_Pipeline _ _ [T_Redirecting _ _ (T_Function _ _ _ name _)] ->
modify $ Map.insert name t
T_Annotation _ _ w -> examine w
T_Pipeline _ _ cmds -> do
m <- get
unless (Map.null m) $
mapM_ (checkUsage m) $ concatMap recursiveSequences cmds
_ -> return ()
findFunction t =
case t of
T_Function id _ _ name _ -> modify (Map.insertWith (++) name [id])
_ -> return ()
checkUsage map cmd = sequence_ $ do
name <- getCommandName cmd
def <- Map.lookup name map
return $
err (getId cmd) 2218
"This function is only defined later. Move the definition up."
revCommands = reverse $ concat $ getCommandSequences t
recursiveSequences x =
let list = concat $ getCommandSequences x in
if null list
then [x]
else concatMap recursiveSequences list
findInvocation cfga funcs t =
case t of
T_SimpleCommand id _ (cmd:_) -> sequence_ $ do
name <- getLiteralString cmd
invocations <- Map.lookup name funcs
-- Is the function definitely being defined later?
guard $ any (\c -> CF.doesPostDominate cfga c id) invocations
-- Was one already defined, so it's actually a re-definition?
guard . not $ any (\c -> CF.doesPostDominate cfga id c) invocations
return $ err id 2218 "This function is only defined later. Move the definition up."
_ -> return ()
prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done"
prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done"
@@ -3890,12 +3997,17 @@ checkSubshelledTests params t =
T_Annotation {} -> True
_ -> False
prop_checkInvertedStringTest1 = verify checkInvertedStringTest "[ ! -z $var ]"
prop_checkInvertedStringTest2 = verify checkInvertedStringTest "! [[ -n $var ]]"
prop_checkInvertedStringTest3 = verifyNot checkInvertedStringTest "! [ -x $var ]"
prop_checkInvertedStringTest4 = verifyNot checkInvertedStringTest "[[ ! -w $var ]]"
prop_checkInvertedStringTest5 = verifyNot checkInvertedStringTest "[ -z $var ]"
checkInvertedStringTest _ t =
prop_checkUnnecessarilyInvertedTest1 = verify checkUnnecessarilyInvertedTest "[ ! -z $var ]"
prop_checkUnnecessarilyInvertedTest2 = verify checkUnnecessarilyInvertedTest "! [[ -n $var ]]"
prop_checkUnnecessarilyInvertedTest3 = verifyNot checkUnnecessarilyInvertedTest "! [ -x $var ]"
prop_checkUnnecessarilyInvertedTest4 = verifyNot checkUnnecessarilyInvertedTest "[[ ! -w $var ]]"
prop_checkUnnecessarilyInvertedTest5 = verifyNot checkUnnecessarilyInvertedTest "[ -z $var ]"
prop_checkUnnecessarilyInvertedTest6 = verify checkUnnecessarilyInvertedTest "! [ $var != foo ]"
prop_checkUnnecessarilyInvertedTest7 = verify checkUnnecessarilyInvertedTest "[[ ! $var == foo ]]"
prop_checkUnnecessarilyInvertedTest8 = verifyNot checkUnnecessarilyInvertedTest "! [[ $var =~ .* ]]"
prop_checkUnnecessarilyInvertedTest9 = verify checkUnnecessarilyInvertedTest "[ ! $var -eq 0 ]"
prop_checkUnnecessarilyInvertedTest10 = verify checkUnnecessarilyInvertedTest "! [[ $var -gt 3 ]]"
checkUnnecessarilyInvertedTest _ t =
case t of
TC_Unary _ _ "!" (TC_Unary _ _ op _) ->
case op of
@@ -3908,7 +4020,34 @@ checkInvertedStringTest _ t =
"-n" -> style (getId t) 2237 "Use [ -z .. ] instead of ! [ -n .. ]."
"-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]."
_ -> return ()
TC_Unary _ _ "!" (TC_Binary _ bracketStyle op _ _) ->
maybeSuggestRewrite True bracketStyle (getId t) op
T_Banged _ (T_Pipeline _ _
[T_Redirecting _ _ (T_Condition _ _ (TC_Binary _ bracketStyle op _ _))]) ->
maybeSuggestRewrite False bracketStyle (getId t) op
_ -> return ()
where
inversionMap = Map.fromList [
("=", "!="),
("==", "!="),
("!=", "="),
("-eq", "-ne"),
("-ne", "-eq"),
("-le", "-gt"),
("-gt", "-le"),
("-ge", "-lt"),
("-lt", "-ge")
]
maybeSuggestRewrite bangInside bracketStyle id op = sequence_ $ do
newOp <- Map.lookup op inversionMap
let oldExpr = "a " ++ op ++ " b"
let newExpr = "a " ++ newOp ++ " b"
let bracket s = if bracketStyle == SingleBracket then "[ " ++ s ++ " ]" else "[[ " ++ s ++ " ]]"
return $
if bangInside
then style id 2335 $ "Use " ++ newExpr ++ " instead of ! " ++ oldExpr ++ "."
else style id 2335 $ "Use " ++ (bracket newExpr) ++ " instead of ! " ++ (bracket oldExpr) ++ "."
prop_checkRedirectionToCommand1 = verify checkRedirectionToCommand "ls > rm"
prop_checkRedirectionToCommand2 = verifyNot checkRedirectionToCommand "ls > 'rm'"
@@ -3962,13 +4101,10 @@ prop_checkTranslatedStringVariable4 = verifyNot checkTranslatedStringVariable "v
prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\""
checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s])
| all isVariableChar s
&& Map.member s assignments
&& S.member s assignments
= warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id)
where
assignments = foldl (flip ($)) Map.empty (map insertAssignment $ variableFlow params)
insertAssignment (Assignment (_, token, name, _)) | isVariableName name =
Map.insert name token
insertAssignment _ = Prelude.id
assignments = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params, isVariableName name]
fix id = fixWith [replaceStart id params 2 "\"$"]
checkTranslatedStringVariable _ _ = return ()
@@ -3998,6 +4134,7 @@ prop_checkUselessBang6 = verify checkUselessBang "set -e; { ! true; }"
prop_checkUselessBang7 = verifyNot checkUselessBang "set -e; x() { ! [ x ]; }"
prop_checkUselessBang8 = verifyNot checkUselessBang "set -e; if { ! true; }; then true; fi"
prop_checkUselessBang9 = verifyNot checkUselessBang "set -e; while ! true; do true; done"
prop_checkUselessBang10 = verify checkUselessBang "set -e\nshellcheck disable=SC0000\n! true\nrest"
checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturningCommands t)
where
check t =
@@ -4006,6 +4143,7 @@ checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturning
addComment $ makeCommentWithFix InfoC id 2251
"This ! is not on a condition and skips errexit. Use `&& exit 1` instead, or make sure $? is checked."
(fixWith [replaceStart id params 1 "", replaceEnd (getId cmd) params 0 " && exit 1"])
T_Annotation _ _ t -> check t
_ -> return ()
-- Get all the subcommands that aren't likely to be the return value
@@ -4196,7 +4334,7 @@ checkBadTestAndOr params t =
in
mapM_ checkTest commandWithSeps
checkTest (before, cmd, after) =
when (isTest cmd) $ do
when (isTestCommand cmd) $ do
checkPipe before
checkPipe after
@@ -4212,17 +4350,10 @@ checkBadTestAndOr params t =
T_AndIf _ _ rhs -> checkAnds id rhs
T_OrIf _ _ rhs -> checkAnds id rhs
T_Pipeline _ _ list | not (null list) -> checkAnds id (last list)
cmd -> when (isTest cmd) $
cmd -> when (isTestCommand cmd) $
errWithFix id 2265 "Use && for logical AND. Single & will background and return true." $
(fixWith [replaceEnd id params 0 "&"])
isTest t =
case t of
T_Condition {} -> True
T_SimpleCommand {} -> t `isCommand` "test"
T_Redirecting _ _ t -> isTest t
T_Annotation _ _ t -> isTest t
_ -> False
prop_checkComparisonWithLeadingX1 = verify checkComparisonWithLeadingX "[ x$foo = xlol ]"
prop_checkComparisonWithLeadingX2 = verify checkComparisonWithLeadingX "test x$foo = xlol"
@@ -4230,14 +4361,16 @@ prop_checkComparisonWithLeadingX3 = verifyNot checkComparisonWithLeadingX "[ $fo
prop_checkComparisonWithLeadingX4 = verifyNot checkComparisonWithLeadingX "test $foo = xbar"
prop_checkComparisonWithLeadingX5 = verify checkComparisonWithLeadingX "[ \"x$foo\" = 'xlol' ]"
prop_checkComparisonWithLeadingX6 = verify checkComparisonWithLeadingX "[ x\"$foo\" = x'lol' ]"
prop_checkComparisonWithLeadingX7 = verify checkComparisonWithLeadingX "[ X$foo != Xbar ]"
checkComparisonWithLeadingX params t =
case t of
TC_Binary id typ op lhs rhs | op == "=" || op == "==" ->
check lhs rhs
T_SimpleCommand _ _ [cmd, lhs, op, rhs] |
getLiteralString cmd == Just "test" &&
getLiteralString op `elem` [Just "=", Just "=="] ->
check lhs rhs
TC_Binary id typ op lhs rhs
| op `elem` ["=", "==", "!="] ->
check lhs rhs
T_SimpleCommand _ _ [cmd, lhs, op, rhs]
| getLiteralString cmd == Just "test" &&
getLiteralString op `elem` [Just "=", Just "==", Just "!="] ->
check lhs rhs
_ -> return ()
where
msg = "Avoid x-prefix in comparisons as it no longer serves a purpose."
@@ -4247,19 +4380,20 @@ checkComparisonWithLeadingX params t =
return $ styleWithFix (getId lhs) 2268 msg $ fixWith [l, r]
fixLeadingX token =
case getWordParts token of
T_Literal id ('x':_):_ ->
case getWordParts token of
T_Literal id (c:_):_ | toLower c == 'x' ->
case token of
-- The side is a single, unquoted x, so we have to quote
T_NormalWord _ [T_Literal id "x"] ->
-- The side is a single, unquoted x or X, so we have to quote
T_NormalWord _ [T_Literal id [c]] ->
return $ replaceStart id params 1 "\"\""
-- Otherwise we can just delete it
_ -> return $ replaceStart id params 1 ""
T_SingleQuoted id ('x':_):_ ->
-- Replace the single quote and x
return $ replaceStart id params 2 "'"
T_SingleQuoted id (c:rest):_ | toLower c == 'x' ->
-- Replace the single quote and the character x or X
return $ replaceStart id params 2 "'"
_ -> Nothing
prop_checkAssignToSelf1 = verify checkAssignToSelf "x=$x"
prop_checkAssignToSelf2 = verify checkAssignToSelf "x=${x}"
prop_checkAssignToSelf3 = verify checkAssignToSelf "x=\"$x\""
@@ -4538,13 +4672,13 @@ prop_checkRequireDoubleBracket2 = verifyTree checkRequireDoubleBracket "[ foo -o
prop_checkRequireDoubleBracket3 = verifyNotTree checkRequireDoubleBracket "#!/bin/sh\n[ -x foo ]"
prop_checkRequireDoubleBracket4 = verifyNotTree checkRequireDoubleBracket "[[ -x foo ]]"
checkRequireDoubleBracket params =
if isBashLike params
if (shellType params) `elem` [Bash, Ksh, BusyboxSh]
then nodeChecksToTreeCheck [check] params
else const []
where
check _ t = case t of
T_Condition id SingleBracket _ ->
styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh." (fixFor t)
styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh/Busybox." (fixFor t)
_ -> return ()
fixFor t = fixWith $
@@ -4895,16 +5029,33 @@ checkBatsTestDoesNotUseNegation params t =
prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz"
prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz"
prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz"
prop_checkCommandIsUnreachable4 = verifyNot checkCommandIsUnreachable "f() { foo; }; # Maybe sourced"
prop_checkCommandIsUnreachable5 = verify checkCommandIsUnreachable "f() { foo; }; exit # Not sourced"
checkCommandIsUnreachable params t =
case t of
T_Pipeline {} -> sequence_ $ do
cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga id
state <- CF.getIncomingState cfga (getId t)
guard . not $ CF.stateIsReachable state
guard . not $ isSourced params t
return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."
guard . not $ any (\t -> isUnreachable t || isUnreachableFunction t) $ NE.drop 1 $ getPath (parentMap params) t
return $ info (getId t) 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."
T_Function id _ _ _ _ ->
when (isUnreachableFunction t
&& (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t)
&& (not $ isSourced params t)) $
info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)."
_ -> return ()
where id = getId t
where
isUnreachableFunction :: Token -> Bool
isUnreachableFunction f =
case f of
T_Function id _ _ _ t -> isUnreachable t
_ -> False
isUnreachable t = fromMaybe False $ do
cfga <- cfgAnalysis params
state <- CF.getIncomingState cfga (getId t)
return . not $ CF.stateIsReachable state
prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]"
@@ -4984,14 +5135,14 @@ checkUnnecessaryParens params t =
T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))") [x,y,z]
T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t
T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t
TA_Parentesis _ (TA_Sequence _ [ TA_Parentesis id _ ]) ->
TA_Parenthesis _ (TA_Sequence _ [ TA_Parenthesis id _ ]) ->
styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id
_ -> return ()
where
checkLeading str t =
case t of
TA_Sequence _ [TA_Parentesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id
TA_Sequence _ [TA_Parenthesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id
_ -> return ()
fix id =
@@ -5017,7 +5168,8 @@ checkPlusEqualsNumber params t =
state <- CF.getIncomingState cfga id
guard $ isNumber state word
guard . not $ fromMaybe False $ CF.variableMayBeDeclaredInteger state var
return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), declare -i var, or quote number to silence."
-- Recommend "typeset" because ksh does not have "declare".
return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), typeset -i var, or quote number to silence."
_ -> return ()
where
@@ -5039,5 +5191,52 @@ checkPlusEqualsNumber params t =
isUnquotedNumber || isNumericalVariableName || isNumericalVariableExpansion
prop_checkExpansionWithRedirection1 = verify checkExpansionWithRedirection "var=$(foo > bar)"
prop_checkExpansionWithRedirection2 = verify checkExpansionWithRedirection "var=`foo 1> bar`"
prop_checkExpansionWithRedirection3 = verify checkExpansionWithRedirection "var=${ foo >> bar; }"
prop_checkExpansionWithRedirection4 = verify checkExpansionWithRedirection "var=$(foo | bar > baz)"
prop_checkExpansionWithRedirection5 = verifyNot checkExpansionWithRedirection "stderr=$(foo 2>&1 > /dev/null)"
prop_checkExpansionWithRedirection6 = verifyNot checkExpansionWithRedirection "var=$(foo; bar > baz)"
prop_checkExpansionWithRedirection7 = verifyNot checkExpansionWithRedirection "var=$(foo > bar; baz)"
prop_checkExpansionWithRedirection8 = verifyNot checkExpansionWithRedirection "var=$(cat <&3)"
checkExpansionWithRedirection params t =
case t of
T_DollarExpansion id [cmd] -> check id cmd
T_Backticked id [cmd] -> check id cmd
T_DollarBraceCommandExpansion id _ [cmd] -> check id cmd
_ -> return ()
where
check id pipe =
case pipe of
(T_Pipeline _ _ t@(_:_)) -> checkCmd id (last t)
_ -> return ()
checkCmd captureId (T_Redirecting _ redirs _) = foldr (walk captureId) (return ()) redirs
walk captureId t acc =
case t of
T_FdRedirect _ _ (T_IoDuplicate _ _ "1") -> return ()
T_FdRedirect id "1" (T_IoDuplicate _ _ _) -> return ()
T_FdRedirect id "" (T_IoDuplicate _ op _) | op `elem` [T_GREATAND (Id 0), T_Greater (Id 0)] -> emit id captureId True
T_FdRedirect id str (T_IoFile _ op file) | str `elem` ["", "1"] && op `elem` [ T_DGREAT (Id 0), T_Greater (Id 0) ] ->
emit id captureId $ getLiteralString file /= Just "/dev/null"
_ -> acc
emit redirectId captureId suggestTee = do
warn captureId 2327 "This command substitution will be empty because the command's output gets redirected away."
err redirectId 2328 $ "This redirection takes output away from the command substitution" ++ if suggestTee then " (use tee to duplicate)." else "."
prop_checkUnaryTestA1 = verify checkUnaryTestA "[ -a foo ]"
prop_checkUnaryTestA2 = verify checkUnaryTestA "[ ! -a foo ]"
prop_checkUnaryTestA3 = verifyNot checkUnaryTestA "[ foo -a bar ]"
checkUnaryTestA params t =
case t of
TC_Unary id _ "-a" _ ->
styleWithFix id 2331 "For file existence, prefer standard -e over legacy -a." $
fixWith [replaceStart id params 2 "-e"]
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -89,6 +89,8 @@ data Parameters = Parameters {
hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- Whether this script has 'shopt -s execfail' anywhere.
hasExecfail :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- A map from Id to Token
@@ -226,6 +228,10 @@ makeParameters spec = params
BusyboxSh -> isOptionSet "pipefail" root
Sh -> True
Ksh -> isOptionSet "pipefail" root,
hasExecfail =
case shellType params of
Bash -> isOptionSet "execfail" root
_ -> False,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
idMap = getTokenMap root,
parentMap = getParentTree root,
@@ -535,7 +541,9 @@ getModifiedVariables t =
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal)
(t, t, "output", DataString SourceExternal),
(t, t, "stderr", DataString SourceExternal),
(t, t, "stderr_lines", DataArray SourceExternal)
]
-- Count [[ -v foo ]] as an "assignment".
@@ -557,8 +565,12 @@ getModifiedVariables t =
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
T_CoProc _ name _ ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
T_CoProc _ Nothing _ ->
[(t, t, "COPROC", DataArray SourceInteger)]
T_CoProc _ (Just token) _ -> do
name <- maybeToList $ getLiteralString token
[(t, t, name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
@@ -902,16 +914,6 @@ supportsArrays Bash = True
supportsArrays Ksh = True
supportsArrays _ = False
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
BusyboxSh -> False
Sh -> False
isTrueAssignmentSource c =
case c of
DataString SourceChecked -> False
@@ -929,6 +931,14 @@ modifiesVariable params token name =
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
_ -> False
isTestCommand t =
case t of
T_Condition {} -> True
T_SimpleCommand {} -> t `isCommand` "test"
T_Redirecting _ _ t -> isTestCommand t
T_Annotation _ _ t -> isTestCommand t
T_Pipeline _ _ [t] -> isTestCommand t
_ -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -295,19 +295,19 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
regularEdges = filter isRegularEdge edges
inDegree = counter $ map (\(from,to,_) -> from) regularEdges
outDegree = counter $ map (\(from,to,_) -> to) regularEdges
structuralNodes = S.fromList $ map fst $ filter isStructural nodes
structuralNodes = S.fromList [node | (node, CFStructuralNode) <- nodes]
candidateNodes = S.filter isLinear structuralNodes
edgesToCollapse = S.fromList $ filter filterEdges regularEdges
remapping :: M.Map Node Node
remapping = foldl' (\m (new, old) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse
recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping
remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse
recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping
filterEdges (a,b,_) =
a `S.member` candidateNodes && b `S.member` candidateNodes
orderEdge (a,b,_) = if a < b then (a,b) else (b,a)
counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty
orderEdge (a,b,_) = if a < b then (b,a) else (a,b)
counter = M.fromListWith (+) . map (\key -> (key, 1))
isRegularEdge (_, _, CFEFlow) = True
isRegularEdge _ = False
@@ -317,11 +317,6 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
Nothing -> node
Just x -> recursiveLookup map x
isStructural (node, label) =
case label of
CFStructuralNode -> True
_ -> False
isLinear node =
M.findWithDefault 0 node inDegree == 1
&& M.findWithDefault 0 node outDegree == 1
@@ -495,7 +490,7 @@ build t = do
TA_Binary _ _ a b -> sequentially [a,b]
TA_Expansion _ list -> sequentially list
TA_Sequence _ list -> sequentially list
TA_Parentesis _ t -> build t
TA_Parenthesis _ t -> build t
TA_Trinary _ cond a b -> do
condition <- build cond
@@ -673,10 +668,18 @@ build t = do
status <- newNodeRange $ CFSetExitCode id
linkRange cond status
T_CoProc id maybeName t -> do
let name = fromMaybe "COPROC" maybeName
T_CoProc id maybeNameToken t -> do
-- If unspecified, "COPROC". If not a constant string, Nothing.
let maybeName = case maybeNameToken of
Just x -> getLiteralString x
Nothing -> Just "COPROC"
let parentNode = case maybeName of
Just str -> applySingle $ IdTagged id $ CFWriteVariable str CFValueArray
Nothing -> CFStructuralNode
start <- newStructuralNode
parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray
parent <- newNodeRange parentNode
child <- subshell id "coproc" $ build t
end <- newNodeRange $ CFSetExitCode id
@@ -713,6 +716,9 @@ build t = do
linkRange totalRead result
else return totalRead
T_DollarBraceCommandExpansion id _ body ->
sequentially body
T_DoubleQuoted _ list -> sequentially list
T_DollarExpansion id body ->

View File

@@ -133,7 +133,7 @@ internalToExternal s =
literalValue = Nothing
}
}
flatVars = M.unionsWith (\_ last -> last) $ map mapStorage [sGlobalValues s, sLocalValues s, sPrefixValues s]
flatVars = M.unions $ map mapStorage [sPrefixValues s, sLocalValues s, sGlobalValues s]
-- Conveniently get the state before a token id
getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState
@@ -672,7 +672,7 @@ vmPatch base diff =
_ | vmIsQuickEqual base diff -> diff
_ -> VersionedMap {
mapVersion = -1,
mapStorage = M.unionWith (flip const) (mapStorage base) (mapStorage diff)
mapStorage = M.union (mapStorage diff) (mapStorage base)
}
-- Set a variable. This includes properties. Applies it to the appropriate scope.
@@ -1286,7 +1286,7 @@ dataflow ctx entry = do
else do
let (next, rest) = S.deleteFindMin ps
nexts <- process states next
writeSTRef pending $ foldl (flip S.insert) rest nexts
writeSTRef pending $ S.union (S.fromList nexts) rest
f (n-1) pending states
process states node = do
@@ -1350,7 +1350,7 @@ analyzeControlFlow params t =
-- All nodes we've touched
invocations <- readSTRef $ cInvocations ctx
let invokedNodes = M.fromDistinctAscList $ map (\c -> (c, ())) $ S.toList $ M.keysSet $ groupByNode $ M.map snd invocations
let invokedNodes = M.fromSet (const ()) $ S.unions $ map (M.keysSet . snd) $ M.elems invocations
-- Invoke all functions that were declared but not invoked
-- This is so that we still get warnings for dead code
@@ -1373,7 +1373,7 @@ analyzeControlFlow params t =
-- Fill in the map with unreachable states for anything we didn't get to
let baseStates = M.fromDistinctAscList $ map (\c -> (c, (unreachableState, unreachableState))) $ uncurry enumFromTo $ nodeRange $ cfGraph cfg
let allStates = M.unionWith (flip const) baseStates invokedStates
let allStates = M.union invokedStates baseStates
-- Convert to external states
let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates

View File

@@ -221,6 +221,9 @@ prop_worksWhenSourcing =
prop_worksWhenSourcingWithDashDash =
null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\""
prop_worksWhenSourcingWithDashP =
null $ checkWithIncludes [("lib", "bar=1")] "source -p \"$MYPATH\" lib; echo \"$bar\""
prop_worksWhenDotting =
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""

View File

@@ -725,6 +725,9 @@ prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
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"
prop_checkGetPrintfFormats8 = getPrintfFormats "%ld" == "d"
prop_checkGetPrintfFormats9 = getPrintfFormats "%lld" == "d"
prop_checkGetPrintfFormats10 = getPrintfFormats "%Q" == "Q"
getPrintfFormats = getFormats
where
-- Get the arguments in the string as a string of type characters,
@@ -743,17 +746,17 @@ getPrintfFormats = getFormats
regexBasedGetFormats rest =
case matchRegex re rest of
Just [width, precision, typ, rest, _] ->
Just [width, precision, len, typ, rest, _] ->
(if width == "*" then "*" else "") ++
(if precision == "*" then "*" else "") ++
typ ++ getFormats rest
Nothing -> take 1 rest ++ getFormats rest
where
-- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])((\n|.)*)"
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
-- V V V V V
-- flags field width precision format character rest
re = mkRegex "^#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)(hh|h|l|ll|q|L|j|z|Z|t)?([diouxXfFeEgGaAcsbqQSC])((\n|.)*)"
-- \____ _____/\___ ____/ \____ ____/\__________ ___________/\___________ ___________/\___ ___/
-- V V V V V V
-- flags field width precision length modifier format character rest
-- field width and precision can be specified with an '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
@@ -1241,7 +1244,7 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
command <- getLiteralString commandArg
guard $ command `elem` builtins
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
builtins = [ "cd", "eval", "export", "history", "read", "source", "wait" ]
builtins = [ "cd", "command", "declare", "eval", "exec", "exit", "export", "hash", "history", "local", "popd", "pushd", "read", "readonly", "return", "set", "source", "trap", "type", "typeset", "ulimit", "umask", "unset", "wait" ]
-- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
@@ -1431,9 +1434,8 @@ prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where
check t = do
cfga <- asks cfgAnalysis
when (isJust cfga) $
foldM_ (perArg $ fromJust cfga) M.empty $ arguments t
maybeCfga <- asks cfgAnalysis
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
perArg cfga leftArgs t =
case t of

View File

@@ -78,7 +78,7 @@ controlFlowEffectChecks = [
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
runNodeChecks perNode = do
cfg <- asks cfgAnalysis
sequence_ $ runOnAll <$> cfg
mapM_ runOnAll cfg
where
getData datas n@(node, label) = do
(pre, post) <- M.lookup node datas

View File

@@ -1,7 +1,7 @@
{-
This empty file is provided for ease of patching in site specific checks.
However, there are no guarantees regarding compatibility between versions.
-}
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where

View File

@@ -63,6 +63,7 @@ checks = [
,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
,checkNegatedUnaryOps
]
testChecker (ForShell _ t) =
@@ -86,7 +87,7 @@ checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file"
@@ -212,6 +213,16 @@ prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}"
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'"
prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'"
prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test"
prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar"
prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo"
prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test"
prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]"
prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp"
prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]"
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
params <- ask
kludge params t
@@ -229,7 +240,8 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
bashism (T_DollarSingleQuoted id _) =
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
@@ -239,34 +251,16 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism (T_Condition id DoubleBracket _) =
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs])
| op `elem` [ "-ot", "-nt", "-ef" ] =
unless isDash $ warnMsg id 3013 $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
unless isBusyboxSh $ warnMsg id 3014 "== in place of = is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "==", rhs]) =
unless isBusyboxSh $ warnMsg id 3014 "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id 3015 "=~ regex matching is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "=~", rhs]) =
warnMsg id 3015 "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-v", _]) =
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) =
warnMsg id 3017 "unary -a in place of -e is"
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-a", _]) =
warnMsg id 3017 "unary -a in place of -e is"
bashism (TC_Binary id _ op _ _) =
checkTestOp bashismBinaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) =
checkTestOp bashismBinaryTestFlags op id
bashism (TC_Unary id _ op _) =
checkTestOp bashismUnaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) =
checkTestOp bashismUnaryTestFlags op id
bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id 3018 $ filter (/= '|') op ++ " is"
@@ -321,7 +315,11 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex =
if isDash
if isBusyboxSh
then
unless (argString `matches` busyboxFlagRegex) $
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
else if isDash
then
when (argString /= "-n") $
warnMsg (getId arg) 3036 "echo flags besides -n"
@@ -330,6 +328,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
where
argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$"
busyboxFlagRegex = mkRegex "^-[en]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
@@ -443,10 +442,10 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]),
("printf", Just []),
("read", Just $ if isDash then ["r", "p"] else ["r"]),
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
("readonly", Just ["p"]),
("trap", Just []),
("type", Just []),
("type", Just $ if isBusyboxSh then ["p"] else []),
("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]),
("unset", Just ["f", "v"]),
@@ -498,6 +497,50 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
Assignment (_, _, name, _) -> name == var
_ -> False
checkTestOp table op id = sequence_ $ do
(code, shells, msg) <- Map.lookup op table
guard . not $ shellType params `elem` shells
return $ warnMsg id code (msg op)
buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list
bashismBinaryTestFlags = buildTestFlagMap [
-- ([list of applicable flags],
-- (error code, exempt shells, message builder :: String -> String)),
--
-- Distinct error codes allow the wiki to give more helpful, targeted
-- information.
(["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="],
(3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")),
(["=="],
(3014, [BusyboxSh], \op -> op ++ " in place of = is")),
(["=~"],
(3015, [], \op -> op ++ " regex matching is")),
([], (0,[],const ""))
]
bashismUnaryTestFlags = buildTestFlagMap [
(["-v"],
(3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")),
(["-a"],
(3017, [], \op -> "unary " ++ op ++ " in place of -e is")),
(["-o"],
(3062, [], \op -> "test " ++ op ++ " to check options is")),
(["-R"],
(3063, [], \op -> "test " ++ op ++ " and namerefs in general are")),
(["-N"],
(3064, [], \op -> "test " ++ op ++ " is")),
(["-k"],
(3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-G"],
(3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-O"],
(3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
([], (0,[],const ""))
]
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
@@ -637,5 +680,22 @@ checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
_ -> return ()
prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]"
prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]"
prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]"
prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]"
prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]"
checkNegatedUnaryOps = ForShell [Bash] f
where
f token = case token of
TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] ->
err id 2332 $ msg op
_ -> return ()
msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]."
msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead."
msg _ = pleaseReport "unhandled negated unary message"
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -49,6 +49,7 @@ internalVariables = [
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
"auto_resume", "histchars",
-- Other
@@ -62,6 +63,9 @@ internalVariables = [
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return"
-- Bats
,"stderr", "stderr_lines"
]
specialIntegerVariables = [
@@ -75,7 +79,7 @@ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES"
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@@ -164,6 +168,7 @@ shellForExecutable name =
"ksh" -> return Ksh
"ksh88" -> return Ksh
"ksh93" -> return Ksh
"oksh" -> return Ksh
_ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"

View File

@@ -191,11 +191,17 @@ splitLast x =
let (last, rest) = splitAt 1 $ reverse x
in (reverse rest, last)
-- git patch does not like `\` on Windows
normalizePath path =
case path of
c:rest -> (if c == pathSeparator then '/' else c) : normalizePath rest
[] -> []
formatDoc color (DiffDoc name lf regions) =
let (most, last) = splitLast regions
in
(color bold $ "--- " ++ ("a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ ("b" </> name)) ++ "\n" ++
(color bold $ "--- " ++ (normalizePath $ "a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ (normalizePath $ "b" </> name)) ++ "\n" ++
concatMap (formatRegion color LinefeedOk) most ++
concatMap (formatRegion color lf) last

View File

@@ -169,7 +169,7 @@ showFixedString color comments lineNum fileLines =
-- and/or other unrelated lines.
let (excerptFix, excerpt) = sliceFile mergedFix fileLines
-- in the spirit of error prone
putStrLn $ color "message" "Did you mean: "
putStrLn $ color "message" "Did you mean:"
putStrLn $ unlines $ applyFix excerptFix excerpt
cuteIndent :: PositionedComment -> String

View File

@@ -48,6 +48,7 @@ import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import Debug.Trace
import Test.QuickCheck.All (quickCheckAll)
@@ -66,10 +67,14 @@ singleQuote = char '\''
doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_"
-- Chars to allow in function names
functionChars = variableChars <|> oneOf ":+?-./^@,"
-- Chars to allow function names to start with
functionStartChars = variableChars <|> oneOf ":+?-./^@,"
-- Chars to allow inside function names
functionChars = variableChars <|> oneOf "#:+?-./^@,"
-- Chars to allow function names to start with, using the 'function' keyword
extendedFunctionStartChars = functionStartChars <|> oneOf "[]*=!"
-- Chars to allow in functions using the 'function' keyword
extendedFunctionChars = functionChars <|> oneOf "[]*=!"
extendedFunctionChars = extendedFunctionStartChars <|> oneOf "[]*=!"
specialVariable = oneOf (concat specialVariables)
paramSubSpecialChars = oneOf "/:+-=%"
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
@@ -141,15 +146,9 @@ carriageReturn = do
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
return '\r'
almostSpace =
choice [
check '\xA0' "unicode non-breaking space",
check '\x200B' "unicode zerowidth space"
]
where
check c name = do
parseNote ErrorC 1018 $ "This is a " ++ name ++ ". Delete and retype it."
char c
almostSpace = do
parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it."
oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F"
return ' '
--------- Message/position annotation on top of user state
@@ -827,7 +826,7 @@ readArithmeticContents =
char ')'
id <- endSpan start
spacing
return $ TA_Parentesis id s
return $ TA_Parenthesis id s
readArithTerm = readGroup <|> readVariable <|> readExpansion
@@ -1701,16 +1700,17 @@ readAmbiguous prefix expected alternative warner = do
prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }"
prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}"
readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
prop_readDollarBraceCommandExpansion3 = isOk readDollarBraceCommandExpansion "${| REPLY=42; }"
readDollarBraceCommandExpansion = called "ksh-style ${ ..; } command expansion" $ do
start <- startSpan
try $ do
string "${"
whitespace
c <- try $ do
string "${"
char '|' <|> whitespace
allspacing
term <- readTerm
char '}' <|> fail "Expected } to end the ksh ${ ..; } command expansion"
char '}' <|> fail "Expected } to end the ksh-style ${ ..; } command expansion"
id <- endSpan start
return $ T_DollarBraceCommandExpansion id term
return $ T_DollarBraceCommandExpansion id (if c == '|' then Piped else Unpiped) term
prop_readDollarBraced1 = isOk readDollarBraced "${foo//bar/baz}"
prop_readDollarBraced2 = isOk readDollarBraced "${foo/'{cow}'}"
@@ -2211,17 +2211,18 @@ readSimpleCommand = called "simple command" $ do
readSource :: Monad m => Token -> SCParser m Token
readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = do
let file = getFile file' rest'
readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:args'))) = do
let file = getFile args'
override <- getSourceOverride
let literalFile = do
name <- override `mplus` getLiteralString file `mplus` stripDynamicPrefix file
name <- override `mplus` (getLiteralString =<< file) `mplus` (stripDynamicPrefix =<< file)
-- Hack to avoid 'source ~/foo' trying to read from literal tilde
guard . not $ "~/" `isPrefixOf` name
return name
let fileId = fromMaybe (getId cmd) (getId <$> file)
case literalFile of
Nothing -> do
parseNoteAtId (getId file) WarningC 1090
parseNoteAtId fileId WarningC 1090
"ShellCheck can't follow non-constant source. Use a directive to specify location."
return t
Just filename -> do
@@ -2229,7 +2230,7 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
if not proceed
then do
-- FIXME: This actually gets squashed without -a
parseNoteAtId (getId file) InfoC 1093
parseNoteAtId fileId InfoC 1093
"This file appears to be recursively sourced. Ignoring."
return t
else do
@@ -2247,7 +2248,7 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
return (contents, resolved)
case input of
Left err -> do
parseNoteAtId (getId file) InfoC 1091 $
parseNoteAtId fileId InfoC 1091 $
"Not following: " ++ err
return t
Right script -> do
@@ -2259,18 +2260,19 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
return $ T_SourceCommand id1 t (T_Include id2 src)
let failed = do
parseNoteAtId (getId file) WarningC 1094
parseNoteAtId fileId WarningC 1094
"Parsing of sourced file failed. Ignoring it."
return t
included <|> failed
where
getFile :: Token -> [Token] -> Token
getFile file (next:rest) =
case getLiteralString file of
Just "--" -> next
x -> file
getFile file _ = file
getFile :: [Token] -> Maybe Token
getFile (first:rest) =
case getLiteralString first of
Just "--" -> rest !!! 0
Just "-p" -> rest !!! 1
_ -> return first
getFile _ = Nothing
getSourcePath t =
case t of
@@ -2757,6 +2759,8 @@ prop_readFunctionDefinition10 = isOk readFunctionDefinition "function foo () { t
prop_readFunctionDefinition11 = isWarning readFunctionDefinition "function foo{\ntrue\n}"
prop_readFunctionDefinition12 = isOk readFunctionDefinition "function []!() { true; }"
prop_readFunctionDefinition13 = isOk readFunctionDefinition "@require(){ true; }"
prop_readFunctionDefinition14 = isOk readFunctionDefinition "foo#bar(){ :; }"
prop_readFunctionDefinition15 = isNotOk readFunctionDefinition "#bar(){ :; }"
readFunctionDefinition = called "function" $ do
start <- startSpan
functionSignature <- try readFunctionSignature
@@ -2774,7 +2778,7 @@ readFunctionDefinition = called "function" $ do
string "function"
whitespace
spacing
name <- many1 extendedFunctionChars
name <- (:) <$> extendedFunctionStartChars <*> many extendedFunctionChars
spaces <- spacing
hasParens <- wasIncluded readParens
when (not hasParens && null spaces) $
@@ -2783,7 +2787,7 @@ readFunctionDefinition = called "function" $ do
return $ \id -> T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name
readWithoutFunction = try $ do
name <- many1 functionChars
name <- (:) <$> functionStartChars <*> many functionChars
guard $ name /= "time" -- Interferes with time ( foo )
spacing
readParens
@@ -2801,17 +2805,29 @@ readFunctionDefinition = called "function" $ do
prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }"
prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }"
prop_readCoProc3 = isOk readCoProc "coproc echo bar"
prop_readCoProc4 = isOk readCoProc "coproc a=b echo bar"
prop_readCoProc5 = isOk readCoProc "coproc 'foo' { echo bar; }"
prop_readCoProc6 = isOk readCoProc "coproc \"foo$$\" { echo bar; }"
prop_readCoProc7 = isOk readCoProc "coproc 'foo' ( echo bar )"
prop_readCoProc8 = isOk readCoProc "coproc \"foo$$\" while true; do true; done"
readCoProc = called "coproc" $ do
start <- startSpan
try $ do
string "coproc"
whitespace
spacing1
choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]
where
readCompoundCoProc start = do
var <- optionMaybe $
readVariableName `thenSkip` whitespace
body <- readBody readCompoundCommand
notFollowedBy2 readAssignmentWord
(var, body) <- choice [
try $ do
body <- readBody readCompoundCommand
return (Nothing, body),
try $ do
var <- readNormalWord `thenSkip` spacing
body <- readBody readCompoundCommand
return (Just var, body)
]
id <- endSpan start
return $ T_CoProc id var body
readSimpleCoProc start = do
@@ -3381,7 +3397,8 @@ readScriptFile sourced = do
"busybox sh",
"bash",
"bats",
"ksh"
"ksh",
"oksh"
]
badShells = [
"awk",
@@ -3390,6 +3407,7 @@ readScriptFile sourced = do
"fish",
"perl",
"python",
"python3",
"ruby",
"tcsh",
"zsh"
@@ -3442,13 +3460,22 @@ isOk p s = parsesCleanly p s == Just True -- The string parses with no wa
isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings
isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse
parsesCleanly parser string = runIdentity $ do
(res, sys) <- runParser testEnvironment
(parser >> eof >> getState) "-" string
case (res, sys) of
(Right userState, systemState) ->
return $ Just . null $ parseNotes userState ++ parseProblems systemState
(Left _, _) -> return Nothing
-- If the parser matches the string, return Right [ParseNotes+ParseProblems]
-- If it does not match the string, return Left [ParseProblems]
getParseOutput parser string = runIdentity $ do
(res, systemState) <- runParser testEnvironment
(parser >> eof >> getState) "-" string
return $ case res of
Right userState ->
Right $ parseNotes userState ++ parseProblems systemState
Left _ -> Left $ parseProblems systemState
-- If the parser matches the string, return Just whether it was clean (without emitting suggestions)
-- Otherwise, Nothing
parsesCleanly parser string =
case getParseOutput parser string of
Right list -> Just $ null list
Left _ -> Nothing
parseWithNotes parser = do
item <- parser

View File

@@ -12,6 +12,12 @@ command -v cabal ||
cabal update ||
die "can't update"
if [ -e "cabal.project.freeze" ]
then
echo "Renaming cabal.project.freeze to .bak to avoid it interferring" >&2
mv "cabal.project.freeze" "cabal.project.freeze.bak" || die "Couldn't rename"
fi
if [ -e /etc/arch-release ]
then
# Arch has an unconventional packaging setup

View File

@@ -12,6 +12,22 @@ then
fail "There are uncommitted changes"
fi
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
then
fail "Expected git log message to contain CHANGELOG"
fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
current=$(git tag --points-at)
if [[ -z "$current" ]]
then
@@ -34,17 +50,6 @@ then
fail "You are not on master"
fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then
fail "Expected git log message to be 'Stable version ...'"
@@ -56,11 +61,14 @@ cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run \`builders/build_builder build/*/\` to update all builder images.
$((j++)). \`builders/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz builders/*/\` to verify that they work.
$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them.
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
$((i++)). Make sure SnapCraft build currently works: https://snapcraft.io/shellcheck/builds
$((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds.
$((i++)). Make sure the Hackage package builds locally.
Release Steps
@@ -72,5 +80,6 @@ $((j++)). Verify release:
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md
$((j++)). Run 'autoupdate' from https://github.com/koalaman/shellcheck-precommit
EOF
exit "$failed"

View File

@@ -8,6 +8,15 @@ die() { echo "$*" >&4; exit 1; }
[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir"
if ( snap list | grep -q docker ) > /dev/null 2>&1
then
# Snap docker can't mount /tmp in containers
echo "You appear to be using Docker from snap. Creating ~/tmp for temp files." >&2
echo >&2
export TMPDIR="$HOME/tmp"
mkdir -p "$TMPDIR"
fi
[ "$1" = "--run" ] || {
cat << EOF
This script pulls multiple distros via Docker and compiles
@@ -17,13 +26,13 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue.
Also note that dist* will be deleted.
Also note that dist*/ and .stack-work/ will be deleted.
EOF
exit 0
}
echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
rm -rf dist dist-newstyle .stack-work
execs=$(find . -name shellcheck)
@@ -74,11 +83,12 @@ fedora:latest dnf install -y cabal-install ghc-template-haskell-devel fi
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Ubuntu LTS
ubuntu:24.04 apt-get update && apt-get install -y cabal-install
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS
ubuntu:22.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:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF
exit "$final"

View File

@@ -15,7 +15,7 @@ die() { echo "$*" >&2; exit 1; }
command -v stack ||
die "stack is missing"
stack setup || die "Failed to setup with default resolver"
stack setup --allow-different-user || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
# Nice to haves, but not necessary