Compare commits

...

204 commits

Author SHA1 Message Date
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
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
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
Vidar Holen
37dfb67768 Stable version v0.10.0
This release is dedicated to LLMs, for finally fulfilling the promise of
1960s scifi: systems you can hack using logic games and creative lies.
2024-03-07 17:54:39 -08:00
Vidar Holen
a7e65dca8d Update some copyright years 2024-03-04 09:19:51 -08:00
Vidar Holen
8bc7345aa7 Remove outdated distros from testing 2024-03-03 16:11:44 -08:00
Vidar Holen
ad3c3146f0 Fix snap build 2024-03-03 12:34:29 -08:00
Vidar Holen
55be4543f2 Avoid stripping darwin.aarch64 binaries to keep code signature 2024-02-19 11:40:30 -08:00
Vidar Holen
8c4c112c25 Initial version of an ARM64 macOS build 2024-02-19 09:29:27 -08:00
Vidar Holen
d80fdfa9e8 Add extended-analysis directive to toggle DFA 2024-02-03 16:11:39 -08:00
Vidar Holen
1565091b1d
Merge pull request #2892 from ottok/doc/pulsar-not-atom
Replace Atom reference with Pulsar Edit equivalent
2024-02-03 13:46:23 -08:00
Vidar Holen
d056549406
Merge pull request #2885 from juhp/patch-1
.cabal: allow Diff-0.5
2024-02-03 13:43:52 -08:00
Vidar Holen
f5758e1789 Merge branch 'tacerus-config' 2024-02-03 13:38:56 -08:00
Vidar Holen
6a44a19f17 Only read --rcfile once, and skip search if unavailable 2024-02-03 13:34:49 -08:00
Vidar Holen
b1b95c2c17
Merge pull request #2917 from grische/fix/tests-readme
Remove deprecated "install --enable-tests" command
2024-02-03 13:04:06 -08:00
Grische
de95624d31 Remove deprecated "install --enable-tests" command 2024-02-02 12:35:52 +01:00
Vidar Holen
b5ab220652
Merge pull request #2879 from slycordinator/winget
Add installation directions for winget
2024-01-21 11:44:58 -08:00
Georg Pfuetzenreuter
1bce426fcf
Implement rcfile option
This introduces the "--rcfile" argument which allows a specific
shellcheckrc file to be passed.
If specified and the given file exists, the default locations
will not be searched and the specified file will be used.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-01-21 02:59:47 +01:00
Joseph C. Sible
ba86c6363c Use maybe instead of fromMaybe and fmap 2024-01-02 14:46:07 -05:00
Joseph C. Sible
67abfe159e Remove most of the partial head and tail functions from src/ShellCheck/CFG.hs 2024-01-01 19:04:26 -05:00
Joseph C. Sible
025cc5266e Simplify isUnquotedFlag 2024-01-01 16:00:19 -05:00
Joseph C. Sible
5a6f4840ad Replace a few more occurrences of !!! with pattern matching 2024-01-01 14:18:52 -05:00
Joseph C. Sible
9e0fdbe431 Simplify isTransparentCommand 2023-12-31 18:13:32 -05:00
Joseph C. Sible
b7f88ec4b7 Stop building tuples that we never look at both sides of 2023-12-31 18:09:02 -05:00
Joseph C. Sible
7b0589988f Implement isCondition in terms of foldr 2023-12-31 17:21:50 -05:00
Joseph C. Sible
71889c139a Use a case expression instead of any and take 1 2023-12-31 16:44:21 -05:00
Joseph C. Sible
a6984cddb0 Switch then and else to remove a not 2023-12-31 16:40:18 -05:00
Joseph C. Sible
3f40b688ee Simplify getStringFromParsec 2023-12-31 16:33:34 -05:00
Joseph C. Sible
6c81505870 Use a pattern guard instead of fromJust in checkLoopKeywordScope 2023-12-31 16:26:03 -05:00
Joseph C. Sible
10afe83ce3 Use getLiteralStringDef instead of rebuilding it with fromJust 2023-12-31 16:23:45 -05:00
Joseph C. Sible
a786f996a1 Replace !!! with pattern-matching where it's easy 2023-12-31 15:55:06 -05:00
Joseph C. Sible
6e5b5401c6 Manually fuse elem and map in checkArrayValueUsedAsIndex 2023-12-31 02:31:07 -05:00
Joseph C. Sible
71c0fcb737 Manually fuse elem and map in isParentOf 2023-12-31 02:27:52 -05:00
Joseph C. Sible
add49cda17 Make getPath return a NonEmpty 2023-12-31 02:12:58 -05:00
Joseph C. Sible
e1ad063834 Implement getPath in terms of unfoldr 2023-12-31 01:59:53 -05:00
Otto Kekäläinen
ee41c780f4 Replace Atom reference with Pulsar Edit equivalent
Since Microsoft acquired GitHub and discontinued Atom in 2022,
the community started a fork at https://pulsar-edit.dev/. Linking
to an archived repository under the Atom organization does not make
sense anymore, so link to active Pulsar fork instead.
2023-12-31 10:47:40 +08:00
Joseph C. Sible
980e7d3ca8 Use <$> instead of >>= and return 2023-12-30 14:49:26 -05:00
Joseph C. Sible
dedf932fe8 Use traverse instead of sequence and map 2023-12-30 13:59:15 -05:00
Joseph C. Sible
3bd7df955b Use a pattern match instead of null and head in checkCommand 2023-12-29 14:18:42 -05:00
Joseph C. Sible
dab77b2c8d Implement parseEnum in terms of lookup 2023-12-21 13:48:47 -05:00
Joseph C. Sible
f983d9ae93 Simplify functionMap and remove unnecessary partiality 2023-12-21 13:35:22 -05:00
Joseph C. Sible
bfe4342697 Remove unnecessary partiality from check 2023-12-19 02:30:48 -05:00
Joseph C. Sible
a47a42cb45 Remove unnecessary partiality from isAssignmentParamToCommand 2023-12-19 02:17:59 -05:00
Joseph C. Sible
eed0174e90 Make "Unresolved scope in dependency" impossible 2023-12-19 02:06:45 -05:00
Joseph C. Sible
0c46b8b2d5 Use NonEmpty to remove partiality from handleCommand 2023-12-19 01:49:04 -05:00
Joseph C. Sible
208e38358e Use a list comprehension to remove partiality from notesForContext 2023-12-19 01:00:20 -05:00
Joseph C. Sible
c1452e0d17 Remove unnecessary partiality from kludgeAwayQuotes 2023-12-19 00:53:08 -05:00
Joseph C. Sible
c97abdb939 Make HereDocPending only hold the relevant pieces of a T_HereDoc instead of an arbitrary Token 2023-12-19 00:41:12 -05:00
Joseph C. Sible
f242922a2e Use onlyLiteralString in more places 2023-12-19 00:00:32 -05:00
Joseph C. Sible
a37803d2b8 Remove partial head function from src/ShellCheck/Formatter/CheckStyle.hs 2023-12-18 23:57:47 -05:00
Jens Petersen
09d04c4c9b
.cabal: allow Diff-0.5 2023-12-15 22:40:48 +08:00
slycordinator
e5028481e2 Add installation directions for winge
ShellCheck is now available on winget, so we can add it to the installation methods.
2023-12-14 15:24:49 +09:00
Joseph C. Sible
5a961371a7 Remove partial head function from src/ShellCheck/Formatter/GCC.hs 2023-12-11 15:55:29 -05:00
Joseph C. Sible
e5208ccb50 Remove partial head function from src/ShellCheck/Formatter/JSON1.hs 2023-12-11 15:43:35 -05:00
Joseph C. Sible
4c1d9171b2 Remove partial head function from src/ShellCheck/Formatter/TTY.hs 2023-12-11 15:08:39 -05:00
Vidar Holen
a9e7bf1950 Reparse indices after attaching here docs (fixes #2846) 2023-12-10 19:13:34 -08:00
Vidar Holen
f2729f73cb Abuse STRIP to avoid crashes on unsupported AST nodes 2023-12-10 17:58:47 -08:00
Vidar Holen
175d3cc9b7
Merge pull request #2876 from andreasabel/master
Testsuite: report which module failed the tests
2023-12-10 17:34:51 -08:00
Vidar Holen
5c50b0b189 Merge branch 'grische-feature/busyboxsh-support' 2023-12-10 17:15:57 -08:00
Vidar Holen
74282b0a93 Recognize 'busybox' in --shell and directives. Add to doc texts. 2023-12-10 17:05:29 -08:00
Andreas Abel
b6d4952e2e Testsuite: report which module failed the tests
This also fixes the problem that the testsuite threw `exitFailure`
even when it succeeded (which I found inexplicable).

Once this is published, the testsuite could be enabled in Stackage again.
2023-12-06 18:41:53 +01:00
Grische
fdcce458c1 silence some shell expansions for busybox sh 2023-11-27 13:03:29 +01:00
Grische
ca255fe326 silence SC3046 and SC3051 for busybox sh 2023-11-27 13:03:17 +01:00
Grische
a3b8be82fe silence SC3048 for busybox sh 2023-11-27 13:03:07 +01:00
Grische
ac63dc33c9 silence SC3020 for busybox sh 2023-11-27 13:02:56 +01:00
Grische
903421fb5d silence SC3014 for busybox sh 2023-11-27 13:02:45 +01:00
Grische
00ffd2db33 silence SC3010 for busybox sh 2023-11-27 13:02:28 +01:00
Grische
1e1045e73e make busybox sh Dash-like 2023-11-27 13:01:22 +01:00
Grische
be8e4b2b8a add basic busybox sh support 2023-11-27 13:00:10 +01:00
Vidar Holen
a71a13c2fc
Merge pull request #2837 from ulidtko/fix/missed-test(1)-bashisms
Fix: extend []-related bashism checks on `test` calls too
2023-11-08 13:06:26 -08:00
Joseph C. Sible
1aeab287e6 Add nil case that went missing in 4fd0615 2023-11-03 01:33:49 -04:00
Joseph C. Sible
2a95bc6be3 Switch to getLiteralStringDef to avoid an unnecessary fromJust 2023-10-16 20:00:31 -04:00
Joseph C. Sible
4fd0615501 Stop using head in isLeadingNumberVar 2023-10-16 00:55:04 -04:00
Joseph C. Sible
8b3c37aa36 Use find instead of listToMaybe and filter 2023-10-16 00:06:53 -04:00
Joseph C. Sible
dc2f388310 Adjust bounds to compile on 9.8
You'll need --allow-newer=fgl:deepseq for it to work too,
until haskell/fgl#111 gets merged.
2023-10-14 18:12:51 -04:00
Vidar Holen
99a94421ab Manually install 'hub' dependency 2023-10-08 19:42:31 -07:00
Vidar Holen
6a6d8e9fc4 Revert "Bump actions/checkout from 3 to 4"
This reverts commit 410ec54617.
2023-10-08 18:52:05 -07:00
Vidar Holen
592c17e4f2
Merge pull request #2824 from koalaman/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-10-08 14:14:25 -07:00
Max Ulidtko
9605396bef
Docs: describe fixes of PR #2837 in changelog 2023-10-01 21:23:25 +02:00
Max Ulidtko
c89ec2fd49
Fix: do []-related bashism checks on test(1) calls too 2023-10-01 19:57:19 +02:00
dependabot[bot]
410ec54617
Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-05 08:21:55 +00:00
Vidar Holen
90d3172dfe Add a newSystemInterface to go with the rest of the new* constructors 2023-08-13 16:35:28 -07:00
Vidar Holen
d18b2553cf
Merge pull request #2808 from bruce-ricard/pr/dfbr
improve short description for SC2038
2023-08-13 14:53:15 -07:00
Vidar Holen
dd747b2a98 SC2325/SC2326: Warn about ! ! foo and foo | ! bar (fixes #2810) 2023-07-30 19:18:27 -07:00
Vidar Holen
9490b94886 Save and restore pending here docs when sourcing files (fixes #2803) 2023-07-30 16:52:40 -07:00
Vidar Holen
372c0b667e SC2324: Warn when x+=1 appends. 2023-07-30 15:00:43 -07:00
Danny Faught
01aee1a859 improve short description
* The short description used to say that until commit
  aac7d76047 from 2014. It appears that
  it was changed by mistake in that commit to something less readable.

* With the message "use -print0/-0" we were confused and introduced a
  bug in our code because we didn't understand what to do with the
  "-0".

* SC2011 (source
  c9e27c2470/src/ShellCheck/Analytics.hs (L591))
  uses that exact warning message, we copied it from there.

Signed-off-by: Bruce Ricard <bricard@vmware.com>
2023-07-28 14:19:54 -04:00
Vidar Holen
c9e27c2470
Merge pull request #2768 from nicolas-ot/add-dependabot
Add dependabot
2023-06-04 15:40:27 -07:00
Vidar Holen
4ffa9cc397
Merge pull request #2765 from josephcsible/bracedstring
Get rid of a dangerous partial function from checkSpacefulnessCfg'
2023-06-04 15:22:23 -07:00
Nicolas Theodarus
b625cc1acc add dependabot.yml 2023-05-28 12:33:16 +02:00
Joseph C. Sible
f03c437e2f Get rid of a dangerous partial function from checkSpacefulnessCfg' 2023-05-24 16:38:53 -04:00
Vidar Holen
824c802b63
Merge pull request #2749 from josephcsible/2734
Fix #2734: adjust bounds to compile on 9.6
2023-05-22 17:52:34 -07:00
Joseph C. Sible
b3932dfa10 Fix #2734: adjust bounds to compile on 9.6
The whole test suite passes for me, including prop_checkOverwrittenExitCode8,
and I get the same set of findings with this build and shellcheck.net on
tools/testing/selftests/net/icmp_redirect.sh.
2023-05-01 00:02:53 -04:00
Vidar Holen
a54965dd2c Merge branch 'ArenM-posix-read' 2023-04-30 14:49:36 -07:00
Vidar Holen
46b678fca8 Minor fixes to POSIX read without variable check 2023-04-30 14:49:10 -07:00
Vidar Holen
be0d5d4163
Merge pull request #2746 from J-M0/fish-bad-shell
Add fish to the badShells list
2023-04-30 13:31:34 -07:00
James Morris
5fec3f9b34 Add fish to the badShells list 2023-04-24 22:08:22 -04:00
Vidar Holen
1164aa4efc Installing custom docker should no longer be necessary for buildx 2023-04-23 19:35:54 -07:00
Vidar Holen
ff85a5a2a2 Merge branch 'felipecrs-vscode-binaries' 2023-04-23 16:48:28 -07:00
Vidar Holen
08b437974e Rewrite vscode-shellcheck blurb 2023-04-23 16:47:49 -07:00
Vidar Holen
15fd2c314c
Merge pull request #2682 from sxlijin/patch-1
Document Trunk Check integration
2023-04-23 10:23:03 -07:00
Felipe Santos
e6e8ab0415
Mention VS Code ShellCheck binaries distribution 2023-02-05 11:13:07 -03:00
Vidar Holen
b1ca3929e3 Upgrade cross-compilers to 9.2.5 to handle hashable-1.4.2.0 2023-02-04 19:55:25 -08:00
Vidar Holen
c05380d518 Count CFEExit as control flow for the purposes of finding dominators 2023-02-04 14:47:40 -08:00
Vidar Holen
2842ce97b8 Remove fgl-5.8.1.0 as a dependency
ShellCheck is temporarily broken by
c8f56c1824
2023-02-04 11:38:20 -08:00
Vidar Holen
78dea1d4f9 Update changelog from release 2023-02-04 10:27:59 -08:00
Samuel Lijin
5a3eb89e38
Document Trunk Check integration
Trunk Check is a universal linter which integrates with a wide variety of linters and formatters, `shellcheck` included.

We're big fans of `shellcheck` and figured that you might find our tool to be interesting enough to include it in the integrations list.
2023-02-03 09:17:47 -08:00
ArenM
3342902d9a Warn about 'read' without a variable in POSIX sh
Dash throws an error if the read command isn't supplied a variable name.
2022-11-17 18:46:15 -05:00
53 changed files with 1618 additions and 624 deletions

7
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View file

@ -15,7 +15,7 @@ jobs:
sudo apt-get install cabal-install sudo apt-get install cabal-install
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -37,24 +37,47 @@ jobs:
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: source name: source
path: 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: build_source:
name: Build Source Code name: Build
needs: package_source needs: package_source
strategy: strategy:
matrix: matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
- name: Build source - name: Build source
run: | run: |
@ -63,9 +86,9 @@ jobs:
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} ) ( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: bin name: ${{matrix.build}}.bin
path: bin/ path: bin/
package_binary: package_binary:
@ -74,25 +97,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
- name: Work around GitHub permissions bug - name: Work around GitHub permissions bug
run: chmod +x bin/*/shellcheck* run: chmod +x *.bin/*/shellcheck*
- name: Package binaries - name: Package binaries
run: | run: |
export TAGS="$(cat source/tags)" export TAGS="$(cat source/tags)"
mkdir -p deploy mkdir -p deploy
cp -r bin/* deploy cp -r *.bin/* deploy
cd deploy cd deploy
../.prepare_deploy ../.prepare_deploy
rm -rf */ README* LICENSE* rm -rf */ README* LICENSE*
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: deploy name: deploy
path: deploy/ path: deploy/
@ -103,11 +126,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Deploy environment: Deploy
steps: steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install hub
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
- name: Upload to GitHub - name: Upload to GitHub
env: env:

1
.gitignore vendored
View file

@ -20,3 +20,4 @@ cabal.config
/parts/ /parts/
/prime/ /prime/
*.snap *.snap
/dist-newstyle/

View file

@ -3,28 +3,10 @@
# binaries previously built and deployed to GitHub. # binaries previously built and deployed to GitHub.
function multi_arch_docker::install_docker_buildx() { function multi_arch_docker::install_docker_buildx() {
# Install up-to-date version of docker, with buildx support.
local -r docker_apt_repo='https://download.docker.com/linux/ubuntu'
curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add -
local -r os="$(lsb_release -cs)"
sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable"
sudo apt-get update
sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
# Enable docker daemon experimental support (for 'pull --platform').
local -r config='/etc/docker/daemon.json'
if [[ -e "$config" ]]; then
sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
else
echo '{ "experimental": true }' | sudo tee "$config"
fi
sudo systemctl restart docker
# Install QEMU multi-architecture support for docker buildx. # Install QEMU multi-architecture support for docker buildx.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# Instantiate docker buildx builder with multi-architecture support. # Instantiate docker buildx builder with multi-architecture support.
export DOCKER_CLI_EXPERIMENTAL=enabled
docker buildx create --name mybuilder docker buildx create --name mybuilder
docker buildx use mybuilder docker buildx use mybuilder
# Start up buildx and verify that all is OK. # Start up buildx and verify that all is OK.
@ -98,6 +80,7 @@ function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64' export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64' DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6' DOCKER_PLATFORMS+=' linux/arm/v6'
DOCKER_PLATFORMS+=' linux/riscv64'
multi_arch_docker::install_docker_buildx multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub multi_arch_docker::login_to_docker_hub

View file

@ -1,14 +0,0 @@
# In 2015, cabal-install had a http bug triggered when proxies didn't keep
# the connection open. This version made it into Ubuntu Xenial as used by
# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
#
# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
#
# Workaround: add more proxy
visible_hostname localhost
http_port 8888
cache_peer 10.10.10.1 parent 8222 0 no-query default
cache_peer_domain localhost !.internal
http_access allow all

View file

@ -1,3 +1,47 @@
## Git
### 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 ]`.
- 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.
- 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
### 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.
### 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)
- Added support for busybox sh
- Added flag --rcfile to specify an rc file by name.
- Added `extended-analysis=true` directive to enable/disable dataflow analysis
(with a corresponding --extended-analysis flag).
- SC2324: Warn when x+=1 appends instead of increments
- SC2325: Warn about multiple `!`s in dash/sh.
- SC2326: Warn about `foo | ! bar` in bash/dash/sh.
- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
- SC3014: Warn bashism `test _ == _` like in [ ]
- SC3015: Warn bashism `test _ =~ _` like in [ ]
- SC3016: Warn bashism `test -v _` like in [ ]
- SC3017: Warn bashism `test -a _` like in [ ]
### Fixed
- source statements with here docs now work correctly
- "(Array.!): undefined array element" error should no longer occur
## v0.9.0 - 2022-12-12 ## v0.9.0 - 2022-12-12
### Added ### Added
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!) - SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)

View file

@ -77,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck). * Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck). * Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar).
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck). * VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
@ -110,8 +110,11 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/) * [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/) * [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/) * [Code Factor](https://www.codefactor.io/)
* [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) * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux) * [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)
* [CodeRabbit](https://coderabbit.ai/)
Most other services, including [GitLab](https://about.gitlab.com/), let you install Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@ -193,6 +196,12 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
C:\> choco install shellcheck C:\> choco install shellcheck
``` ```
Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)):
```cmd
C:\> winget install --id koalaman.shellcheck
```
Or Windows (via [scoop](http://scoop.sh)): Or Windows (via [scoop](http://scoop.sh)):
```cmd ```cmd
@ -221,17 +230,26 @@ Using the [nix package manager](https://nixos.org/nix):
nix-env -iA nixpkgs.shellcheck 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: 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, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [macOS, 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) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds). (including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
There are currently no official binaries for Apple Silicon, but third party builds are available via
[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
Distro packages already come with a `man` page. If you are building from source, it can be installed with: Distro packages already come with a `man` page. If you are building from source, it can be installed with:
```console ```console
@ -299,10 +317,6 @@ Verify that `cabal` is installed and update its dependency list with
$ cabal install $ cabal install
Or if you intend to run the tests:
$ cabal install --enable-tests
This will compile ShellCheck and install it to your `~/.cabal/bin` directory. This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
@ -548,4 +562,3 @@ Happy ShellChecking!
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221). * The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)! * ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!

View file

@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.9.0 Version: 0.10.0
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@ -46,21 +46,21 @@ library
semigroups semigroups
build-depends: build-depends:
-- The lower bounds are based on GHC 7.10.3 -- The lower bounds are based on GHC 7.10.3
-- The upper bounds are based on GHC 9.4.3 -- The upper bounds are based on GHC 9.8.1
aeson >= 1.4.0 && < 2.2, aeson >= 1.4.0 && < 2.3,
array >= 0.5.1 && < 0.6, array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5, base >= 4.8.0.0 && < 5,
bytestring >= 0.10.6 && < 0.12, bytestring >= 0.10.6 && < 0.13,
containers >= 0.5.6 && < 0.7, containers >= 0.5.6 && < 0.8,
deepseq >= 1.4.1 && < 1.5, deepseq >= 1.4.1 && < 1.6,
Diff >= 0.4.0 && < 0.5, Diff >= 0.4.0 && < 1.1,
fgl >= 5.7.0 && < 5.9, 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.3, mtl >= 2.2.2 && < 2.4,
parsec >= 3.1.14 && < 3.2, parsec >= 3.1.14 && < 3.2,
QuickCheck >= 2.14.2 && < 2.15, QuickCheck >= 2.14.2 && < 2.16,
regex-tdfa >= 1.2.0 && < 1.4, regex-tdfa >= 1.2.0 && < 1.4,
transformers >= 0.4.2 && < 0.6, transformers >= 0.4.2 && < 0.7,
-- getXdgDirectory from 1.2.3.0 -- getXdgDirectory from 1.2.3.0
directory >= 1.2.3 && < 1.4, directory >= 1.2.3 && < 1.4,

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`, An image can be built and tagged using `build_builder`,
and run on a source tarball using `run_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

@ -0,0 +1,40 @@
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest
ENV TARGET aarch64-apple-darwin22
ENV TARGETNAME darwin.aarch64
# Build dependencies
USER root
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
# Install a more suitable host compiler
WORKDIR /host-ghc
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
RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1
RUN ./configure && make install
# Build GHC. We have to use an old version because cross-compilation across OS has since broken.
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1
RUN apt-get install -y llvm-12
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
# 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"
# Prebuild the dependencies
RUN cabal update
RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

16
build/darwin.aarch64/build Executable file
View file

@ -0,0 +1,16 @@
#!/bin/sh
set -xe
{
tar xzv --strip-components=1
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
# Stripping invalidates the code signature and the build image does
# not appear to have anything similar to the 'codesign' tool.
# "$TARGET-strip" "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
file "$TARGETNAME/shellcheck" | grep "Mach-O 64-bit arm64 executable"
} >&2
tar czv "$TARGETNAME"

1
build/darwin.aarch64/tag Normal file
View file

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

View file

@ -6,15 +6,18 @@ ENV TARGETNAME darwin.x86_64
# Build dependencies # Build dependencies
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl RUN sed -e 's/focal/kinetic/g' -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 # Build GHC
WORKDIR /ghc WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1 RUN 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 ./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 cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin 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 # 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 # It won't reuse caches if ghc-options are specified in ~/.cabal/config

View file

@ -4,7 +4,6 @@ set -xe
tar xzv --strip-components=1 tar xzv --strip-components=1
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS ) ( IFS=';'; cabal build $CABALOPTS )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME" ls -l "$TARGETNAME"

View file

@ -6,19 +6,29 @@ ENV TARGETNAME linux.aarch64
# Build dependencies # Build dependencies
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
# 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
# Kinetic does not receive updates anymore, switch to last available
RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/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 # Build GHC
WORKDIR /ghc WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1 RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" 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 cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin 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 # 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 # It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg" ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
# Prebuild the dependencies # Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck

View file

@ -4,7 +4,6 @@ set -xe
tar xzv --strip-components=1 tar xzv --strip-components=1
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) ( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME" ls -l "$TARGETNAME"

View file

@ -1,25 +1,7 @@
# I've again spent days trying to get a working armv6hf compiler going. # This Docker file uses a custom QEmu fork with patches to follow execve
# God only knows how many recompilations of GCC, GHC, libraries, and # to build all of ShellCheck emulated.
# 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 FROM ubuntu:24.04
ENV TARGETNAME linux.armv6hf ENV TARGETNAME linux.armv6hf
@ -27,34 +9,34 @@ ENV TARGETNAME linux.armv6hf
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update RUN apt-get update
RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev 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 /build WORKDIR /qemu
RUN git clone --depth 1 https://github.com/koalaman/qemu RUN git clone --depth 1 https://github.com/koalaman/qemu .
RUN cd qemu && ./configure --static && cd build && ninja qemu-arm RUN ./configure --static --disable-werror && cd build && ninja qemu-arm
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
ENV QEMU_EXECVE 1 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 # Set up an armv6 userspace
WORKDIR / WORKDIR /
RUN apt-get install -y debootstrap qemu-user-static RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian
# We expect this to fail if the host doesn't have binfmt qemu support RUN cp /qemu/build/qemu-arm /chroot/bin/qemu
RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ] RUN scutil emu /debootstrap/debootstrap --second-stage
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 # Install deps in the chroot
RUN pirun apt-get update RUN scutil emu apt-get update
RUN pirun apt-get install -y ghc cabal-install 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. # 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" 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 # Generated with `cabal freeze --constraint 'hashable -arch-native'`
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck COPY cabal.project.freeze /chroot/etc
RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS
# Copy the build script # Copy the build script
WORKDIR /pi/scratch COPY build /chroot/bin
COPY build /pi/usr/bin ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]

View file

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

View file

@ -0,0 +1,93 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Diff ==0.5,
any.OneTuple ==0.4.2,
any.QuickCheck ==2.14.3,
QuickCheck -old-random +templatehaskell,
any.StateVar ==1.2.2,
any.aeson ==2.2.3.0,
aeson +ordered-keymap,
any.array ==0.5.4.0,
any.assoc ==1.1.1,
assoc -tagged,
any.base ==4.15.1.0,
any.base-orphans ==0.9.2,
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.comonad ==5.0.8,
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.3,
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.2.0,
fgl +containers042,
any.filepath ==1.4.2.1,
any.foldable1-classes-compat ==0.1,
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.6.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.3.1,
integer-logarithms -check-bounds +integer-gmp,
any.mtl ==2.2.2,
any.network-uri ==2.6.4.2,
any.parsec ==3.1.14.0,
any.pretty ==1.1.3.6,
any.primitive ==0.9.0.0,
any.process ==1.6.13.2,
any.random ==1.2.1.2,
any.regex-base ==0.94.0.2,
any.regex-tdfa ==1.3.2.2,
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.0.5,
splitmix -optimised-mixer,
any.stm ==2.5.0.0,
any.strict ==0.5,
any.tagged ==0.8.8,
tagged +deepseq +transformers,
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.0.0,
any.th-compat ==0.1.5,
any.these ==1.2.1,
any.time ==1.9.3,
any.time-compat ==1.9.7,
any.transformers ==0.5.6.2,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
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.1.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-stream ==0.1.0.1,
any.witherable ==0.5
index-state: hackage.haskell.org 2024-06-18T02:21:19Z

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 |
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,46 @@
FROM ubuntu:24.04
ENV TARGETNAME linux.riscv64
ENV TARGET riscv64-linux-gnu
USER root
ENV DEBIAN_FRONTEND noninteractive
# Init base
RUN apt-get update -y
# Install qemu
RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools debootstrap
WORKDIR /qemu
RUN git clone --depth 1 https://github.com/koalaman/qemu .
RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror
RUN cd build && ninja qemu-riscv64
ENV QEMU_EXECVE 1
# Convenience utility
COPY scutil /bin/scutil
# We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists
COPY scutil /chroot/usr/bin/scutil
RUN chmod +x /bin/scutil /chroot/usr/bin/scutil
# Set up a riscv64 userspace
WORKDIR /
RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports
RUN cp /qemu/build/qemu-riscv64 /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
# Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it.
COPY cabal.project.freeze /chroot/etc
# Build all dependencies from the freeze file. The emulator segfaults at random,
# so retry a few times.
RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going
# Copy the build script
COPY build /chroot/bin/build
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]

21
build/linux.riscv64/build Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
set -xe
IFS=';'
{
mkdir -p /tmp/scratch
cd /tmp/scratch
tar xzv --strip-components=1
chmod +x striptests && ./striptests
# Use a freeze file to ensure we use the same dependencies we cached during
# the docker image build. We don't want to spend time compiling anything new.
cp /etc/cabal.project.freeze .
mkdir "$TARGETNAME"
# Retry in case of random segfault
scutil retry 3 cabal build --enable-executable-static
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

View file

@ -0,0 +1,93 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Diff ==0.5,
any.OneTuple ==0.4.2,
any.QuickCheck ==2.14.3,
QuickCheck -old-random +templatehaskell,
any.StateVar ==1.2.2,
any.aeson ==2.2.3.0,
aeson +ordered-keymap,
any.array ==0.5.4.0,
any.assoc ==1.1.1,
assoc -tagged,
any.base ==4.17.2.0,
any.base-orphans ==0.9.2,
any.bifunctors ==5.6.2,
bifunctors +tagged,
any.binary ==0.8.9.1,
any.bytestring ==0.11.5.2,
any.character-ps ==0.1,
any.comonad ==5.0.8,
comonad +containers +distributive +indexed-traversable,
any.containers ==0.6.7,
any.contravariant ==1.5.5,
contravariant +semigroups +statevar +tagged,
any.data-fix ==0.3.3,
any.deepseq ==1.4.8.0,
any.directory ==1.3.7.1,
any.distributive ==0.6.2.1,
distributive +semigroups +tagged,
any.dlist ==1.0,
dlist -werror,
any.exceptions ==0.10.5,
any.fgl ==5.8.2.0,
fgl +containers042,
any.filepath ==1.4.2.2,
any.foldable1-classes-compat ==0.1,
foldable1-classes-compat +tagged,
any.generically ==0.1.1,
any.ghc-bignum ==1.3,
any.ghc-boot-th ==9.4.7,
any.ghc-prim ==0.9.1,
any.hashable ==1.4.6.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.3.1,
integer-logarithms -check-bounds +integer-gmp,
any.mtl ==2.2.2,
any.network-uri ==2.6.4.2,
any.os-string ==2.0.3,
any.parsec ==3.1.16.1,
any.pretty ==1.1.3.6,
any.primitive ==0.9.0.0,
any.process ==1.6.17.0,
any.random ==1.2.1.2,
any.regex-base ==0.94.0.2,
any.regex-tdfa ==1.3.2.2,
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.0.5,
splitmix -optimised-mixer,
any.stm ==2.5.1.0,
any.strict ==0.5,
any.tagged ==0.8.8,
tagged +deepseq +transformers,
any.template-haskell ==2.19.0.0,
any.text ==2.0.2,
any.text-iso8601 ==0.1.1,
any.text-short ==0.1.6,
text-short -asserts,
any.th-abstraction ==0.7.0.0,
any.th-compat ==0.1.5,
any.these ==1.2.1,
any.time ==1.12.2,
any.time-compat ==1.9.7,
any.transformers ==0.5.6.2,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
any.unix ==2.7.3,
any.unordered-containers ==0.2.20,
unordered-containers -debug,
any.uuid-types ==1.0.6,
any.vector ==0.13.1.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-stream ==0.1.0.1,
any.witherable ==0.5
index-state: hackage.haskell.org 2024-06-17T00:48:51Z

1
build/linux.riscv64/tag Normal file
View file

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

View file

@ -1,4 +1,8 @@
FROM alpine:latest FROM alpine:3.16
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes
ENV TARGETNAME linux.x86_64 ENV TARGETNAME linux.x86_64

View file

@ -12,7 +12,7 @@ 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 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 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://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/* . RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
ENV WINEPATH /haskell/bin ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue # It's unknown whether Cabal on Windows suffers from the same issue

View file

@ -8,7 +8,6 @@ set -xe
tar xzv --strip-components=1 tar xzv --strip-components=1
chmod +x striptests && ./striptests chmod +x striptests && ./striptests
mkdir "$TARGETNAME" mkdir "$TARGETNAME"
cabal update
( IFS=';'; cabal build $CABALOPTS ) ( IFS=';'; cabal build $CABALOPTS )
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \; find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME" ls -l "$TARGETNAME"

View file

@ -56,6 +56,13 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
options are cumulative, but all the codes can be specified at once, options are cumulative, but all the codes can be specified at once,
comma-separated as a single argument. comma-separated as a single argument.
**--extended-analysis=true/false**
: Enable/disable Dataflow Analysis to identify more issues (default true). If
ShellCheck uses too much CPU/RAM when checking scripts with several
thousand lines of code, extended analysis can be disabled with this flag
or a directive. This flag overrides directives and rc files.
**-f** *FORMAT*, **--format=***FORMAT* **-f** *FORMAT*, **--format=***FORMAT*
: Specify the output format of shellcheck, which prints its results in the : Specify the output format of shellcheck, which prints its results in the
@ -71,6 +78,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Don't try to look for .shellcheckrc configuration files. : Don't try to look for .shellcheckrc configuration files.
**--rcfile** *RCFILE*
: Prefer the specified configuration file over searching for one
in the default locations.
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...] **-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: Enable optional checks. The special name *all* enables all of them. : Enable optional checks. The special name *all* enables all of them.
@ -85,7 +97,8 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-s**\ *shell*,\ **--shell=***shell* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*. : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
and *busybox*.
The default is to deduce the shell from the file's `shell` directive, The default is to deduce the shell from the file's `shell` directive,
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
POSIX `sh` (not the system's), and will warn of portability issues. POSIX `sh` (not the system's), and will warn of portability issues.
@ -243,6 +256,12 @@ Valid keys are:
: Enable an optional check by name, as listed with **--list-optional**. : Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered. Only file-wide `enable` directives are considered.
**extended-analysis**
: Set to true/false to enable/disable dataflow analysis. Specifying
`# shellcheck extended-analysis=false` in particularly large (2000+ line)
auto-generated scripts will reduce ShellCheck's resource usage at the
expense of certain checks. Extended analysis is enabled by default.
**external-sources** **external-sources**
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open : Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
arbitrary files from 'source' statements (the way most tools do). arbitrary files from 'source' statements (the way most tools do).
@ -298,7 +317,7 @@ Here is an example `.shellcheckrc`:
disable=SC2236 disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck 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 (usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used. Windows. Only the first file found will be used.
@ -378,10 +397,10 @@ long list of wonderful contributors.
# COPYRIGHT # COPYRIGHT
Copyright 2012-2022, Vidar Holen and contributors. Copyright 2012-2024, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later, Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html see https://gnu.org/licenses/gpl.html
# SEE ALSO # SEE ALSO
sh(1) bash(1) sh(1) bash(1) dash(1) ksh(1)

View file

@ -76,7 +76,8 @@ data Options = Options {
externalSources :: Bool, externalSources :: Bool,
sourcePaths :: [FilePath], sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions, formatterOptions :: FormatterOptions,
minSeverity :: Severity minSeverity :: Severity,
rcfile :: Maybe FilePath
} }
defaultOptions = Options { defaultOptions = Options {
@ -86,7 +87,8 @@ defaultOptions = Options {
formatterOptions = newFormatterOptions { formatterOptions = newFormatterOptions {
foColorOption = ColorAuto foColorOption = ColorAuto
}, },
minSeverity = StyleC minSeverity = StyleC,
rcfile = Nothing
} }
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@ -100,6 +102,8 @@ options = [
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings", (ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"] Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings", (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "" ["extended-analysis"]
(ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)",
Option "f" ["format"] Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") $ (ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")", "Output format (" ++ formatList ++ ")",
@ -107,6 +111,9 @@ options = [
(NoArg $ Flag "list-optional" "true") "List checks disabled by default", (NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"] Option "" ["norc"]
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files", (NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
Option "" ["rcfile"]
(ReqArg (Flag "rcfile") "RCFILE")
"Prefer the specified configuration file over searching for one",
Option "o" ["enable"] Option "o" ["enable"]
(ReqArg (Flag "enable") "check1,check2..") (ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')", "List of optional checks to enable (or 'all')",
@ -115,7 +122,7 @@ options = [
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)", "Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") (ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)", "Specify dialect (sh, bash, dash, ksh, busybox)",
Option "S" ["severity"] Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY") (ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)", "Minimum severity of errors to consider (error, warning, info, style)",
@ -252,9 +259,9 @@ runFormatter sys format options files = do
else SomeProblems else SomeProblems
parseEnum name value list = parseEnum name value list =
case filter ((== value) . fst) list of case lookup value list of
[(name, value)] -> return value Just value -> return value
[] -> do Nothing -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++ printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list) "Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure throwError SupportFailure
@ -367,6 +374,11 @@ parseOption flag options =
} }
} }
Flag "rcfile" str -> do
return options {
rcfile = Just str
}
Flag "enable" value -> Flag "enable" value ->
let cs = checkSpec options in return options { let cs = checkSpec options in return options {
checkSpec = cs { checkSpec = cs {
@ -374,6 +386,14 @@ parseOption flag options =
} }
} }
Flag "extended-analysis" str -> do
value <- parseBool str
return options {
checkSpec = (checkSpec options) {
csExtendedAnalysis = Just value
}
}
-- This flag is handled specially in 'process' -- This flag is handled specially in 'process'
Flag "format" _ -> return options Flag "format" _ -> return options
@ -391,12 +411,20 @@ parseOption flag options =
throwError SyntaxFailure throwError SyntaxFailure
return (Prelude.read num :: Integer) return (Prelude.read num :: Integer)
parseBool str = do
case str of
"true" -> return True
"false" -> return False
_ -> do
printErr $ "Invalid boolean, expected true/false: " ++ str
throwError SyntaxFailure
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO) ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
ioInterface options files = do ioInterface options files = do
inputs <- mapM normalize files inputs <- mapM normalize files
cache <- newIORef emptyCache cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing) configCache <- newIORef ("", Nothing)
return SystemInterface { return (newSystemInterface :: SystemInterface IO) {
siReadFile = get cache inputs, siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options), siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache siGetConfig = getConfig configCache
@ -441,8 +469,23 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path fallback path _ = return path
-- Returns the name and contents of .shellcheckrc for the given file -- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename = do getConfig cache filename =
case rcfile options of
Just file -> do
-- We have a specified rcfile. Ignore normal rcfile resolution.
(path, result) <- readIORef cache
if path == "/"
then return result
else do
result <- readConfig file
when (isNothing result) $
hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file
writeIORef cache ("/", result)
return result
Nothing -> do
path <- normalize filename path <- normalize filename
let dir = takeDirectory path let dir = takeDirectory path
(previousPath, result) <- readIORef cache (previousPath, result) <- readIORef cache
@ -490,7 +533,7 @@ ioInterface options files = do
where where
handler :: FilePath -> IOException -> IO (String, Bool) handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do handler file err = do
putStrLn $ file ++ ": " ++ show err hPutStrLn stderr $ file ++ ": " ++ show err
return ("", True) return ("", True)
andM a b arg = do andM a b arg = do

View file

@ -23,7 +23,7 @@ description: |
# snap connect shellcheck:removable-media # snap connect shellcheck:removable-media
version: git version: git
base: core18 base: core20
grade: stable grade: stable
confinement: strict confinement: strict
@ -40,16 +40,16 @@ parts:
source: . source: .
build-packages: build-packages:
- cabal-install - cabal-install
- squid stage-packages:
- libatomic1
override-build: | override-build: |
# See comments in .snapsquid.conf # Give ourselves enough memory to build
[ "$http_proxy" ] && { dd if=/dev/zero of=/tmp/swap bs=1M count=2000
squid3 -f .snapsquid.conf mkswap /tmp/swap
export http_proxy="http://localhost:8888" swapon /tmp/swap
sleep 3
}
cabal sandbox init cabal sandbox init
cabal update || cat /var/log/squid/* cabal update
cabal install -j cabal install -j
install -d $SNAPCRAFT_PART_INSTALL/usr/bin install -d $SNAPCRAFT_PART_INSTALL/usr/bin

View file

@ -138,7 +138,7 @@ data InnerToken t =
| Inner_T_WhileExpression [t] [t] | Inner_T_WhileExpression [t] [t]
| Inner_T_Annotation [Annotation] t | Inner_T_Annotation [Annotation] t
| Inner_T_Pipe String | Inner_T_Pipe String
| Inner_T_CoProc (Maybe String) t | Inner_T_CoProc (Maybe Token) t
| Inner_T_CoProcBody t | Inner_T_CoProcBody t
| Inner_T_Include t | Inner_T_Include t
| Inner_T_SourceCommand t t | Inner_T_SourceCommand t t
@ -152,6 +152,7 @@ data Annotation =
| ShellOverride String | ShellOverride String
| SourcePath String | SourcePath String
| ExternalSources Bool | ExternalSources Bool
| ExtendedAnalysis Bool
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
@ -205,7 +206,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_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t) pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) 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 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_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) pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
@ -258,7 +259,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_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression 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 instance Eq Token where
OuterToken _ a == OuterToken _ b = a == b OuterToken _ a == OuterToken _ b = a == b

View file

@ -31,6 +31,7 @@ import Data.Functor
import Data.Functor.Identity import Data.Functor.Identity
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map import qualified Data.Map as Map
import Numeric (showHex) import Numeric (showHex)
@ -157,9 +158,10 @@ isFlag token =
_ -> False _ -> False
-- Is this token a flag where the - is unquoted? -- Is this token a flag where the - is unquoted?
isUnquotedFlag token = fromMaybe False $ do isUnquotedFlag token =
str <- getLeadingUnquotedString token case getLeadingUnquotedString token of
return $ "-" `isPrefixOf` str Just ('-':_) -> True
_ -> False
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read` -- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
-- -re -d : -u 3 bar -- -re -d : -u 3 bar
@ -444,6 +446,12 @@ getLiteralStringExt more = g
-- Is this token a string literal? -- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t isLiteral t = isJust $ getLiteralString t
-- 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. -- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful. -- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage e4m = escapeForMessage
@ -758,8 +766,8 @@ prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash" prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash" prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash" prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash" prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash" prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
-- Get the shell executable from a string like '/usr/bin/env bash' -- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String executableFromShebang :: String -> String
@ -776,7 +784,8 @@ executableFromShebang = shellFor
[x] -> basename x [x] -> basename x
(first:second:args) | basename first == "busybox" -> (first:second:args) | basename first == "busybox" ->
case basename second of case basename second of
"sh" -> "ash" -- busybox sh is ash "sh" -> "busybox sh"
"ash" -> "busybox ash"
x -> x x -> x
(first:args) | basename first == "env" -> (first:args) | basename first == "env" ->
fromEnvArgs args fromEnvArgs args
@ -856,8 +865,7 @@ getBracedModifier s = headOrDefault "" $ do
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]} -- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"] prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
getIndexReferences s = fromMaybe [] $ do getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s index:_ <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index return $ matchAllStrings variableNameRegex index
where where
re = mkRegex "(\\[.*\\])" re = mkRegex "(\\[.*\\])"
@ -868,8 +876,7 @@ prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"] prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ] -- if mods start with [, then drop until ]
match <- matchRegex re mods _:offsets:_ <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets return $ matchAllStrings variableNameRegex offsets
where where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)" re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
@ -886,11 +893,17 @@ isUnmodifiedParameterExpansion t =
in getBracedReference str == str in getBracedReference str == str
_ -> False _ -> False
-- Return the referenced variable if (and only if) it's an unmodified parameter expansion.
getUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ _ list -> do
let str = concat $ oversimplify list
guard $ getBracedReference str == str
return str
_ -> Nothing
--- A list of the element and all its parents up to the root node. --- A list of the element and all its parents up to the root node.
getPath tree t = t : getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
isClosingFileOp op = isClosingFileOp op =
case op of case op of
@ -903,5 +916,11 @@ getEnableDirectives root =
T_Annotation _ list _ -> [s | EnableComment s <- list] T_Annotation _ list _ -> [s | EnableComment s <- list]
_ -> [] _ -> []
getExtendedAnalysisDirective :: Token -> Maybe Bool
getExtendedAnalysisDirective root =
case root of
T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list]
_ -> Nothing
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

File diff suppressed because it is too large Load diff

View file

@ -41,6 +41,7 @@ import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import Data.Semigroup import Data.Semigroup
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
@ -88,6 +89,8 @@ data Parameters = Parameters {
hasSetE :: Bool, hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere. -- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool, hasPipefail :: Bool,
-- Whether this script has 'shopt -s execfail' anywhere.
hasExecfail :: Bool,
-- A linear (bad) analysis of data flow -- A linear (bad) analysis of data flow
variableFlow :: [StackData], variableFlow :: [StackData],
-- A map from Id to Token -- A map from Id to Token
@ -103,7 +106,7 @@ data Parameters = Parameters {
-- map from token id to start and end position -- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position), tokenPositions :: Map.Map Id (Position, Position),
-- Result from Control Flow Graph analysis (including data flow analysis) -- Result from Control Flow Graph analysis (including data flow analysis)
cfgAnalysis :: CF.CFGAnalysis cfgAnalysis :: Maybe CF.CFGAnalysis
} deriving (Show) } deriving (Show)
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
@ -196,8 +199,10 @@ makeCommentWithFix severity id code str fix =
} }
in force withFix in force withFix
-- makeParameters :: CheckSpec -> Parameters
makeParameters spec = params makeParameters spec = params
where where
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
params = Parameters { params = Parameters {
rootNode = root, rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
@ -206,26 +211,35 @@ makeParameters spec = params
case shellType params of case shellType params of
Bash -> isOptionSet "lastpipe" root Bash -> isOptionSet "lastpipe" root
Dash -> False Dash -> False
BusyboxSh -> False
Sh -> False Sh -> False
Ksh -> True, Ksh -> True,
hasInheritErrexit = hasInheritErrexit =
case shellType params of case shellType params of
Bash -> isOptionSet "inherit_errexit" root Bash -> isOptionSet "inherit_errexit" root
Dash -> True Dash -> True
BusyboxSh -> True
Sh -> True Sh -> True
Ksh -> False, Ksh -> False,
hasPipefail = hasPipefail =
case shellType params of case shellType params of
Bash -> isOptionSet "pipefail" root Bash -> isOptionSet "pipefail" root
Dash -> True Dash -> True
BusyboxSh -> isOptionSet "pipefail" root
Sh -> True Sh -> True
Ksh -> isOptionSet "pipefail" root, Ksh -> isOptionSet "pipefail" root,
hasExecfail =
case shellType params of
Bash -> isOptionSet "execfail" root
_ -> False,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
idMap = getTokenMap root, idMap = getTokenMap root,
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec, tokenPositions = asTokenPositions spec,
cfgAnalysis = CF.analyzeControlFlow cfParams root cfgAnalysis = do
guard extendedAnalysis
return $ CF.analyzeControlFlow cfParams root
} }
cfParams = CF.CFGParameters { cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params, CF.cfLastpipe = hasLastpipe params,
@ -284,8 +298,8 @@ prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
determineShellTest = determineShellTest' Nothing determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@ -333,12 +347,12 @@ isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict shell tree t = isQuoteFreeNode strict shell tree t =
isQuoteFreeElement t || isQuoteFreeElement t ||
(fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t) (fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t)
where where
-- Is this node self-quoting in itself? -- Is this node self-quoting in itself?
isQuoteFreeElement t = isQuoteFreeElement t =
case t of case t of
T_Assignment {} -> assignmentIsQuoting t T_Assignment id _ _ _ _ -> assignmentIsQuoting id
T_FdRedirect {} -> True T_FdRedirect {} -> True
_ -> False _ -> False
@ -350,7 +364,7 @@ isQuoteFreeNode strict shell tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True TA_Sequence {} -> return True
T_Arithmetic {} -> return True T_Arithmetic {} -> return True
T_Assignment {} -> return $ assignmentIsQuoting t T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id
T_Redirecting {} -> return False T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True T_DollarDoubleQuoted _ _ -> return True
@ -365,11 +379,11 @@ isQuoteFreeNode strict shell tree t =
-- Check whether this assignment is self-quoting due to being a recognized -- Check whether this assignment is self-quoting due to being a recognized
-- assignment passed to a Declaration Utility. This will soon be required -- assignment passed to a Declaration Utility. This will soon be required
-- by POSIX: https://austingroupbugs.net/view.php?id=351 -- by POSIX: https://austingroupbugs.net/view.php?id=351
assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t) assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
shellParsesParamsAsAssignments = shell /= Sh shellParsesParamsAsAssignments = shell /= Sh
-- Is this assignment a parameter to a command like export/typeset/etc? -- Is this assignment a parameter to a command like export/typeset/etc?
isAssignmentParamToCommand (T_Assignment id _ _ _ _) = isAssignmentParamToCommand id =
case Map.lookup id tree of case Map.lookup id tree of
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args) Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
_ -> False _ -> False
@ -395,7 +409,7 @@ isParamTo tree cmd =
-- Get the parent command (T_Redirecting) of a Token, if any. -- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t = getClosestCommand tree t =
findFirst findCommand $ getPath tree t findFirst findCommand $ NE.toList $ getPath tree t
where where
findCommand t = findCommand t =
case t of case t of
@ -409,7 +423,7 @@ getClosestCommandM t = do
return $ getClosestCommand (parentMap params) t return $ getClosestCommand (parentMap params) t
-- Is the token used as a command name (the first word in a T_SimpleCommand)? -- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
where where
go currentId (T_NormalWord id [word]:rest) go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest | currentId == getId word = go id rest
@ -426,7 +440,9 @@ getPathM t = do
return $ getPath (parentMap params) t return $ getPath (parentMap params) t
isParentOf tree parent child = isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child any (\t -> parentId == getId t) (getPath tree child)
where
parentId = getId parent
parents params = getPath (parentMap params) parents params = getPath (parentMap params)
@ -525,7 +541,9 @@ getModifiedVariables t =
T_BatsTest {} -> [ T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal), (t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger), (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". -- Count [[ -v foo ]] as an "assignment".
@ -547,8 +565,12 @@ getModifiedVariables t =
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
T_CoProc _ name _ -> T_CoProc _ Nothing _ ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] [(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 --Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)] T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
@ -810,7 +832,7 @@ getReferencedVariables parents t =
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
isArithmeticAssignment t = case getPath parents t of isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False _ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
@ -892,15 +914,6 @@ supportsArrays Bash = True
supportsArrays Ksh = True supportsArrays Ksh = True
supportsArrays _ = False 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
Sh -> False
isTrueAssignmentSource c = isTrueAssignmentSource c =
case c of case c of
DataString SourceChecked -> False DataString SourceChecked -> False
@ -918,6 +931,14 @@ modifiesVariable params token name =
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
_ -> False _ -> 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 [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -51,6 +51,7 @@ import Control.Monad.Identity
import Data.Array.Unboxed import Data.Array.Unboxed
import Data.Array.ST import Data.Array.ST
import Data.List hiding (map) import Data.List hiding (map)
import qualified Data.List.NonEmpty as NE
import Data.Maybe import Data.Maybe
import qualified Data.Map as M import qualified Data.Map as M
import qualified Data.Set as S import qualified Data.Set as S
@ -111,8 +112,8 @@ data CFEdge =
-- Actions we track -- Actions we track
data CFEffect = data CFEffect =
CFSetProps Scope String (S.Set CFVariableProp) CFSetProps (Maybe Scope) String (S.Set CFVariableProp)
| CFUnsetProps Scope String (S.Set CFVariableProp) | CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp)
| CFReadVariable String | CFReadVariable String
| CFWriteVariable String CFValue | CFWriteVariable String CFValue
| CFWriteGlobal String CFValue | CFWriteGlobal String CFValue
@ -192,7 +193,7 @@ buildGraph params root =
base base
idToRange = M.fromList mapping idToRange = M.fromList mapping
isRealEdge (from, to, edge) = case edge of CFEFlow -> True; _ -> False isRealEdge (from, to, edge) = case edge of CFEFlow -> True; CFEExit -> True; _ -> False
onlyRealEdges = filter isRealEdge edges onlyRealEdges = filter isRealEdge edges
(_, mainExit) = fromJust $ M.lookup (getId root) idToRange (_, mainExit) = fromJust $ M.lookup (getId root) idToRange
@ -294,19 +295,19 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
regularEdges = filter isRegularEdge edges regularEdges = filter isRegularEdge edges
inDegree = counter $ map (\(from,to,_) -> from) regularEdges inDegree = counter $ map (\(from,to,_) -> from) regularEdges
outDegree = counter $ map (\(from,to,_) -> to) 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 candidateNodes = S.filter isLinear structuralNodes
edgesToCollapse = S.fromList $ filter filterEdges regularEdges edgesToCollapse = S.fromList $ filter filterEdges regularEdges
remapping :: M.Map Node Node remapping :: M.Map Node Node
remapping = foldl' (\m (new, old) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse
recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping
filterEdges (a,b,_) = filterEdges (a,b,_) =
a `S.member` candidateNodes && b `S.member` candidateNodes a `S.member` candidateNodes && b `S.member` candidateNodes
orderEdge (a,b,_) = if a < b then (a,b) else (b,a) orderEdge (a,b,_) = if a < b then (b,a) else (a,b)
counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty counter = M.fromListWith (+) . map (\key -> (key, 1))
isRegularEdge (_, _, CFEFlow) = True isRegularEdge (_, _, CFEFlow) = True
isRegularEdge _ = False isRegularEdge _ = False
@ -316,11 +317,6 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =
Nothing -> node Nothing -> node
Just x -> recursiveLookup map x Just x -> recursiveLookup map x
isStructural (node, label) =
case label of
CFStructuralNode -> True
_ -> False
isLinear node = isLinear node =
M.findWithDefault 0 node inDegree == 1 M.findWithDefault 0 node inDegree == 1
&& M.findWithDefault 0 node outDegree == 1 && M.findWithDefault 0 node outDegree == 1
@ -494,7 +490,7 @@ build t = do
TA_Binary _ _ a b -> sequentially [a,b] TA_Binary _ _ a b -> sequentially [a,b]
TA_Expansion _ list -> sequentially list TA_Expansion _ list -> sequentially list
TA_Sequence _ list -> sequentially list TA_Sequence _ list -> sequentially list
TA_Parentesis _ t -> build t TA_Parenthesis _ t -> build t
TA_Trinary _ cond a b -> do TA_Trinary _ cond a b -> do
condition <- build cond condition <- build cond
@ -578,7 +574,7 @@ build t = do
T_Array _ list -> sequentially list T_Array _ list -> sequentially list
T_Assignment {} -> buildAssignment DefaultScope t T_Assignment {} -> buildAssignment Nothing t
T_Backgrounded id body -> do T_Backgrounded id body -> do
start <- newStructuralNode start <- newStructuralNode
@ -614,15 +610,15 @@ build t = do
T_CaseExpression id t [] -> build t T_CaseExpression id t [] -> build t
T_CaseExpression id t list -> do T_CaseExpression id t list@(hd:tl) -> do
start <- newStructuralNode start <- newStructuralNode
token <- build t token <- build t
branches <- mapM buildBranch list branches <- mapM buildBranch (hd NE.:| tl)
end <- newStructuralNode end <- newStructuralNode
let neighbors = zip branches $ tail branches let neighbors = zip (NE.toList branches) $ NE.tail branches
let (_, firstCond, _) = head branches let (_, firstCond, _) = NE.head branches
let (_, lastCond, lastBody) = last branches let (_, lastCond, lastBody) = NE.last branches
linkRange start token linkRange start token
linkRange token firstCond linkRange token firstCond
@ -672,10 +668,18 @@ build t = do
status <- newNodeRange $ CFSetExitCode id status <- newNodeRange $ CFSetExitCode id
linkRange cond status linkRange cond status
T_CoProc id maybeName t -> do T_CoProc id maybeNameToken t -> do
let name = fromMaybe "COPROC" maybeName -- 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 start <- newStructuralNode
parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray parent <- newNodeRange parentNode
child <- subshell id "coproc" $ build t child <- subshell id "coproc" $ build t
end <- newNodeRange $ CFSetExitCode id end <- newNodeRange $ CFSetExitCode id
@ -857,8 +861,8 @@ build t = do
status <- newNodeRange (CFSetExitCode id) status <- newNodeRange (CFSetExitCode id)
linkRange assignments status linkRange assignments status
T_SimpleCommand id vars list@(cmd:_) -> T_SimpleCommand id vars (cmd:args) ->
handleCommand t vars list $ getUnquotedLiteral cmd handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd
T_SingleQuoted _ _ -> none T_SingleQuoted _ _ -> none
@ -887,7 +891,9 @@ build t = do
T_Less _ -> none T_Less _ -> none
T_ParamSubSpecialChar _ _ -> none T_ParamSubSpecialChar _ _ -> none
x -> error ("Unimplemented: " ++ show x) x -> do
error ("Unimplemented: " ++ show x) -- STRIP
none
-- Still in `where` clause -- Still in `where` clause
forInHelper id name words body = do forInHelper id name words body = do
@ -923,8 +929,8 @@ handleCommand cmd vars args literalCmd = do
-- TODO: Handle assignments in declaring commands -- TODO: Handle assignments in declaring commands
case literalCmd of case literalCmd of
Just "exit" -> regularExpansion vars args $ handleExit Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit
Just "return" -> regularExpansion vars args $ handleReturn Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn
Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args
Just "declare" -> handleDeclare args Just "declare" -> handleDeclare args
@ -947,14 +953,14 @@ handleCommand cmd vars args literalCmd = do
-- This will mostly behave like 'command' but ok -- This will mostly behave like 'command' but ok
Just "builtin" -> Just "builtin" ->
case args of case args of
[_] -> regular _ NE.:| [] -> regular
(_:newargs@(newcmd:_)) -> (_ NE.:| newcmd:newargs) ->
handleCommand newcmd vars newargs $ getLiteralString newcmd handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd
Just "command" -> Just "command" ->
case args of case args of
[_] -> regular _ NE.:| [] -> regular
(_:newargs@(newcmd:_)) -> (_ NE.:| newcmd:newargs) ->
handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd
_ -> regular _ -> regular
where where
@ -982,7 +988,7 @@ handleCommand cmd vars args literalCmd = do
unreachable <- newNode CFUnreachable unreachable <- newNode CFUnreachable
return $ Range ret unreachable return $ Range ret unreachable
handleUnset (cmd:args) = do handleUnset (cmd NE.:| args) = do
case () of case () of
_ | "n" `elem` flagNames -> unsetWith CFUndefineNameref _ | "n" `elem` flagNames -> unsetWith CFUndefineNameref
_ | "v" `elem` flagNames -> unsetWith CFUndefineVariable _ | "v" `elem` flagNames -> unsetWith CFUndefineVariable
@ -994,14 +1000,14 @@ handleCommand cmd vars args literalCmd = do
(names, flags) = partition (null . fst) pairs (names, flags) = partition (null . fst) pairs
flagNames = map fst flags flagNames = map fst flags
literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")] literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")]
literalNames = mapMaybe (\(_, t) -> getLiteralString t >>= (return . (,) t)) names literalNames = mapMaybe (\(_, t) -> (,) t <$> getLiteralString t) names
-- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id -- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id
unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames
variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)=" variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)="
handleDeclare (cmd:args) = do handleDeclare (cmd NE.:| args) = do
isFunc <- asks cfIsFunction isFunc <- asks cfIsFunction
-- This is a bit of a kludge: we don't have great support for things like -- This is a bit of a kludge: we don't have great support for things like
-- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x -- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x
@ -1028,9 +1034,9 @@ handleCommand cmd vars args literalCmd = do
scope isFunc = scope isFunc =
case () of case () of
_ | global -> GlobalScope _ | global -> Just GlobalScope
_ | isFunc -> LocalScope _ | isFunc -> Just LocalScope
_ -> DefaultScope _ -> Nothing
addedProps = S.fromList $ concat $ [ addedProps = S.fromList $ concat $ [
[ CFVPArray | array ], [ CFVPArray | array ],
@ -1058,7 +1064,7 @@ handleCommand cmd vars args literalCmd = do
let let
id = getId t id = getId t
pre = [t] pre = [t]
literal = fromJust $ getLiteralStringExt (const $ Just "\0") t literal = getLiteralStringDef "\0" t
isKnown = '\0' `notElem` literal isKnown = '\0' `notElem` literal
match = fmap head $ variableAssignRegex `matchRegex` literal match = fmap head $ variableAssignRegex `matchRegex` literal
name = fromMaybe literal match name = fromMaybe literal match
@ -1090,7 +1096,7 @@ handleCommand cmd vars args literalCmd = do
in in
concatMap (drop 1) plusses concatMap (drop 1) plusses
handlePrintf (cmd:args) = handlePrintf (cmd NE.:| args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@ -1099,7 +1105,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString
handleWait (cmd:args) = handleWait (cmd NE.:| args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@ -1108,7 +1114,7 @@ handleCommand cmd vars args literalCmd = do
name <- getLiteralString arg name <- getLiteralString arg
return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger
handleMapfile (cmd:args) = handleMapfile (cmd NE.:| args) =
newNodeRange $ CFApplyEffects [findVar] newNodeRange $ CFApplyEffects [findVar]
where where
findVar = findVar =
@ -1128,7 +1134,7 @@ handleCommand cmd vars args literalCmd = do
guard $ isVariableName name guard $ isVariableName name
return (getId c, name) return (getId c, name)
handleRead (cmd:args) = newNodeRange $ CFApplyEffects main handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main
where where
main = fromMaybe fallback $ do main = fromMaybe fallback $ do
flags <- getGnuOpts flagsForRead args flags <- getGnuOpts flagsForRead args
@ -1158,7 +1164,7 @@ handleCommand cmd vars args literalCmd = do
in in
map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault
handleDEFINE (cmd:args) = handleDEFINE (cmd NE.:| args) =
newNodeRange $ CFApplyEffects $ maybeToList findVar newNodeRange $ CFApplyEffects $ maybeToList findVar
where where
findVar = do findVar = do
@ -1168,14 +1174,14 @@ handleCommand cmd vars args literalCmd = do
return $ IdTagged (getId name) $ CFWriteVariable str CFValueString return $ IdTagged (getId name) $ CFWriteVariable str CFValueString
handleOthers id vars args cmd = handleOthers id vars args cmd =
regularExpansion vars args $ do regularExpansion vars (NE.toList args) $ do
exe <- newNodeRange $ CFExecuteCommand cmd exe <- newNodeRange $ CFExecuteCommand cmd
status <- newNodeRange $ CFSetExitCode id status <- newNodeRange $ CFSetExitCode id
linkRange exe status linkRange exe status
regularExpansion vars args p = do regularExpansion vars args p = do
args <- sequentially args args <- sequentially args
assignments <- mapM (buildAssignment PrefixScope) vars assignments <- mapM (buildAssignment (Just PrefixScope)) vars
exe <- p exe <- p
dropAssignments <- dropAssignments <-
if null vars if null vars
@ -1187,15 +1193,15 @@ handleCommand cmd vars args literalCmd = do
linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments
regularExpansionWithStatus vars args@(cmd:_) p = do regularExpansionWithStatus vars args@(cmd NE.:| _) p = do
initial <- regularExpansion vars args p initial <- regularExpansion vars (NE.toList args) p
status <- newNodeRange $ CFSetExitCode (getId cmd) status <- newNodeRange $ CFSetExitCode (getId cmd)
linkRange initial status linkRange initial status
none = newStructuralNode none = newStructuralNode
data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope data Scope = GlobalScope | LocalScope | PrefixScope
deriving (Eq, Ord, Show, Generic, NFData) deriving (Eq, Ord, Show, Generic, NFData)
buildAssignment scope t = do buildAssignment scope t = do
@ -1209,10 +1215,10 @@ buildAssignment scope t = do
let valueType = if null indices then f id value else CFValueArray let valueType = if null indices then f id value else CFValueArray
let scoper = let scoper =
case scope of case scope of
PrefixScope -> CFWritePrefix Just PrefixScope -> CFWritePrefix
LocalScope -> CFWriteLocal Just LocalScope -> CFWriteLocal
GlobalScope -> CFWriteGlobal Just GlobalScope -> CFWriteGlobal
DefaultScope -> CFWriteVariable Nothing -> CFWriteVariable
write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType
linkRanges [expand, index, read, write] linkRanges [expand, index, read, write]
where where
@ -1301,7 +1307,10 @@ findPostDominators mainexit graph = asArray
reversed = grev withExitEdges reversed = grev withExitEdges
postDoms = dom reversed mainexit postDoms = dom reversed mainexit
(_, maxNode) = nodeRange graph (_, maxNode) = nodeRange graph
asArray = array (0, maxNode) postDoms -- Holes in the array cause "Exception: (Array.!): undefined array element" while
-- inspecting/debugging, so fill the array first and then update.
initializedArray = listArray (0, maxNode) $ repeat []
asArray = initializedArray // postDoms
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -59,6 +59,8 @@ module ShellCheck.CFGAnalysis (
,getIncomingState ,getIncomingState
,getOutgoingState ,getOutgoingState
,doesPostDominate ,doesPostDominate
,variableMayBeDeclaredInteger
,variableMayBeAssignedInteger
,ShellCheck.CFGAnalysis.runTests -- STRIP ,ShellCheck.CFGAnalysis.runTests -- STRIP
) where ) where
@ -131,7 +133,7 @@ internalToExternal s =
literalValue = Nothing 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 -- Conveniently get the state before a token id
getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState
@ -153,6 +155,20 @@ doesPostDominate analysis target base = fromMaybe False $ do
(targetStart, _) <- M.lookup target $ tokenToRange analysis (targetStart, _) <- M.lookup target $ tokenToRange analysis
return $ targetStart `elem` (postDominators analysis ! baseEnd) return $ targetStart `elem` (postDominators analysis ! baseEnd)
-- See if any execution path results in the variable containing a state
variableMayHaveState :: ProgramState -> String -> CFVariableProp -> Maybe Bool
variableMayHaveState state var property = do
value <- M.lookup var $ variablesInScope state
return $ any (S.member property) $ variableProperties value
-- See if any execution path declares the variable an integer (declare -i).
variableMayBeDeclaredInteger state var = variableMayHaveState state var CFVPInteger
-- See if any execution path suggests the variable may contain an integer value
variableMayBeAssignedInteger state var = do
value <- M.lookup var $ variablesInScope state
return $ (numericalStatus $ variableValue value) >= NumericalStatusMaybe
getDataForNode analysis node = M.lookup node $ nodeToData analysis getDataForNode analysis node = M.lookup node $ nodeToData analysis
-- The current state of data flow at a point in the program, potentially as a diff -- The current state of data flow at a point in the program, potentially as a diff
@ -283,7 +299,6 @@ depsToState set = foldl insert newInternalState $ S.toList set
PrefixScope -> (sPrefixValues, insertPrefix) PrefixScope -> (sPrefixValues, insertPrefix)
LocalScope -> (sLocalValues, insertLocal) LocalScope -> (sLocalValues, insertLocal)
GlobalScope -> (sGlobalValues, insertGlobal) GlobalScope -> (sGlobalValues, insertGlobal)
DefaultScope -> error $ pleaseReport "Unresolved scope in dependency"
alreadyExists = isJust $ vmLookup name $ mapToCheck state alreadyExists = isJust $ vmLookup name $ mapToCheck state
in in
@ -657,7 +672,7 @@ vmPatch base diff =
_ | vmIsQuickEqual base diff -> diff _ | vmIsQuickEqual base diff -> diff
_ -> VersionedMap { _ -> VersionedMap {
mapVersion = -1, 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. -- Set a variable. This includes properties. Applies it to the appropriate scope.
@ -814,7 +829,7 @@ lookupStack' functionOnly get dep def ctx key = do
f (s:rest) = do f (s:rest) = do
-- Go up the stack until we find the value, and add -- Go up the stack until we find the value, and add
-- a dependency on each state (including where it was found) -- a dependency on each state (including where it was found)
res <- fromMaybe (f rest) (return <$> get (stackState s) key) res <- maybe (f rest) return (get (stackState s) key)
modifySTRef (dependencies s) $ S.insert $ dep key res modifySTRef (dependencies s) $ S.insert $ dep key res
return res return res
@ -1104,34 +1119,34 @@ transferEffect ctx effect =
CFSetProps scope name props -> CFSetProps scope name props ->
case scope of case scope of
DefaultScope -> do Nothing -> do
state <- readVariable ctx name state <- readVariable ctx name
writeVariable ctx name $ addProperties props state writeVariable ctx name $ addProperties props state
GlobalScope -> do Just GlobalScope -> do
state <- readGlobal ctx name state <- readGlobal ctx name
writeGlobal ctx name $ addProperties props state writeGlobal ctx name $ addProperties props state
LocalScope -> do Just LocalScope -> do
out <- readSTRef (cOutput ctx) out <- readSTRef (cOutput ctx)
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ addProperties props state writeLocal ctx name $ addProperties props state
PrefixScope -> do Just PrefixScope -> do
-- Prefix values become local -- Prefix values become local
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ addProperties props state writeLocal ctx name $ addProperties props state
CFUnsetProps scope name props -> CFUnsetProps scope name props ->
case scope of case scope of
DefaultScope -> do Nothing -> do
state <- readVariable ctx name state <- readVariable ctx name
writeVariable ctx name $ removeProperties props state writeVariable ctx name $ removeProperties props state
GlobalScope -> do Just GlobalScope -> do
state <- readGlobal ctx name state <- readGlobal ctx name
writeGlobal ctx name $ removeProperties props state writeGlobal ctx name $ removeProperties props state
LocalScope -> do Just LocalScope -> do
out <- readSTRef (cOutput ctx) out <- readSTRef (cOutput ctx)
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state writeLocal ctx name $ removeProperties props state
PrefixScope -> do Just PrefixScope -> do
-- Prefix values become local -- Prefix values become local
state <- readLocal ctx name state <- readLocal ctx name
writeLocal ctx name $ removeProperties props state writeLocal ctx name $ removeProperties props state
@ -1271,7 +1286,7 @@ dataflow ctx entry = do
else do else do
let (next, rest) = S.deleteFindMin ps let (next, rest) = S.deleteFindMin ps
nexts <- process states next 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 f (n-1) pending states
process states node = do process states node = do
@ -1335,7 +1350,7 @@ analyzeControlFlow params t =
-- All nodes we've touched -- All nodes we've touched
invocations <- readSTRef $ cInvocations ctx 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 -- Invoke all functions that were declared but not invoked
-- This is so that we still get warnings for dead code -- This is so that we still get warnings for dead code
@ -1358,7 +1373,7 @@ analyzeControlFlow params t =
-- Fill in the map with unreachable states for anything we didn't get to -- 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 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 -- Convert to external states
let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates

View file

@ -25,6 +25,7 @@ import ShellCheck.ASTLib
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
import Debug.Trace -- DO NOT SUBMIT
import Data.Either import Data.Either
import Data.Functor import Data.Functor
import Data.List import Data.List
@ -86,6 +87,7 @@ checkScript sys spec = do
asCheckSourced = csCheckSourced spec, asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed, asExecutionMode = Executed,
asTokenPositions = tokenPositions, asTokenPositions = tokenPositions,
asExtendedAnalysis = csExtendedAnalysis spec,
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
} where as = newAnalysisSpec root } where as = newAnalysisSpec root
let analysisMessages = let analysisMessages =
@ -508,5 +510,55 @@ prop_rcCanSuppressEarlyProblems2 = null result
csScript = "!/bin/bash\necho 'hello world'" csScript = "!/bin/bash\necho 'hello world'"
} }
prop_sourceWithHereDocWorks = null result
where
result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
where
result = check "cat << eof"
prop_hereDocsWillHaveParsedIndices = null result
where
result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
prop_rcCanSuppressDfa = null result
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\nexit; foo;"
}
prop_fileCanSuppressDfa = null $ traceShowId result
where
result = checkWithRc "" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
}
prop_fileWinsWhenSuppressingDfa1 = null result
where
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
}
prop_fileWinsWhenSuppressingDfa2 = result == [2317]
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
}
prop_flagWinsWhenSuppressingDfa1 = result == [2317]
where
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
csExtendedAnalysis = Just True
}
prop_flagWinsWhenSuppressingDfa2 = null result
where
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
csExtendedAnalysis = Just False
}
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View file

@ -20,6 +20,7 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE PatternGuards #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
@ -42,6 +43,7 @@ import Data.Functor.Identity
import qualified Data.Graph.Inductive.Graph as G import qualified Data.Graph.Inductive.Graph as G
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import qualified Data.Set as S import qualified Data.Set as S
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
@ -181,14 +183,13 @@ checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
name <- getLiteralString cmd name <- getLiteralString cmd
return $ return $
if '/' `elem` name if | '/' `elem` name ->
then
M.findWithDefault nullCheck (Basename $ basename name) map t M.findWithDefault nullCheck (Basename $ basename name) map t
else if name == "builtin" && not (null rest) then | name == "builtin", (h:_) <- rest ->
let t' = T_SimpleCommand id cmdPrefix rest let t' = T_SimpleCommand id cmdPrefix rest
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest selectedBuiltin = onlyLiteralString h
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t' in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
else do | otherwise -> do
M.findWithDefault nullCheck (Exactly name) map t M.findWithDefault nullCheck (Exactly name) map t
M.findWithDefault nullCheck (Basename name) map t M.findWithDefault nullCheck (Basename name) map t
@ -299,7 +300,7 @@ checkExpr = CommandCheck (Basename "expr") f where
"'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|." "'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
[first, second] | [first, second] |
(fromMaybe "" $ getLiteralString first) /= "length" onlyLiteralString first /= "length"
&& not (willSplit first || willSplit second) -> do && not (willSplit first || willSplit second) -> do
checkOp first checkOp first
warn (getId t) 2307 warn (getId t) 2307
@ -930,7 +931,7 @@ prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1" prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where checkTimedCommand = CommandCheck (Exactly "time") f where
f (T_SimpleCommand _ _ (c:args@(_:_))) = f (T_SimpleCommand _ _ (c:args@(_:_))) =
whenShell [Sh, Dash] $ do whenShell [Sh, Dash, BusyboxSh] $ do
let cmd = last args -- "time" is parsed with a command as argument let cmd = last args -- "time" is parsed with a command as argument
when (isPiped cmd) $ when (isPiped cmd) $
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead." warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
@ -954,7 +955,7 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
prop_checkLocalScope1 = verify checkLocalScope "local foo=3" prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }" prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope = CommandCheck (Exactly "local") $ \t -> checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t path <- getPathM t
unless (any isFunctionLike path) $ unless (any isFunctionLike path) $
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
@ -1005,8 +1006,8 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
sequence_ $ do sequence_ $ do
options <- getLiteralString arg1 options <- getLiteralString arg1
getoptsVar <- getLiteralString name getoptsVar <- getLiteralString name
(T_WhileExpression _ _ body) <- findFirst whileLoop path (T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path)
caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0 T_CaseExpression id var list <- mapMaybe findCase body !!! 0
-- Make sure getopts name and case variable matches -- Make sure getopts name and case variable matches
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var [T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
@ -1016,11 +1017,11 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
-- Make sure the variable isn't modified -- Make sure the variable isn't modified
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list
f _ = return () f _ = return ()
check :: Id -> [String] -> Token -> Analysis check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis
check optId opts (T_CaseExpression id _ list) = do check optId opts id list = do
unless (Nothing `M.member` handledMap) $ do unless (Nothing `M.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
@ -1236,8 +1237,7 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = sequence_ $ do f t = sequence_ $ do
opts <- parseOpts $ arguments t opts <- parseOpts $ arguments t
let nonFlags = [x | ("",(x, _)) <- opts] (_,(commandArg, _)) <- find (null . fst) opts
commandArg <- nonFlags !!! 0
command <- getLiteralString commandArg command <- getLiteralString commandArg
guard $ command `elem` builtins guard $ command `elem` builtins
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?" return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
@ -1430,26 +1430,27 @@ prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x" prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where where
check t = foldM_ perArg M.empty $ arguments t check t = do
maybeCfga <- asks cfgAnalysis
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
perArg leftArgs t = perArg cfga leftArgs t =
case t of case t of
T_Assignment id _ name idx t -> do T_Assignment id _ name idx t -> do
warnIfBackreferencing leftArgs $ t:idx warnIfBackreferencing cfga leftArgs $ t:idx
return $ M.insert name id leftArgs return $ M.insert name id leftArgs
t -> do t -> do
warnIfBackreferencing leftArgs [t] warnIfBackreferencing cfga leftArgs [t]
return leftArgs return leftArgs
warnIfBackreferencing backrefs l = do warnIfBackreferencing cfga backrefs l = do
references <- findReferences l references <- findReferences cfga l
let reused = M.intersection backrefs references let reused = M.intersection backrefs references
mapM msg $ M.toList reused mapM msg $ M.toList reused
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s." msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
findReferences list = do findReferences cfga list = do
cfga <- asks cfgAnalysis
let graph = CF.graph cfga let graph = CF.graph cfga
let nodesMap = CF.tokenToNodes cfga let nodesMap = CF.tokenToNodes cfga
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list

View file

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

View file

@ -19,6 +19,7 @@
-} -}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST import ShellCheck.AST
@ -60,6 +61,9 @@ checks = [
,checkBraceExpansionVars ,checkBraceExpansionVars
,checkMultiDimensionalArrays ,checkMultiDimensionalArrays
,checkPS1Assignments ,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
,checkNegatedUnaryOps
] ]
testChecker (ForShell _ t) = testChecker (ForShell _ t) =
@ -73,22 +77,24 @@ verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
where where
f t@(TA_Expansion id _) = sequence_ $ do f t@(TA_Expansion id _) = sequence_ $ do
str <- getLiteralString t first:rest <- getLiteralString t
first <- str !!! 0 guard $ isDigit first && '.' `elem` rest
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return () f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" 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_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file" prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42"
prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]"
prop_checkBashisms6d = verify checkBashisms "test foo =~ bar"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
@ -104,6 +110,7 @@ prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt" prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
prop_checkBashisms20 = verify checkBashisms "read -ra foo" prop_checkBashisms20 = verify checkBashisms "read -ra foo"
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]" prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
prop_checkBashisms21b = verify checkBashisms "test -a foo"
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]" prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT" prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM" prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
@ -184,49 +191,82 @@ prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}" prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}" prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo" prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
checkBashisms = ForShell [Sh, Dash] $ \t -> do prop_checkBashisms100 = verify checkBashisms "read -r"
prop_checkBashisms101 = verify checkBashisms "read"
prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
prop_checkBashisms103 = verifyNot checkBashisms "read foo"
prop_checkBashisms104 = verifyNot checkBashisms "read ''"
prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
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 params <- ask
kludge params t kludge params t
where where
-- This code was copy-pasted from Analytics where params was a variable -- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism kludge params = bashism
where where
isDash = shellType params == Dash isBusyboxSh = shellType params == BusyboxSh
isDash = shellType params == Dash || isBusyboxSh
warnMsg id code s = warnMsg id code s =
if isDash if isDash
then err id code $ "In dash, " ++ s ++ " not supported." then err id code $ "In dash, " ++ s ++ " not supported."
else warn id code $ "In POSIX sh, " ++ s ++ " undefined." else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
asStr = getLiteralString
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is" bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob 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_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are" bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is" bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is" bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are" bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is" bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is" bashism (T_Condition id DoubleBracket _) =
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = bashism (TC_Binary id _ op _ _) =
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" checkTestOp bashismBinaryTestFlags op id
bashism (TC_Binary id SingleBracket op _ _) bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) =
| op `elem` [ "-ot", "-nt", "-ef" ] = checkTestOp bashismBinaryTestFlags op id
unless isDash $ warnMsg id 3013 $ op ++ " is" bashism (TC_Unary id _ op _) =
bashism (TC_Binary id SingleBracket "==" _ _) = checkTestOp bashismUnaryTestFlags op id
warnMsg id 3014 "== in place of = is" bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) =
bashism (TC_Binary id SingleBracket "=~" _ _) = checkTestOp bashismUnaryTestFlags op id
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 (TC_Unary id _ "-a" _) =
warnMsg id 3017 "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] = | op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id 3018 $ filter (/= '|') op ++ " is" warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
unless isBusyboxSh $ warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) = bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is" unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are" bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
@ -246,7 +286,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
warnMsg id 3028 $ str ++ " is" warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion unless isBusyboxSh $ mapM_ check simpleExpansions
mapM_ check advancedExpansions
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id 3028 $ var ++ " is" warnMsg id 3028 $ var ++ " is"
where where
@ -274,7 +315,11 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex = | t `isCommand` "echo" && argString `matches` flagRegex =
if isDash if isBusyboxSh
then
unless (argString `matches` busyboxFlagRegex) $
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
else if isDash
then then
when (argString /= "-n") $ when (argString /= "-n") $
warnMsg (getId arg) 3036 "echo flags besides -n" warnMsg (getId arg) 3036 "echo flags besides -n"
@ -283,6 +328,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
where where
argString = concat $ oversimplify arg argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$" flagRegex = mkRegex "^-[eEsn]+$"
busyboxFlagRegex = mkRegex "^-[en]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
@ -356,7 +402,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is" when (name == "source" && not isBusyboxSh) $
warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $ when (name == "trap") $
let let
check token = sequence_ $ do check token = sequence_ $ do
@ -365,7 +412,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
return $ do return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is" warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $ when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
warnMsg (getId token) 3048 warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is" "prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $ when (not isDash && upper /= str) $
@ -379,6 +426,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
let literal = onlyLiteralString format let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) 3050 "printf %q is" return $ warnMsg (getId format) 3050 "printf %q is"
when (name == "read" && all isFlag rest) $
warnMsg (getId cmd) 3061 "read without a variable is"
where where
unsupportedCommands = [ unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
@ -392,17 +442,19 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
("hash", Just $ if isDash then ["r", "v"] else ["r"]), ("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]), ("jobs", Just ["l", "p"]),
("printf", Just []), ("printf", Just []),
("read", Just $ if isDash then ["r", "p"] else ["r"]), ("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
("readonly", Just ["p"]), ("readonly", Just ["p"]),
("trap", Just []), ("trap", Just []),
("type", Just []), ("type", Just $ if isBusyboxSh then ["p"] else []),
("ulimit", if isDash then Nothing else Just ["f"]), ("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]), ("umask", Just ["S"]),
("unset", Just ["f", "v"]), ("unset", Just ["f", "v"]),
("wait", Just []) ("wait", Just [])
] ]
bashism t@(T_SourceCommand id src _) bashism t@(T_SourceCommand id src _)
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is" | getCommandName src == Just "source" =
unless isBusyboxSh $
warnMsg id 3051 "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _)) bashism (TA_Expansion _ (T_Literal id str : _))
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is" | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where where
@ -410,14 +462,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [ advancedExpansions = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"), (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"), (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
]
simpleExpansions = let re = mkRegex in [
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"), (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"), (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is") (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
] ]
bashVars = [ bashVars = [
@ -443,6 +497,50 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
Assignment (_, _, name, _) -> name == var Assignment (_, _, name, _) -> name == var
_ -> False _ -> 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_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")" prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
@ -558,5 +656,46 @@ checkPS1Assignments = ForShell [Bash] f
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
where
f token = case token of
T_Banged id (T_Banged _ _) ->
err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1."
_ -> return ()
prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
where
f token = case token of
T_Pipeline _ _ cmds -> mapM_ check cmds
_ -> return ()
check token = case token of
T_Banged id _ ->
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 [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -49,6 +49,7 @@ internalVariables = [
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1", "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
"auto_resume", "histchars", "auto_resume", "histchars",
-- Other -- Other
@ -62,6 +63,9 @@ internalVariables = [
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return" "flags_error", "flags_return"
-- Bats
,"stderr", "stderr_lines"
] ]
specialIntegerVariables = [ specialIntegerVariables = [
@ -75,7 +79,7 @@ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM", "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS", "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES" "HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
-- shflags -- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@ -156,11 +160,15 @@ shellForExecutable name =
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"bats" -> return Bash "bats" -> return Bash
"busybox" -> return BusyboxSh -- Used for directives and --shell=busybox
"busybox sh" -> return BusyboxSh
"busybox ash" -> return BusyboxSh
"dash" -> return Dash "dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this. "ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh "ksh" -> return Ksh
"ksh88" -> return Ksh "ksh88" -> return Ksh
"ksh93" -> return Ksh "ksh93" -> return Ksh
"oksh" -> return Ksh
_ -> Nothing _ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:" flagsForRead = "sreu:n:N:i:p:a:t:"

View file

@ -24,8 +24,8 @@ import ShellCheck.Formatter.Format
import Data.Char import Data.Char
import Data.List import Data.List
import GHC.Exts
import System.IO import System.IO
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = return Formatter { format = return Formatter {
@ -45,12 +45,12 @@ outputResults cr sys =
else mapM_ outputGroup fileGroups else mapM_ outputGroup fileGroups
where where
comments = crComments cr comments = crComments cr
fileGroups = groupWith sourceFile comments fileGroups = NE.groupWith sourceFile comments
outputGroup group = do outputGroup group = do
let filename = sourceFile (head group) let filename = sourceFile (NE.head group)
result <- siReadFile sys (Just True) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
outputFile filename contents group outputFile filename contents (NE.toList group)
outputFile filename contents warnings = do outputFile filename contents warnings = do
let comments = makeNonVirtual warnings contents let comments = makeNonVirtual warnings contents

View file

@ -23,8 +23,8 @@ import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Data.List import Data.List
import GHC.Exts
import System.IO import System.IO
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = return Formatter { format = return Formatter {
@ -39,13 +39,13 @@ outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
outputAll cr sys = mapM_ f groups outputAll cr sys = mapM_ f groups
where where
comments = crComments cr comments = crComments cr
groups = groupWith sourceFile comments groups = NE.groupWith sourceFile comments
f :: [PositionedComment] -> IO () f :: NE.NonEmpty PositionedComment -> IO ()
f group = do f group = do
let filename = sourceFile (head group) let filename = sourceFile (NE.head group)
result <- siReadFile sys (Just True) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
outputResult filename contents group outputResult filename contents (NE.toList group)
outputResult filename contents warnings = do outputResult filename contents warnings = do
let comments = makeNonVirtual warnings contents let comments = makeNonVirtual warnings contents

View file

@ -27,9 +27,9 @@ import Control.DeepSeq
import Data.Aeson import Data.Aeson
import Data.IORef import Data.IORef
import Data.Monoid import Data.Monoid
import GHC.Exts
import System.IO import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.List.NonEmpty as NE
format :: IO Formatter format :: IO Formatter
format = do format = do
@ -114,10 +114,10 @@ outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups collectResult ref cr sys = mapM_ f groups
where where
comments = crComments cr comments = crComments cr
groups = groupWith sourceFile comments groups = NE.groupWith sourceFile comments
f :: [PositionedComment] -> IO () f :: NE.NonEmpty PositionedComment -> IO ()
f group = do f group = do
let filename = sourceFile (head group) let filename = sourceFile (NE.head group)
result <- siReadFile sys (Just True) filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
let comments' = makeNonVirtual comments contents let comments' = makeNonVirtual comments contents

View file

@ -31,9 +31,9 @@ import Data.Ord
import Data.IORef import Data.IORef
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import GHC.Exts
import System.IO import System.IO
import System.Info import System.Info
import qualified Data.List.NonEmpty as NE
wikiLink = "https://www.shellcheck.net/wiki/" wikiLink = "https://www.shellcheck.net/wiki/"
@ -117,19 +117,19 @@ outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options color <- getColorFunc $ foColorOption options
let comments = crComments result let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options) appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = groupWith sourceFile comments let fileGroups = NE.groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do outputForFile color sys comments = do
let fileName = sourceFile (head comments) let fileName = sourceFile (NE.head comments)
result <- siReadFile sys (Just True) fileName result <- siReadFile sys (Just True) fileName
let contents = either (const "") id result let contents = either (const "") id result
let fileLinesList = lines contents let fileLinesList = lines contents
let lineCount = length fileLinesList let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments let groups = NE.groupWith lineNo comments
forM_ groups $ \commentsForLine -> do forM_ groups $ \commentsForLine -> do
let lineNum = fromIntegral $ lineNo (head commentsForLine) let lineNum = fromIntegral $ lineNo (NE.head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
else fileLines ! fromIntegral lineNum else fileLines ! fromIntegral lineNum
@ -139,7 +139,7 @@ outputForFile color sys comments = do
putStrLn (color "source" line) putStrLn (color "source" line)
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
putStrLn "" putStrLn ""
showFixedString color commentsForLine (fromIntegral lineNum) fileLines showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines
-- Pick out only the lines necessary to show a fix in action -- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)

View file

@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2024 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@ -21,14 +21,14 @@
module ShellCheck.Interface module ShellCheck.Interface
( (
SystemInterface(..) SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks) , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks)
, CheckResult(crFilename, crComments) , CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot) , ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks) , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
, AnalysisResult(arComments) , AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount) , FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash) , Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
, ExecutionMode(Executed, Sourced) , ExecutionMode(Executed, Sourced)
, ErrorMessage , ErrorMessage
, Code , Code
@ -39,11 +39,12 @@ module ShellCheck.Interface
, ColorOption(ColorAuto, ColorAlways, ColorNever) , ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix) , TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult , emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult , newAnalysisResult
, newAnalysisSpec
, newFormatterOptions , newFormatterOptions
, newParseResult
, newPosition , newPosition
, newSystemInterface
, newTokenComment , newTokenComment
, mockedSystemInterface , mockedSystemInterface
, mockRcFile , mockRcFile
@ -99,6 +100,7 @@ data CheckSpec = CheckSpec {
csIncludedWarnings :: Maybe [Integer], csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell, csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity, csMinSeverity :: Severity,
csExtendedAnalysis :: Maybe Bool,
csOptionalChecks :: [String] csOptionalChecks :: [String]
} deriving (Show, Eq) } deriving (Show, Eq)
@ -123,6 +125,7 @@ emptyCheckSpec = CheckSpec {
csIncludedWarnings = Nothing, csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing, csShellTypeOverride = Nothing,
csMinSeverity = StyleC, csMinSeverity = StyleC,
csExtendedAnalysis = Nothing,
csOptionalChecks = [] csOptionalChecks = []
} }
@ -135,6 +138,14 @@ newParseSpec = ParseSpec {
psShellTypeOverride = Nothing psShellTypeOverride = Nothing
} }
newSystemInterface :: Monad m => SystemInterface m
newSystemInterface =
SystemInterface {
siReadFile = \_ _ -> return $ Left "Not implemented",
siFindSource = \_ _ _ name -> return name,
siGetConfig = \_ -> return Nothing
}
-- Parser input and output -- Parser input and output
data ParseSpec = ParseSpec { data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
@ -165,6 +176,7 @@ data AnalysisSpec = AnalysisSpec {
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool, asCheckSourced :: Bool,
asOptionalChecks :: [String], asOptionalChecks :: [String],
asExtendedAnalysis :: Maybe Bool,
asTokenPositions :: Map.Map Id (Position, Position) asTokenPositions :: Map.Map Id (Position, Position)
} }
@ -175,6 +187,7 @@ newAnalysisSpec token = AnalysisSpec {
asExecutionMode = Executed, asExecutionMode = Executed,
asCheckSourced = False, asCheckSourced = False,
asOptionalChecks = [], asOptionalChecks = [],
asExtendedAnalysis = Nothing,
asTokenPositions = Map.empty asTokenPositions = Map.empty
} }
@ -212,7 +225,7 @@ newCheckDescription = CheckDescription {
} }
-- Supporting data types -- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq) data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
@ -311,7 +324,7 @@ data ColorOption =
-- For testing -- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface { mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
siReadFile = rf, siReadFile = rf,
siFindSource = fs, siFindSource = fs,
siGetConfig = const $ return Nothing siGetConfig = const $ return Nothing
@ -326,4 +339,3 @@ mockedSystemInterface files = SystemInterface {
mockRcFile rcfile mock = mock { mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile) siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
} }

View file

@ -46,6 +46,7 @@ import Text.Parsec.Error
import Text.Parsec.Pos import Text.Parsec.Pos
import qualified Control.Monad.Reader as Mr import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms import qualified Control.Monad.State as Ms
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (quickCheckAll) import Test.QuickCheck.All (quickCheckAll)
@ -140,15 +141,9 @@ carriageReturn = do
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
return '\r' return '\r'
almostSpace = almostSpace = do
choice [ parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it."
check '\xA0' "unicode non-breaking space", oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F"
check '\x200B' "unicode zerowidth space"
]
where
check c name = do
parseNote ErrorC 1018 $ "This is a " ++ name ++ ". Delete and retype it."
char c
return ' ' return ' '
--------- Message/position annotation on top of user state --------- Message/position annotation on top of user state
@ -160,7 +155,7 @@ data Context =
deriving (Show) deriving (Show)
data HereDocContext = data HereDocContext =
HereDocPending Token [Context] -- on linefeed, read this T_HereDoc HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc
deriving (Show) deriving (Show)
data UserState = UserState { data UserState = UserState {
@ -238,12 +233,12 @@ addToHereDocMap id list = do
hereDocMap = Map.insert id list map hereDocMap = Map.insert id list map
} }
addPendingHereDoc t = do addPendingHereDoc id d q str = do
state <- getState state <- getState
context <- getCurrentContexts context <- getCurrentContexts
let docs = pendingHereDocs state let docs = pendingHereDocs state
putState $ state { putState $ state {
pendingHereDocs = HereDocPending t context : docs pendingHereDocs = HereDocPending id d q str context : docs
} }
popPendingHereDocs = do popPendingHereDocs = do
@ -826,7 +821,7 @@ readArithmeticContents =
char ')' char ')'
id <- endSpan start id <- endSpan start
spacing spacing
return $ TA_Parentesis id s return $ TA_Parenthesis id s
readArithTerm = readGroup <|> readVariable <|> readExpansion readArithTerm = readGroup <|> readVariable <|> readExpansion
@ -1057,6 +1052,16 @@ readAnnotationWithoutPrefix sandboxed = do
"This shell type is unknown. Use e.g. sh or bash." "This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell] return [ShellOverride shell]
"extended-analysis" -> do
pos <- getPosition
value <- plainOrQuoted $ many1 letter
case value of
"true" -> return [ExtendedAnalysis True]
"false" -> return [ExtendedAnalysis False]
_ -> do
parseNoteAt pos ErrorC 1146 "Unknown extended-analysis value. Expected true/false."
return []
"external-sources" -> do "external-sources" -> do
pos <- getPosition pos <- getPosition
value <- plainOrQuoted $ many1 letter value <- plainOrQuoted $ many1 letter
@ -1194,7 +1199,7 @@ readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
readDollarBracedLiteral = do readDollarBracedLiteral = do
start <- startSpan start <- startSpan
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable
id <- endSpan start id <- endSpan start
return $ T_Literal id $ concat vars return $ T_Literal id $ concat vars
@ -1556,7 +1561,7 @@ readGenericLiteral endChars = do
return $ concat strings return $ concat strings
readGenericLiteral1 endExp = do readGenericLiteral1 endExp = do
strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp
return $ concat strings return $ concat strings
readGenericEscaped = do readGenericEscaped = do
@ -1835,7 +1840,7 @@ readHereDoc = called "here document" $ do
-- add empty tokens for now, read the rest in readPendingHereDocs -- add empty tokens for now, read the rest in readPendingHereDocs
let doc = T_HereDoc hid dashed quoted endToken [] let doc = T_HereDoc hid dashed quoted endToken []
addPendingHereDoc doc addPendingHereDoc hid dashed quoted endToken
return doc return doc
where where
unquote :: String -> (Quoted, String) unquote :: String -> (Quoted, String)
@ -1856,7 +1861,7 @@ readPendingHereDocs = do
docs <- popPendingHereDocs docs <- popPendingHereDocs
mapM_ readDoc docs mapM_ readDoc docs
where where
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) = readDoc (HereDocPending id dashed quoted endToken ctx) =
swapContext ctx $ swapContext ctx $
do do
docStartPos <- getPosition docStartPos <- getPosition
@ -2283,22 +2288,31 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
subRead name script = subRead name script =
withContext (ContextSource name) $ withContext (ContextSource name) $
inSeparateContext $ inSeparateContext $ do
subParse (initialPos name) (readScriptFile True) script oldState <- getState
setState $ oldState { pendingHereDocs = [] }
result <- subParse (initialPos name) (readScriptFile True) script
newState <- getState
setState $ newState { pendingHereDocs = pendingHereDocs oldState }
return result
readSource t = return t readSource t = return t
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu" prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu" prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat" prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat"
prop_readPipeline4 = isOk readPipeline "! ! true"
prop_readPipeline5 = isOk readPipeline "true | ! true"
readPipeline = do readPipeline = do
unexpecting "keyword/token" readKeyword unexpecting "keyword/token" readKeyword
do readBanged readPipeSequence
readBanged parser = do
pos <- getPosition
(T_Bang id) <- g_Bang (T_Bang id) <- g_Bang
pipe <- readPipeSequence next <- readBanged parser
return $ T_Banged id pipe return $ T_Banged id next
<|> <|> parser
readPipeSequence
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1" prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo" prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
@ -2354,14 +2368,14 @@ readTerm = do
readPipeSequence = do readPipeSequence = do
start <- startSpan start <- startSpan
(cmds, pipes) <- sepBy1WithSeparators readCommand (cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand)
(readPipe `thenSkip` (spacing >> readLineBreak)) (readPipe `thenSkip` (spacing >> readLineBreak))
id <- endSpan start id <- endSpan start
spacing spacing
return $ T_Pipeline id pipes cmds return $ T_Pipeline id pipes cmds
where where
sepBy1WithSeparators p s = do sepBy1WithSeparators p s = do
let elems = p >>= \x -> return ([x], []) let elems = (\x -> ([x], [])) <$> p
let seps = do let seps = do
separator <- s separator <- s
return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator]) return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
@ -2384,6 +2398,10 @@ readCommand = choice [
] ]
readCmdName = do readCmdName = do
-- If the command name is `!` then
optional . lookAhead . try $ do
char '!'
whitespace
-- Ignore alias suppression -- Ignore alias suppression
optional . try $ do optional . try $ do
char '\\' char '\\'
@ -2777,17 +2795,29 @@ readFunctionDefinition = called "function" $ do
prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }" prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }"
prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }"
prop_readCoProc3 = isOk readCoProc "coproc echo bar" prop_readCoProc3 = isOk readCoProc "coproc echo bar"
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 readCoProc = called "coproc" $ do
start <- startSpan start <- startSpan
try $ do try $ do
string "coproc" string "coproc"
whitespace spacing1
choice [ try $ readCompoundCoProc start, readSimpleCoProc start ] choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]
where where
readCompoundCoProc start = do readCompoundCoProc start = do
var <- optionMaybe $ notFollowedBy2 readAssignmentWord
readVariableName `thenSkip` whitespace (var, body) <- choice [
try $ do
body <- readBody readCompoundCommand body <- readBody readCompoundCommand
return (Nothing, body),
try $ do
var <- readNormalWord `thenSkip` spacing
body <- readBody readCompoundCommand
return (Just var, body)
]
id <- endSpan start id <- endSpan start
return $ T_CoProc id var body return $ T_CoProc id var body
readSimpleCoProc start = do readSimpleCoProc start = do
@ -2891,8 +2921,8 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos) kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p = kludgeAwayQuotes s p =
case s of case s of
first:rest@(_:_) -> first:second:rest ->
let (last:backwards) = reverse rest let (last NE.:| backwards) = NE.reverse (second NE.:| rest)
middle = reverse backwards middle = reverse backwards
in in
if first `elem` "'\"" && first == last if first `elem` "'\"" && first == last
@ -3322,10 +3352,12 @@ readScriptFile sourced = do
then do then do
commands <- readCompoundListOrEmpty commands <- readCompoundListOrEmpty
id <- endSpan start id <- endSpan start
readPendingHereDocs
verifyEof verifyEof
let script = T_Annotation annotationId annotations $ let script = T_Annotation annotationId annotations $
T_Script id shebang commands T_Script id shebang commands
reparseIndices script userstate <- getState
reparseIndices $ reattachHereDocs script (hereDocMap userstate)
else do else do
many anyChar many anyChar
id <- endSpan start id <- endSpan start
@ -3335,8 +3367,8 @@ readScriptFile sourced = do
verifyShebang pos s = do verifyShebang pos s = do
case isValidShell s of case isValidShell s of
Just True -> return () Just True -> return ()
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!" Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' scripts. Sorry!"
Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify." Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to specify."
isValidShell s = isValidShell s =
let good = null s || any (`isPrefixOf` s) goodShells let good = null s || any (`isPrefixOf` s) goodShells
@ -3352,16 +3384,20 @@ readScriptFile sourced = do
"sh", "sh",
"ash", "ash",
"dash", "dash",
"busybox sh",
"bash", "bash",
"bats", "bats",
"ksh" "ksh",
"oksh"
] ]
badShells = [ badShells = [
"awk", "awk",
"csh", "csh",
"expect", "expect",
"fish",
"perl", "perl",
"python", "python",
"python3",
"ruby", "ruby",
"tcsh", "tcsh",
"zsh" "zsh"
@ -3414,13 +3450,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 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 isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse
parsesCleanly parser string = runIdentity $ do -- If the parser matches the string, return Right [ParseNotes+ParseProblems]
(res, sys) <- runParser testEnvironment -- If it does not match the string, return Left [ParseProblems]
getParseOutput parser string = runIdentity $ do
(res, systemState) <- runParser testEnvironment
(parser >> eof >> getState) "-" string (parser >> eof >> getState) "-" string
case (res, sys) of return $ case res of
(Right userState, systemState) -> Right userState ->
return $ Just . null $ parseNotes userState ++ parseProblems systemState Right $ parseNotes userState ++ parseProblems systemState
(Left _, _) -> return Nothing 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 parseWithNotes parser = do
item <- parser item <- parser
@ -3438,8 +3483,7 @@ makeErrorFor parsecError =
pos = errorPos parsecError pos = errorPos parsecError
getStringFromParsec errors = getStringFromParsec errors =
case map f errors of headOrDefault "" (mapMaybe f $ reverse errors) ++
r -> unwords (take 1 $ catMaybes $ reverse r) ++
" Fix any mentioned problems and try again." " Fix any mentioned problems and try again."
where where
f err = f err =
@ -3471,8 +3515,7 @@ parseShell env name contents = do
return newParseResult { return newParseResult {
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state, prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
prTokenPositions = Map.map startEndPosToPos (positionMap userstate), prTokenPositions = Map.map startEndPosToPos (positionMap userstate),
prRoot = Just $ prRoot = Just script
reattachHereDocs script (hereDocMap userstate)
} }
Left err -> do Left err -> do
let context = contextStack state let context = contextStack state
@ -3490,13 +3533,11 @@ parseShell env name contents = do
-- A final pass for ignoring parse errors after failed parsing -- A final pass for ignoring parse errors after failed parsing
isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
notesForContext list = zipWith ($) [first, second] $ filter isName list notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list]
where where
isName (ContextName _ _) = True first (pos, str) = ParseNote pos pos ErrorC 1073 $
isName _ = False
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
"Couldn't parse this " ++ str ++ ". Fix to allow more checks." "Couldn't parse this " ++ str ++ ". Fix to allow more checks."
second (ContextName pos str) = ParseNote pos pos InfoC 1009 $ second (pos, str) = ParseNote pos pos InfoC 1009 $
"The mentioned syntax error was in this " ++ str ++ "." "The mentioned syntax error was in this " ++ str ++ "."
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text -- Go over all T_UnparsedIndex and reparse them as either arithmetic or text

View file

@ -12,6 +12,17 @@ then
fail "There are uncommitted changes" fail "There are uncommitted changes"
fi 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) current=$(git tag --points-at)
if [[ -z "$current" ]] if [[ -z "$current" ]]
then then
@ -34,33 +45,30 @@ then
fail "You are not on master" fail "You are not on master"
fi 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 "* ]] if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then then
fail "Expected git log message to be 'Stable version ...'" fail "Expected git log message to be 'Stable version ...'"
fi fi
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
then
fail "Expected git log message to contain CHANGELOG"
fi
i=1 j=1 i=1 j=1
cat << EOF cat << EOF
Manual Checklist Manual Checklist
$((i++)). Make sure none of the automated checks above failed $((i++)). Make sure none of the automated checks above failed
$((i++)). Run \`build/build_builder build/*/\` to update all builder images.
$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` 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 GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman $((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Format and read over the manual for bad formatting and outdated info. $((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds. $((i++)). Make sure the Hackage package builds locally.
Release Steps Release Steps

View file

@ -17,13 +17,13 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place, Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue. 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 EOF
exit 0 exit 0
} }
echo "Deleting 'dist' and 'dist-newstyle'..." echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
rm -rf dist dist-newstyle rm -rf dist dist-newstyle .stack-work
execs=$(find . -name shellcheck) execs=$(find . -name shellcheck)
@ -74,13 +74,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 archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Ubuntu LTS # 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:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install ubuntu:20.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:16.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS # 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 EOF
exit "$final" exit "$final"

View file

@ -18,21 +18,24 @@ import qualified ShellCheck.Parser
main = do main = do
putStrLn "Running ShellCheck tests..." putStrLn "Running ShellCheck tests..."
results <- sequence [ failures <- filter (not . snd) <$> mapM sequenceA tests
ShellCheck.Analytics.runTests if null failures then exitSuccess else do
,ShellCheck.AnalyzerLib.runTests putStrLn "Tests failed for the following module(s):"
,ShellCheck.ASTLib.runTests mapM (putStrLn . ("- ShellCheck." ++) . fst) failures
,ShellCheck.CFG.runTests exitFailure
,ShellCheck.CFGAnalysis.runTests where
,ShellCheck.Checker.runTests tests =
,ShellCheck.Checks.Commands.runTests [ ("Analytics" , ShellCheck.Analytics.runTests)
,ShellCheck.Checks.ControlFlow.runTests , ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests)
,ShellCheck.Checks.Custom.runTests , ("ASTLib" , ShellCheck.ASTLib.runTests)
,ShellCheck.Checks.ShellSupport.runTests , ("CFG" , ShellCheck.CFG.runTests)
,ShellCheck.Fixer.runTests , ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests)
,ShellCheck.Formatter.Diff.runTests , ("Checker" , ShellCheck.Checker.runTests)
,ShellCheck.Parser.runTests , ("Checks.Commands" , ShellCheck.Checks.Commands.runTests)
, ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests)
, ("Checks.Custom" , ShellCheck.Checks.Custom.runTests)
, ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests)
, ("Fixer" , ShellCheck.Fixer.runTests)
, ("Formatter.Diff" , ShellCheck.Formatter.Diff.runTests)
, ("Parser" , ShellCheck.Parser.runTests)
] ]
if and results
then exitSuccess
else exitFailure

View file

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