Compare commits

...

723 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
Vidar Holen
a526ee0829 Stable version 0.9.0
This release is dedicated to Mindustry: the most fun you can
have with open source (outside of shell scripting of course).
2022-12-12 21:49:19 -08:00
Vidar Holen
8c5fdc3522 Update copyright years 2022-12-12 21:49:19 -08:00
Vidar Holen
ae199edb68 Let distrotest fail fast when there remaining executables 2022-12-11 20:51:39 -08:00
Vidar Holen
7cfcf6db8a Fix stack build 2022-12-11 19:31:58 -08:00
Vidar Holen
a7c5be93dc Tighten bounds on packages 2022-12-11 19:22:21 -08:00
Vidar Holen
8754c21244 Avoid $ trigger TH 2022-12-11 19:22:06 -08:00
Vidar Holen
985ca2530d Add Docker testing for older and newer Ubuntu versions 2022-12-11 19:22:06 -08:00
Vidar Holen
3cae6cd6ab Allow building on deepseq < 1.4.2.0 2022-12-11 15:06:24 -08:00
Vidar Holen
74b1745a19 Fix compiler error on some GHC versions
Fixes the following error:

    src/ShellCheck/CFGAnalysis.hs:1394:40: error:
	* Couldn't match expected type `[S.Set a]'
		      with actual type `M.Map String FunctionValue'
	* In the second argument of `($)', namely
	    `mapStorage $ sFunctionTargets state'
	  In the expression: S.unions $ mapStorage $ sFunctionTargets state
	  In an equation for `declaredFuncs':
	      declaredFuncs = S.unions $ mapStorage $ sFunctionTargets state
	* Relevant bindings include
	    declaredFuncs :: S.Set a
	      (bound at src/ShellCheck/CFGAnalysis.hs:1394:13)
2022-12-11 15:06:24 -08:00
Vidar Holen
495e34d101 Add missing Semigroup import for older GHC 2022-12-11 14:19:24 -08:00
Vidar Holen
2a16a4e8c1 Add missing imports for later GHC versions 2022-12-10 15:17:08 -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
Vidar Holen
0786b2bf3c
Merge pull request #2601 from mavit/colon-after-exec
Permit colon after exec
2022-11-02 21:18:56 -07:00
Vidar Holen
84d8530f14 Add SVG logo 2022-10-29 12:50:37 -07:00
Vidar Holen
86e2b76730 Improve SC1059 error message 2022-10-29 12:50:26 -07:00
Vidar Holen
b770984dfc Try to parse the inside of traps (fixes #2584) 2022-10-13 21:04:38 -07:00
Vidar Holen
d9c9e60fb0 Allow arbitrary bats @test names (fixes #2587) 2022-10-13 20:21:59 -07:00
Vidar Holen
14056a7f3a Don't suggest pgrep for ps -p .. | grep (fixes #2597) 2022-10-12 20:22:02 -07:00
Vidar Holen
a524929b69 Remove outdated test 2022-10-12 20:22:02 -07:00
Vidar Holen
fa7943ac0e Revert "Add employer mandated disclaimer"
This reverts commit 5202072a34.
2022-10-11 20:10:34 -07:00
Vidar Holen
81c2ecaccb Remove true/false from SC2216/SC2217 (fixes #2603) 2022-10-11 19:40:29 -07:00
Vidar Holen
fcba462a99
Merge pull request #2602 from DoxasticFox/issue-2550
Fix false positive for SC2312 when using `time`
2022-10-09 11:55:39 -07:00
Christian Nassif-Haynes
43aca62ca7 Fix false positive for SC2312 when using time 2022-10-10 03:33:38 +11:00
Peter Oliver
128351f5ef Permit colon after exec
ShellCheck throws warning SC2093 when a script contains commands that could never be executed because they are after an `exec`.  Command `:` does nothing, so add it to the list of commands that don’t trigger this warning.
2022-10-07 17:02:31 +01:00
Vidar Holen
d71d6ff294
Merge pull request #2581 from larryv/updatebashvars
Support more Bash internal variables
2022-09-25 11:24:45 -07:00
Vidar Holen
bd65b67578
Merge pull request #2586 from DoxasticFox/issue-2537
Suppress SC2311 with `set -o posix`
2022-09-25 11:04:30 -07:00
Vidar Holen
149b4dbd6f
Merge pull request #2588 from DoxasticFox/issue-2563
Add `mapfile` to harmless commands for SC2094
2022-09-25 11:03:45 -07:00
Christian Nassif-Haynes
ef5f9a7af5 Add mapfile to harmless commands for SC2094 2022-09-25 03:04:20 +10:00
Christian Nassif-Haynes
581981ba76 Suppress SC2311 with set -o posix 2022-09-24 07:20:48 +10:00
Vidar Holen
fcc473e27f Include inherited env for DFA of leftover functions (fixes #2560) 2022-09-21 18:11:18 -07:00
Lawrence Velázquez
0845b81183
Add READLINE_POINT to list of variables without spaces 2022-09-20 20:39:17 -04:00
Lawrence Velázquez
966fb3e3dd
Recognize more Bash internal variables
- BASH_ARGV0, introduced in Bash 5.0
  - BASH_COMPAT, 4.3
  - BASH_LOADABLES_PATH, 4.4
  - CHILD_MAX, 4.3
  - EPOCHREALTIME, 5.0
  - EPOCHSECONDS, 5.0
  - EXECIGNORE, 4.4
  - INSIDE_EMACS, 4.4
  - PS0, 4.4
  - READLINE_ARGUMENT, 5.2
  - READLINE_MARK, 5.1
  - SRANDOM, 5.1

Fixes #1780 and #2554.
2022-09-20 20:39:10 -04:00
Lawrence Velázquez
f28462b01c
Remove duplicate "COPROC" from internal vars list 2022-09-20 20:25:21 -04:00
Lawrence Velázquez
ccab132b38
Reflow lists of internal shell variables
No functional changes; this just makes the next few commits cleaner.
2022-09-20 20:24:37 -04:00
Vidar Holen
4806719035 Handle variable assignments from read in CFG 2022-08-02 15:47:59 -07:00
Vidar Holen
0df9345142 Trace numerical status, use for SC2071 (ref #2541) 2022-08-02 11:29:56 -07:00
Vidar Holen
77069f7445 Store postdominators as Array Node [Node] for a significant win 2022-07-31 15:43:24 -07:00
Vidar Holen
04db46381f Use Data.Map.Strict instead for a ~15% parsing speedup 2022-07-29 09:02:45 -07:00
Vidar Holen
c76b8d9a32 Let annotations take effect earlier (fixes #2534) 2022-07-28 11:05:16 -07:00
Vidar Holen
d0dd81e1fa Allow quoting values in directives (fixes #2517) 2022-07-28 08:56:44 -07:00
Vidar Holen
f440912279 Refactor to not generate Parameters twice 2022-07-28 08:26:56 -07:00
Vidar Holen
3ce310e939 Plug space leaks when processing multiple files 2022-07-27 14:42:57 -07:00
Vidar Holen
a30ac402eb Don't use & for updates as result is unspecified
This fixes `Prelude.foldl1: empty list []` when script has `( exit )`
2022-07-27 11:30:26 -07:00
Vidar Holen
4a27c9a8d5 Fix overlap check 2022-07-26 15:33:25 -07:00
Vidar Holen
b5f5e6347d Discard next rather than existing fixes when they overlap 2022-07-26 13:41:22 -07:00
Vidar Holen
c57e447c89 Correctly discard overlapping fixes in diff output (fixes #2370) 2022-07-26 10:16:12 -07:00
Vidar Holen
e9784fa9a7 Refine #2544 to not warn when $? postdominates [ ] (fixes #2544) 2022-07-25 12:00:59 -07:00
Vidar Holen
f1148b8b41 Include postdominators in CFGResult 2022-07-25 12:00:53 -07:00
Vidar Holen
982681fc05 Add unit test to ensure SC2321 does not trigger on associative arrays 2022-07-24 14:30:31 -07:00
Vidar Holen
52dac51cd4 SC2323: Warn about redundant parens in a[(x+1)] and $(( ((x)) )) (ref: #1666) 2022-07-24 14:18:38 -07:00
Vidar Holen
30bb0e0093 SC2321: Warn about redundant $(()) in arr[$((i))]=x (ref: #1666) 2022-07-24 14:18:38 -07:00
Vidar Holen
d1d574c091 Merge branch 'ygeyzel-grammer_fix_sc2183' 2022-07-23 15:39:26 -07:00
Vidar Holen
ea4e0091c7 Additionally pluralize 'arguments' in SC2183 2022-07-23 15:38:42 -07:00
Vidar Holen
81d9f7e640 Merge branch 'grammer_fix_sc2183' of https://github.com/ygeyzel/shellcheck into ygeyzel-grammer_fix_sc2183 2022-07-23 15:34:22 -07:00
Vidar Holen
69469c3603
Merge pull request #2543 from ygeyzel/SC2028-escape-chars
Add escape characters to SC2028: \a, \b, \e, \f, \v, \\, \', \OOO, \xHH
2022-07-23 15:30:00 -07:00
Vidar Holen
5cf6e01ce9 Warn when $? refers to echo or condition (ref #2541) 2022-07-23 09:39:26 -07:00
ygeyzel
f7857028f7 Add escape characters to SC2028: \a, \b, \e, \f, \v, \\, \', \OOO, \xHH 2022-07-23 19:28:37 +03:00
Vidar Holen
b261ec24f9 Include exit codes in DFA (ref #2541) 2022-07-23 08:50:19 -07:00
Vidar Holen
819470fa1d Omit SC3021 about >& file unless definitely non-numeric (fixes #2520) 2022-07-22 17:06:24 -07:00
Vidar Holen
2f28847b08 Normalize spaces around = in unit tests 2022-07-22 16:35:14 -07:00
Vidar Holen
e47480e93a Also emit SC2004 for array indices (fixes #1666) 2022-07-22 16:29:52 -07:00
Vidar Holen
9caeec104b SC2318: Warn about backreferencing in declare x=1 y=$x (fixes #1653) 2022-07-22 12:40:59 -07:00
Vidar Holen
95b3cbf071 Qualify Data.Map as M instead of tedious Map 2022-07-22 11:11:09 -07:00
Vidar Holen
e7f05d662a In addition to start/end, track sets of nodes belonging to tokens 2022-07-22 10:29:19 -07:00
Vidar Holen
3ee4419ef4 Suppress SC2086 for variables declared -i (ref #2541) 2022-07-22 08:53:27 -07:00
Vidar Holen
8dc0fdb4cc Precompile new fgl dependency on armv6hf 2022-07-20 12:52:57 -07:00
Vidar Holen
da4885a71d Use DFA for SC2086 2022-07-20 08:08:44 -07:00
Vidar Holen
642ad86125 Add SC2317 warning about unreachable commands 2022-07-20 08:08:41 -07:00
Vidar Holen
f77a545282 Control Flow Graph / Data Flow Analysis support 2022-07-20 08:08:24 -07:00
Vidar Holen
7946bf5657 Upgrade cURL for Windows build image 2022-07-20 08:08:24 -07:00
Vidar Holen
cc04b40119 Freeze macOS dependency by sha256 2022-07-20 08:08:24 -07:00
Vidar Holen
c3bce51de3 Allow text to build on Fedora by installing dependencies 2022-07-20 08:08:24 -07:00
Vidar Holen
a4042f7523 Parse &&/|| as left-associative 2022-07-20 08:08:24 -07:00
Vidar Holen
363c0633e0 When reparsing array indices, do it recursively 2022-07-20 08:08:24 -07:00
ygeyzel
7ceb1f1519 SC2183 grammer fix: 'variable' instead of 'variables' if only one variable 2022-07-17 21:46:42 +03:00
Vidar Holen
f1bdda54cb
Merge pull request #2500 from Fdawgs/patch-1
ci: update github actions
2022-05-20 20:36:36 -07:00
Frazer Smith
9aa4c22aa6
ci: update github actions 2022-05-16 06:56:46 +00:00
Vidar Holen
399c04cc17 Mention SC2316 in changelog 2022-05-06 10:11:52 -07:00
Vidar Holen
fd595d1058 Only trigger SC2316 on unquoted words. 2022-05-06 10:06:12 -07:00
Vidar Holen
7c44e1060f Merge branch 'patrickxia-master' 2022-05-06 09:50:08 -07:00
Rune Juhl Jacobsen
2821552688 Fix bug in 2126 when using after/before flags with grep
Using `--after-context`/`-A` or `--before-context`/`-B` would give a warning
recommending the user to use `grep -c`, even though that would give a different
result than using `grep | wc -l`:

```fundamental
$ echo -e "1\n2\n3" | grep -cA 3 1
1
$ echo -e "1\n2\n3" | grep -A 3 1 | wc -l
3
```
2022-05-06 09:17:23 -07:00
Vidar Holen
2034e3886e
Merge pull request #2414 from runejuhl/fix-grep-after-before
Fix bug in 2126 when using after/before flags with grep
2022-05-06 09:15:04 -07:00
Patrick Xia
fa15c0a454 add SC2316: error on multiple declarations like 'readonly local' 2022-05-05 16:19:07 -07:00
Vidar Holen
88cdb4e2c9 Warn about spaces around = in alias (fixes #2442) 2022-02-03 19:23:46 -08:00
Vidar Holen
2292e852e5 Switch linux-x86_64 build from Ubuntu to Alpine for musl 2022-01-23 14:23:56 -08:00
Vidar Holen
ade2bf7b87 Allow parsing [[ x = ["$y"] ]] (fixes #2165) 2022-01-09 16:50:50 -08:00
Vidar Holen
e6e558946c Improve decoding of single quoted literals (fixes #2418) 2021-12-21 14:30:39 -08:00
Rune Juhl Jacobsen
3a118246ef
Fix bug in 2126 when using after/before flags with grep
Using `--after-context`/`-A` or `--before-context`/`-B` would give a warning
recommending the user to use `grep -c`, even though that would give a different
result than using `grep | wc -l`:

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

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

```
003a:err:winediag:SECUR32_initNTLMSP ntlm_auth was not found or is outdated. Make sure that ntlm_auth >= 3.0.25 is in your path. Usually, you can find it in the winbind package of your distribution.
```
2021-03-06 14:19:08 +02:00
Vidar Holen
670c1de01b
Merge pull request #2145 from avdv/fix-2143
Allow `env` to have flags and variables in shebang
2021-02-22 21:00:04 -08:00
Vidar Holen
b9b6975bfa Brand New Build!
Features Linux x86_64 docker builds for all archs
2021-02-22 19:12:57 -08:00
Moritz Röhrich
d6bb8fc0d8 Error on backslash in comment #2132
- Report error in case of a backspace in a comment

Backspaces in comments are no good. In most cases they are the result of
commenting out a longer line, that was broken down. This usually results
in the shell treating the following lines as their own commands on their
own lines instead of as parts of the longer, broken down line.
2021-02-14 19:13:29 +01:00
Claudio Bley
8bb5e01401 Allow env to have flags and variables in shebang
The `env` command has a `-S,--split-string` option which enables
having arguments for the command in a shebang.

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

Fixes #2143.
2021-02-12 10:53:54 +01:00
Austin English
2e59eba6eb add support for /bin/busybox sh shebang 2021-02-05 19:56:44 -06:00
Vidar Holen
15ff87cf80
Merge pull request #2119 from josephcsible/refactors
Various refactorings
2021-02-02 18:14:27 -08:00
Kir Kolyshkin
99e9d5c54b Whitelist podman for SC2016 about '$var'
Same as 08d2eef411 but for podman.

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

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

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

Fixes #1643.

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

This reverts commit 64c31d9142.
2020-04-24 22:14:08 -04:00
Vidar Holen
75863a887e
Merge pull request #1918 from josephcsible/getsuspiciousregexwildcard
Clean up and optimize getSuspiciousRegexWildcard
2020-04-12 15:32:00 -07:00
Vidar Holen
413f0048b8
Merge pull request #1917 from josephcsible/thenskip
Simplify thenSkip, and use in another location
2020-04-12 15:26:36 -07:00
Vidar Holen
e7b5fb9742
Merge pull request #1907 from josephcsible/formatters
Clean up formatters
2020-04-12 15:23:17 -07:00
Vidar Holen
30523555af
Merge pull request #1906 from josephcsible/shellsupport
Simplify ShellSupport
2020-04-12 15:22:52 -07:00
Vidar Holen
58d3e50f43
Merge pull request #1905 from josephcsible/skiprepeating
Make skipRepeating lazier and faster
2020-04-12 15:21:39 -07:00
Vidar Holen
73cc11fd0a
Merge pull request #1901 from josephcsible/bracedstring
Mostly get rid of bracedString
2020-04-12 15:14:50 -07:00
Joseph C. Sible
163c710ba7 Clean up and optimize getSuspiciousRegexWildcard 2020-04-12 16:15:45 -04:00
Vidar Holen
ab1610b004
Merge pull request #1903 from josephcsible/fixer
Only perform the comparisons once
2020-04-11 17:23:51 -07:00
Vidar Holen
148468be70
Merge pull request #1904 from josephcsible/commands
Simplify Commands
2020-04-11 17:23:39 -07:00
Vidar Holen
5eac721fcf
Merge pull request #1902 from josephcsible/astlib
Clean up ASTLib
2020-04-11 17:23:08 -07:00
Joseph C. Sible
b58bb4ba9d Move bracedString to be local to its last use site 2020-04-11 19:24:11 -04:00
Joseph C. Sible
999b7e2596 Get rid of bracedString everywhere it's easy to 2020-04-11 19:24:11 -04:00
Joseph C. Sible
a9d564a8bc Combine bracedString into getSingleUnmodifiedVariable
Everywhere we used getSingleUnmodifiedVariable, we just called bracedString on
the result. Move this into that function instead, and rename it accordingly.
2020-04-11 19:23:13 -04:00
Joseph C. Sible
8a7497c4f0 Simplify checkVariableBraces 2020-04-11 19:23:13 -04:00
Vidar Holen
1eac0d7340
Merge pull request #1900 from josephcsible/analyzerlib
Clean up AnalyzerLib
2020-04-11 16:21:20 -07:00
Vidar Holen
f8c1ffb0dc
Merge pull request #1898 from josephcsible/nameexpansion
Simplify nameExpansion
2020-04-11 16:18:54 -07:00
Joseph C. Sible
3e17a20965 Simplify thenSkip, and use in another location 2020-04-11 17:29:28 -04:00
Joseph C. Sible
1c6202dba4 Avoid some awkward parentheses with forM_ 2020-04-05 22:25:19 -04:00
Joseph C. Sible
64c31d9142 Use fromRight instead of reimplementing it 2020-04-05 22:23:22 -04:00
Joseph C. Sible
8a6679fd8a Remove unnecessary fromMaybe and when from bashism 2020-04-05 22:03:50 -04:00
Joseph C. Sible
facf0d1e27 Write getLiteralArgs with foldr and without fromMaybe or monads 2020-04-05 21:59:27 -04:00
Joseph C. Sible
cd38afce26 Make it slightly lazier still (and more clear) 2020-04-05 21:46:08 -04:00
Joseph C. Sible
5084ba8d7e Make skipRepeating lazier and faster 2020-04-05 21:39:14 -04:00
Joseph C. Sible
ed331b816b Simplify warnRedundant 2020-04-05 20:32:39 -04:00
Joseph C. Sible
cfa2a663af Simplify checkSetAssignment 2020-04-05 20:30:37 -04:00
Joseph C. Sible
df4928f4e3 Use MultiWayIf instead of case-matching on () 2020-04-05 20:30:37 -04:00
Joseph C. Sible
9747b1d5c3 Simplify checkArg 2020-04-05 20:10:56 -04:00
Joseph C. Sible
fa841cb270 Prefer pattern matching in undirected 2020-04-05 20:08:02 -04:00
Joseph C. Sible
e8501151dd Use a guard instead of unless 2020-04-05 20:04:54 -04:00
Joseph C. Sible
9027a9239f Use pattern matching instead of snd 2020-04-05 20:03:17 -04:00
Joseph C. Sible
773e98868d Use foldr in checkFindNameGlob 2020-04-05 19:53:40 -04:00
Joseph C. Sible
d45ab327b0 Only perform the comparisons once 2020-04-05 19:45:28 -04:00
Joseph C. Sible
0f9b0f18a4 Remove unnecessary cases from wordToPseudoGlob 2020-04-05 19:30:21 -04:00
Joseph C. Sible
322842b57e Remove unnecessary monadicity from wordToPseudoGlob 2020-04-05 19:29:40 -04:00
Joseph C. Sible
b6cff5ea0e Simplify getAssociativeArrays 2020-04-05 19:06:30 -04:00
Joseph C. Sible
8f105074fe Simplify getCommandNameAndToken 2020-04-05 19:01:56 -04:00
Joseph C. Sible
d22e0aa4a7 Simplify process
Note to self: This is a lot like foldr or traverse, and would be trivial to
implement as such if it didn't need to peek ahead when takesArg is true. I
wonder if there's a clean way to implement it in terms of one of them anyway.
2020-04-05 16:45:45 -04:00
Joseph C. Sible
fb55072302 Implement supportsArrays with pattern-matching 2020-04-05 16:30:59 -04:00
Joseph C. Sible
0cc5ed4563 Don't bother with asks if you're just immediately binding the result anyway 2020-04-05 16:25:43 -04:00
Joseph C. Sible
ca41440a67 Simplify getSpecial 2020-04-05 16:21:07 -04:00
Joseph C. Sible
1cf0aa25e9 Simplify dropPrefix 2020-04-05 16:19:18 -04:00
Joseph C. Sible
4604066c37 Use head instead of (!! 0) 2020-04-05 16:16:12 -04:00
Joseph C. Sible
2ebf522a52 Simplify isArrayFlag 2020-04-05 16:13:55 -04:00
Joseph C. Sible
e4eb2d157f Remove an unnecessary operator section 2020-04-05 16:13:55 -04:00
Joseph C. Sible
f109f9ab92 Remove unnecessary as-patterns 2020-04-05 16:13:55 -04:00
Joseph C. Sible
67e091674e Remove unnecessary maybeToList
The functions we use here are polymorphic enough to work in the [] monad,
so there's no point to use them in the Maybe monad and then convert.
2020-04-05 16:13:54 -04:00
Joseph C. Sible
f833ee3d5a Use a list comprehension instead of a concatMap with extra lists 2020-04-05 15:54:12 -04:00
Joseph C. Sible
f55d8c45e5 Simplify causesSubshell 2020-04-05 15:54:12 -04:00
Joseph C. Sible
14ee462ccd Use execState instead of reimplementing it 2020-04-05 15:50:42 -04:00
Joseph C. Sible
b3c04ce3d0 Implement findFirst in terms of foldr 2020-04-05 15:50:42 -04:00
Joseph C. Sible
b0dbc79f69 Remove unnecessary Maybe from isQuoteFreeElement 2020-04-05 15:07:36 -04:00
Joseph C. Sible
2a8170ba05 Use force instead of reimplementing it 2020-04-05 15:01:57 -04:00
Vidar Holen
01f4423465 Disable SC2257 about > $((i=42)) for Dash 2020-04-05 11:38:22 -07:00
Joseph C. Sible
d2fa88dd91 Simplify nameExpansion 2020-04-05 14:04:23 -04:00
Vidar Holen
a30e42ab05 Filter GitHub uploads by tag 2020-04-04 19:30:13 -07:00
Vidar Holen
84d6e53659 Update Changelog with new version 2020-04-04 19:29:49 -07:00
Vidar Holen
f7547c9a5a Stable version v0.7.1
This release is dedicated to the board game Pandemic, for teaching us
relevant survival skills like how to stay inside and play board games.
2020-04-04 18:38:39 -07:00
Vidar Holen
bd717c9d1b Don't warn about [ 0 -ne $FOO ] || [ 0 -ne $BAR ] (fixes #1891) 2020-04-01 22:09:00 -07:00
Vidar Holen
da0931740f
Merge pull request #1876 from fork-graveyard/master
recognize `: ${parameter=word}` as assignment
2020-04-01 18:52:53 -07:00
Vidar Holen
555f8a80dd
Merge pull request #1896 from ArturKlauser/travis-deploy-stage-fix
Run "deploy" step only for "Build" stages
2020-04-01 18:45:37 -07:00
Vidar Holen
a9c04e8a37
Merge pull request #1897 from ArturKlauser/use-shellcheck-on-yourself
Use shellcheck on yourself
2020-04-01 18:43:26 -07:00
Artur Klauser
9378227570
Use shellcheck on yourself
Fixing shellcheck warnings on shell scripts in this repo.
2020-04-01 09:03:38 +02:00
Artur Klauser
a128796c0c
Run "deploy" step only for "Build" stages 2020-04-01 07:52:16 +02:00
Vidar Holen
a0005bfa5a
Merge pull request #1885 from ArturKlauser/travis-pr-fix
Don't try to deploy docker images on PR runs
2020-03-31 19:12:14 -07:00
Vidar Holen
37a72d05ec
Merge pull request #1880 from josephcsible/patch-1
Mark that base >= 4.8.0.0 is required
2020-03-31 19:11:15 -07:00
Vidar Holen
c60323fb25
Merge pull request #1873 from josephcsible/checkwhilereadpitfalls
Simplify checkWhileReadPitfalls
2020-03-31 19:10:53 -07:00
Vidar Holen
db11e2f663
Merge pull request #1872 from josephcsible/checkforinquoted
Simplify checkForInQuoted
2020-03-31 19:10:25 -07:00
Vidar Holen
aac1d05a7e
Merge pull request #1893 from josephcsible/pattern-synonyms
Fix #1892: Use pattern synonyms to clean up AST
2020-03-31 18:13:22 -07:00
Vidar Holen
67f0dc4fd5 Update distro tests to support newer Cabal 2020-03-30 22:28:56 -07:00
Joseph C. Sible
8cf037fe5e Fix #1892: Use pattern synonyms to clean up AST 2020-03-28 18:38:07 -04:00
Artur Klauser
615063a9c3
Don't try to deploy docker images on PR runs
For security reasons, PR runs don't have access to Travis secrets. However,
Docker deployment depends on the secret DOCKER_PASSWORD. Thus we shouldn't try
Docker deployment when running PRs since it will fail for lack of access.
2020-03-28 09:27:17 +01:00
Vidar Holen
37e78141bd Stop deploying artifacts to GCS 2020-03-26 17:02:08 -07:00
Joseph C. Sible
9f833770b0
Mark that base >= 4.8.0.0 is required
We've actually already required base >= 4.8.0.0 since commit a8376a0 (in which we first used `<$>` without an import, which wasn't in the Prelude prior to this version). Since then, we've also made use of other more substantial features that de-facto require base >= 4.8.0.0 since they require GHC 7.10, such as `DeriveAnyClass`.
2020-03-19 21:31:10 -04:00
Vidar Holen
7963eeab9d Include shebang in AST traversal (fixes #1858) 2020-03-16 21:36:41 -07:00
girst
7a5e261d03 recognize : ${parameter=word} as assignment 2020-03-16 23:04:54 +01:00
Joseph C. Sible
9d5363377e Simplify checkWhileReadPitfalls
* Clean up usage of not
* Use a case match instead of sequence_ and a do block
2020-03-16 00:14:22 -04:00
Joseph C. Sible
86d470c74f Simplify checkForInQuoted
* Avoid some unnecessary fmaps
* Reuse an identical pattern-match for two guards
* Apply De Morgan's law
* Use forM_ to avoid an unnecessary where
2020-03-15 16:05:55 -04:00
Vidar Holen
acee69676b Try to make TravisCI not fail on deployment of Docker stage 2020-03-15 12:54:54 -07:00
Vidar Holen
a57f6d2886 Improve detection of for loops with single values 2020-03-15 11:30:56 -07:00
Vidar Holen
d28c8f883f
Merge pull request #1865 from josephcsible/patch-1
Use headOrDefault instead of fromMaybe and listToMaybe
2020-03-14 21:21:13 -07:00
Vidar Holen
c43b19f897 Make SC2095 (ssh in while read loops) more robust and suggest fixes 2020-03-14 21:15:47 -07:00
Joseph C. Sible
45a67e7c64
Use headOrDefault instead of fromMaybe and listToMaybe 2020-03-10 13:27:52 -04:00
Vidar Holen
68a03e05e5 Refer to GitHub rather than GCS for release builds 2020-03-08 17:41:46 -07:00
Vidar Holen
014a66f3f6 Fix TravisCI condition 2020-03-07 18:06:14 -08:00
Vidar Holen
fee13732a4
Merge pull request #1862 from austin987/sc2148-shell-directive
src/ShellCheck/Analytics.hs: suggest using a shell directive for SC2148
2020-03-07 17:43:44 -08:00
Austin English
741d499b3d src/ShellCheck/Analytics.hs: suggest using a shell directive for SC2148 2020-03-07 19:23:35 -06:00
Vidar Holen
9b66bc2f13 Upload to assets to GitHub 2020-03-07 17:13:01 -08:00
Luke Davis
00574dd1fc
Add conda install instructions 2020-03-03 13:23:24 -07:00
Vidar Holen
7b998239af SC2257: Warn when changing arithmetic variables in redirections 2020-02-17 18:16:57 -08:00
Vidar Holen
4c9210af79 Inspect 'alias' commands for referenced variables (Fixes #1832) 2020-02-17 14:20:21 -08:00
Vidar Holen
a75219e525 Remove unused instance Ord Replacement (fixes #1829) 2020-02-17 12:44:38 -08:00
Vidar Holen
99d6df8a08 Bump SC1102/SC1105 about ambiguous $(( to Error (fixes #1836) 2020-02-17 12:27:24 -08:00
Vidar Holen
106f321cf0 Parse keywords with case sensitivity (fixes #1809) 2020-02-17 11:13:29 -08:00
Vidar Holen
1da0becb0f Rename 'Test' stage 2020-02-15 19:24:16 -08:00
Vidar Holen
472579052b Don't try to deploy on PRs 2020-02-15 16:56:20 -08:00
Vidar Holen
c735bbf30a
Merge pull request #1831 from josephcsible/checkfindnameglob
Improve checkFindNameGlob
2020-02-15 16:49:22 -08:00
Joseph C. Sible
eecd003e2d Optimize patterns in checkFindNameGlob
1. Instead of pattern-matching the same list multiple times, do it only
   once and then pass the pieces separately.
2. Don't reconstruct an object equivalent to one we just deconstructed.
2020-02-11 01:04:49 -05:00
Joseph C. Sible
440d0038aa Remove a partial pattern match equivalent to fromJust from checkFindNameGlob 2020-02-11 01:03:10 -05:00
Vidar Holen
12bc7a750c
Merge pull request #1785 from ArturKlauser/multi-arch-docker
Add multi-architecture Docker image build
2020-02-10 20:28:33 -08:00
Vidar Holen
c2d67c15f8
Merge pull request #1802 from szydell/master
SC2016, repair false error for M language parser
2020-02-10 18:25:28 -08:00
Vidar Holen
6043deb8f2
Merge pull request #1824 from josephcsible/patch-1
Simplify literalEquals
2020-02-10 18:20:05 -08:00
Vidar Holen
83d329c8da
Merge pull request #1825 from josephcsible/nofilterm
Use findM instead of filterM
2020-02-10 18:10:15 -08:00
Vidar Holen
d0beac6d0b
Merge pull request #1826 from josephcsible/nofromjust
Use the Identity monad to avoid unnecessary uses of fromJust
2020-02-10 18:05:36 -08:00
Vidar Holen
b88b253cad
Merge pull request #1827 from josephcsible/nofromjust2
Remove more unnecessary uses of fromJust
2020-02-10 18:01:38 -08:00
Vidar Holen
a8f9f25ec9
Merge pull request #1828 from josephcsible/cleanups
Another round of cleanups/refactoring
2020-02-10 18:01:12 -08:00
Joseph C. Sible
85c49a8af9 Simplify mockedSystemInterface 2020-02-09 23:50:48 -05:00
Joseph C. Sible
42abcb7ae2 Simplify shellFromFilename 2020-02-09 23:18:09 -05:00
Joseph C. Sible
d5c5128115 Use isJust instead of reimplementing it 2020-02-09 23:18:09 -05:00
Joseph C. Sible
6d06103cab Remove unnecessary uses of head 2020-02-09 23:18:09 -05:00
Joseph C. Sible
c95914f9b3 Simplify determineShell 2020-02-09 23:18:09 -05:00
Joseph C. Sible
ea24e25efd Use Map.member instead of isJust and Map.lookup 2020-02-09 23:18:09 -05:00
Joseph C. Sible
8f0448133c Use isNothing instead of reimplementing it 2020-02-09 23:18:08 -05:00
Joseph C. Sible
7fc9496320 Use forM_ instead of reimplementing it 2020-02-09 23:18:08 -05:00
Joseph C. Sible
962fad038c Avoid a zip that breaks fusion 2020-02-09 23:18:08 -05:00
Joseph C. Sible
a223a7a5a5 Remove unnecessary fromMaybes 2020-02-09 23:18:08 -05:00
Joseph C. Sible
8e9290badb Do toLower earlier 2020-02-09 23:17:53 -05:00
Joseph C. Sible
292b0840d9 Simplify a double negative 2020-02-09 23:17:53 -05:00
Joseph C. Sible
43c24cf79c Use Map.! instead of reimplementing it 2020-02-09 23:17:53 -05:00
Joseph C. Sible
21ad4196db Simplify findFunction 2020-02-09 23:17:53 -05:00
Joseph C. Sible
172aa7c4fc Avoid unnecessary use of when and unless 2020-02-09 23:17:53 -05:00
Joseph C. Sible
c290eace54 Inline an uncurry 2020-02-09 23:17:53 -05:00
Joseph C. Sible
a6efd02807 Simplify <> for SpaceStatus 2020-02-09 23:17:53 -05:00
Joseph C. Sible
057cc714b3 Simplify matchToken 2020-02-09 23:17:52 -05:00
Joseph C. Sible
0e00249eae Use void instead of do and return () 2020-02-09 23:17:52 -05:00
Joseph C. Sible
0ca50159ec Use head instead of reimplementing it
Normally I wouldn't use head, but this code is partial anyway.
2020-02-09 23:17:52 -05:00
Joseph C. Sible
7e6a556ef1 Get rid of potentially
This already exists as sequence_.
2020-02-09 23:17:52 -05:00
Joseph C. Sible
4bfe6496d9 Simplify check and checkTranslatedStringVariable
Avoid the "potentially" and "Maybe" business, and just use regular guards.
2020-02-09 23:17:52 -05:00
Joseph C. Sible
ffbbfcfe25 Use mapM_ and sequence_ instead of reimplementing them 2020-02-09 23:17:52 -05:00
Joseph C. Sible
cc424bac11 Use find instead of take 1 and filter 2020-02-09 23:17:52 -05:00
Joseph C. Sible
cb01cbf7eb Use mapM instead of implementing a slower version of it 2020-02-09 23:17:52 -05:00
Joseph C. Sible
1e32139f66 Replace mapMaybe and concatMap with list comprehensions 2020-02-09 23:17:52 -05:00
Joseph C. Sible
4d92a2e15c Add getLiteralStringDef and simplify with it 2020-02-09 21:36:38 -05:00
Joseph C. Sible
f8648e5465 Switch getLiteralStringExt to Identity where it can never be Nothing 2020-02-09 21:26:42 -05:00
Joseph C. Sible
4fd8de058b Remove more unnecessary uses of fromJust 2020-02-08 23:48:36 -05:00
Joseph C. Sible
aaffe38198 Use the Identity monad to avoid unnecessary uses of fromJust 2020-02-08 23:20:54 -05:00
Joseph C. Sible
bd116f252b Use findM instead of filterM
Using filterM makes us run the monadic predicate on every list element.
Use findM instead so that we can stop once we find a matching one.
2020-02-08 22:55:45 -05:00
Joseph C. Sible
ef51ed3950
Simplify literalEquals 2020-02-08 14:09:17 -05:00
Vidar Holen
61b073d507 Merge pull request #1817 from furkanpham/master
README.md: Fix pre-compiled binary URL for aarch64
2020-02-08 10:48:37 -08:00
Vidar Holen
9d604ae732
Merge pull request #1822 from yetamrra/arrayindex
SC2191: Tighten index checks
2020-02-08 10:48:07 -08:00
Vidar Holen
1ca0b72329
Merge pull request #1816 from josephcsible/cleanups
Various cleanups and refactorings
2020-02-08 10:38:27 -08:00
Benjamin Gordon
474b23d6e7 SC2191: Tighten index checks
When adding a value containing an equals sign to an indexed array, the
left side is treated as an index if it looks like [N]=val and N is
numeric.  SC2191 currently warns about anything that looks like key=val
even though non-numeric values of key will never be treated as an index.
This causes spurious warnings for the common pattern of building up
program arguments in an array, such as:
  args=(
    --dry-run
    --in="${my_var}"
    --out=/some/path
    -f
  )
  /bin/program "${args[@]}"

Since only numeric expressions can be a valid index for an indexed
array, only emit SC2191 if the left side of a literal string containing
an equals looks numeric.  Other more complicated constructs should still
warn because shellcheck doesn't know if they may evaluate to a numeric
result.  Associative arrays still warn because a non-numeric left side
is a valid subscript.
2020-02-05 16:50:32 -07:00
Furkan Pham
fe2b4b5079 Fix pre-compiled binary URL for aarch64 2020-02-03 13:00:10 +01:00
Joseph C. Sible
e820a5642b Adjust a pattern to get rid of a fromJust 2020-02-02 00:40:22 -05:00
Joseph C. Sible
392b57b8e8 Use maybe instead of isJust and fromJust 2020-02-02 00:27:05 -05:00
Joseph C. Sible
6595e14d25 Adjust a pattern to avoid tail 2020-02-02 00:24:24 -05:00
Joseph C. Sible
115ef29079 Use pattern matching instead of head 2020-02-02 00:16:59 -05:00
Joseph C. Sible
76b798394f Use case matching instead of null
Using null followed by a head, tail, or a partial pattern match is
an anti-pattern. Use case matching instead.
2020-02-01 23:07:16 -05:00
Joseph C. Sible
8a005526cc Use drop instead of splitAt since we only use the second half 2020-02-01 23:04:04 -05:00
Joseph C. Sible
c29b6afa56 Use null instead of comparing with empty lists 2020-02-01 23:04:04 -05:00
Joseph C. Sible
e6e89d68fd Use list comprehensions instead of clunky combinations of map and filter 2020-02-01 23:04:04 -05:00
Joseph C. Sible
f25b8bd03a Use gets instead of fmapping the result of get 2020-02-01 22:50:20 -05:00
Joseph C. Sible
d7278b95f2 Remove unnecessary "map snd" 2020-02-01 22:50:19 -05:00
Joseph C. Sible
5487b3f229 Use sortOn instead of sortBy and comparing 2020-02-01 22:50:18 -05:00
Joseph C. Sible
28978a8b65 Use maybe instead of fromMaybe and fmap 2020-02-01 22:50:17 -05:00
Joseph C. Sible
f5c6771016 Use find instead of listToMaybe and filter 2020-02-01 22:50:16 -05:00
Joseph C. Sible
0f48bb78a5 Remove incorrect otherwise
You're supposed to use otherwise where you need a Boolean, not a pattern
match. This is misleadingly shadowing the real otherwise. Use _ instead.
2020-02-01 22:50:14 -05:00
Joseph C. Sible
93be86f988 Use "drop 1" instead of clumsily rewriting it 2020-02-01 22:50:14 -05:00
Joseph C. Sible
3449e6be21 Get rid of our getOpt, as it already exists as lookup 2020-02-01 22:50:13 -05:00
Joseph C. Sible
2e52c2b56a Use notElem instead of not on the result of elem 2020-02-01 22:50:11 -05:00
Vidar Holen
1696296c0a Make SC2141 trigger more broadly 2020-02-01 16:51:40 -08:00
Peter Gromov
a82e606e8d Don't trigger SC2154 (unassigned var) in -n/-z expressions #1583 2020-01-31 14:49:25 +01:00
Marcin Szydelski
93486ed6ac SC2016: disable for mumps -run %XCMD and LOOP%XCMD 2020-01-21 12:43:27 +01:00
Artur Klauser
499e0ceaba
Add multi-architecture Docker image build
* Adds a shell script with functions to install multi-architecture docker
  support, as well as build, deploy, and test the shellcheck docker images for
  the same set of architectures for which binaries were already built and
  deployed as tarballs.
* Hooks up the multi-architecture docker build, deploy, and test to the existing
  Travis CI/CD pipeline. It is organized as a separate stage which only runs if
  all previous steps in the already existing test stage succeed.
2020-01-07 21:05:17 +01:00
Vidar Holen
ff5f29f661
Merge pull request #1784 from ArturKlauser/travis-warnings
Fix Travis warnings
2020-01-05 12:53:42 -08:00
Vidar Holen
c7bf1fd96e
Merge pull request #1783 from ArturKlauser/fix-osx-travis-build
Fix OSX build on Travis
2020-01-05 12:53:28 -08:00
Artur Klauser
b96b7f35f4
Fix Travis warnings
Fixing the following Travis build config validation warnings:
  * root: deprecated key sudo (The key `sudo` has no effect anymore.)
  * root: missing os, using the default linux
  * deploy: key local-dir is not underscored, using local_dir
  * language: value sh is an alias for shell, using shell
  * root: key matrix is an alias for jobs, using jobs
2019-12-28 14:24:23 +01:00
Artur Klauser
926ee54036
Fix OSX build on Travis
Symlink was failing due to pre-existing target file:
  sudo ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
  ln: /usr/local/bin/sha512sum: File exists
2019-12-28 09:51:11 +01:00
Gandalf-
fdd02c94c0 Issue 1759 mapfile and process substition
https://github.com/koalaman/shellcheck/issues/1759

When a simple process substition is used, this tripped up
the getMapfileArray function by making the last argument
not a variable
2019-12-22 23:19:03 -08:00
Vidar Holen
9008a6833b
Merge pull request #1711 from renatoassis01/master
add github actions link
2019-12-21 18:53:16 -08:00
Vidar Holen
ce60a1764f Merge branch 'jabberabbe-iss1724-builtin-support' 2019-12-21 18:50:52 -08:00
Vidar Holen
cbcca528ae Merge branch 'iss1724-builtin-support' of https://github.com/jabberabbe/shellcheck into jabberabbe-iss1724-builtin-support 2019-12-21 18:13:07 -08:00
Vidar Holen
83187dafd7 Added a unit test for parsing shell keyword case branches 2019-12-21 17:59:09 -08:00
Vidar Holen
d919aaa847 Merge branch 'Gandalf--issue_1731_case_pattern_literals' 2019-12-21 17:56:26 -08:00
Gandalf-
3f296a08c1 Issue 1731 Literals in case patterns
https://github.com/koalaman/shellcheck/issues/1731

Any literal except esac is valid pattern in a case statement
2019-12-18 20:23:48 -08:00
Vidar Holen
0f15fa49ba Make SC2230 optional 2019-12-07 16:11:49 -08:00
Vidar Holen
0a4580e234 Mention that ShellCheck is now compatible with Cabal 3 2019-12-07 16:11:49 -08:00
Vidar Holen
5c7d8129ad Try to search for binary on macOS/Cabal3 2019-11-18 17:12:25 -08:00
Vidar Holen
e075cde357 Revert docker image to 18.04 since ld fails on later versions 2019-11-16 11:46:58 -08:00
Vidar Holen
9f578f41a1 Explicitly add 'mappend' for old GHC versions 2019-11-16 11:16:15 -08:00
Vidar Holen
2c026f1ec7 Support Cabal 3. Man page no longer autobuilds. 2019-11-16 11:06:18 -08:00
Vidar Holen
874bdcb514
Merge pull request #1728 from mgttlinger/patch-1
Nix install instructions
2019-11-15 21:03:01 -08:00
Vidar Holen
fa3eb47193
Merge pull request #1716 from ryantig/ryantig-patch-1
Update README.md
2019-11-15 21:01:24 -08:00
Vidar Holen
989ac32625
Merge pull request #1734 from gabrielelana/braced-regular-for
Parse regular `for` with body in curly braces
2019-11-15 20:59:51 -08:00
Vidar Holen
2bbfd0570d
Merge pull request #1735 from gabrielelana/quoted-heredoc
Support for heredoc quoted token like `'"FOO"`
2019-11-15 20:27:10 -08:00
Vidar Holen
9b1befadc1 Update brew before building on macOS due to incompatible Ruby 2019-11-15 09:26:01 -08:00
Vidar Holen
f44624a9c0 Hide <> from Writer to not conflict with Semigroup 2019-11-14 20:02:25 -08:00
Vidar Holen
c75bbcbd60 Include missing Semigroup import 2019-11-13 22:10:27 -08:00
Vidar Holen
daa9c08dd5
Merge pull request #1749 from lvjp/simple-docker-build
Make image build process a bit simpler
2019-11-13 22:04:14 -08:00
Vidar Holen
4da34fbc64 Merge branch 'translatedVars' 2019-11-13 21:48:53 -08:00
Vidar Holen
4a63a3a8bd For SC2256, make sure the complete string is a variable name 2019-11-13 21:48:01 -08:00
Benjamin Gordon
2341a4c683 SC2256: Check for translated strings matching known variables
SC2247 already warns about translated strings that look like $"(foo)" or
$"{foo}".  Since typical use of translated strings is to translate whole
messages, a string like $"foo" is likely to be a similar mistake if foo
is the name of an existing variable.  Conversely, a string like
$"foo bar" is potentially meant to be a message id even if foo is a
known variable.

Add a warning for the $"foo" case, but make it separate from the
existing warning so that projects that reuse variable names as their
message ids can separately disable the new warning.
2019-11-13 16:41:16 -07:00
Laurent VERDOÏA
7eb6b35cb0 Make image build process a bit simpler
Take full leverage of multi-stage docker build.
2019-11-09 10:26:59 +01:00
Vidar Holen
93eca1cb8e Only trigger SC1014 when command is a complete word (fixes #1737) 2019-11-03 13:26:23 -08:00
Vidar Holen
e701cf6fad Warn about [ x -ot y ] in POSIX mode 2019-11-03 13:25:35 -08:00
Vidar Holen
5962b01816 Correctly handle empty variables for SC2086 (fixes #1722) 2019-11-03 12:46:25 -08:00
Tito Sacchi
5becc673b2 Modify CHANGELOG.md 2019-11-01 14:36:15 +01:00
Tito Sacchi
84ca7711c4 Make command-specific checks act on builtin ...
Now if shellchecks encounters a command like `builtin cmd ...`
it applies the same check that would be applied to `cmd ...`.
2019-11-01 14:28:00 +01:00
Tito Sacchi
0e0de94045 Fix issue #1724
(bash: missing support for 'builtin' keyword)
Now shellcheck looks for the arguments to 'builtin' to determine
read/written variables. A change in the parser makes sure that
assignments are parsed correctly in commands that start with 'builtin'.
2019-11-01 13:49:17 +01:00
gabriele.lana
699aac589a Support for heredoc quoted token like '"FOO"
Fixes #1650
2019-10-26 17:36:32 +02:00
gabriele.lana
30c75340e6 Parse regular for with body in curly braces
Fixes #1694
2019-10-26 15:41:46 +02:00
Vidar Holen
4dfd7eb1cf Use single quotes for the format string example in SC2059 2019-10-24 10:33:17 -07:00
Merlin Göttlinger
79ba67dbd3
Nix install instructions 2019-10-21 08:04:59 +02:00
Vidar Holen
60f75e5b8a Warn about unexpected characters after ]/]] (fixes #1680) 2019-10-13 20:26:40 -07:00
Vidar Holen
f042b0ebd1 Merge branch 'iboss-ptk-read-t-0' 2019-10-12 20:55:32 -07:00
Vidar Holen
764fdcb260 Move failing test to correct check 2019-10-12 20:50:55 -07:00
Vidar Holen
7473d4a743 Make read -t 0 test more forgiving towards other flags 2019-10-12 20:45:36 -07:00
Vidar Holen
91abd979f2 Merge branch 'read-t-0' of https://github.com/iboss-ptk/shellcheck into iboss-ptk-read-t-0 2019-10-12 20:23:13 -07:00
Vidar Holen
afea62de4e Suggest using $((..)) in [ 2*3 -eq 6 ] (fixes #1641) 2019-10-12 19:55:20 -07:00
ryantig
fa0f88c106
Update README.md
Repair link to #installing-a-pre-compiled-binary (was pointing to #installing-the-shellcheck-binary)
2019-10-04 11:11:21 -07:00
Supanat Pothivarakorn
7fb399528c Allow read -t 0 to not require -r flag
since it has specific purpose for checking only
2019-10-02 22:34:43 +07:00
Vidar Holen
de9ab4e6ef Fix glob range duplicate warning in [!!] (fixes #1706) 2019-09-28 14:03:11 -07:00
Renato Assis
ff1eab286c
add github 2019-09-25 19:20:25 -03:00
Vidar Holen
e01c470598 Suggest quoting case patterns, as for SC2053 (fixes #1682) 2019-09-08 20:08:43 -07:00
Glen Mailer
9423691039
Mention the CircleCI shellcheck orb in the README. 2019-08-12 22:24:47 +01:00
Vidar Holen
71a4053e8c Remove _cleanup now that builds don't run in sequence 2019-07-31 21:32:13 -07:00
Vidar Holen
3fdc6babb2 Update TravisCI config for new winghc docker image 2019-07-31 21:16:36 -07:00
Vidar Holen
c175971bf0 Make -f diff stop saying it found more issues when it didn't. 2019-07-28 20:50:50 -07:00
85 changed files with 10393 additions and 2720 deletions

View file

@ -1,82 +0,0 @@
#!/bin/bash
_cleanup(){
rm -rf dist shellcheck || true
}
build_linux() {
# Linux Docker image
name="$DOCKER_BASE"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
docker build -t "$name:current" .
docker run "$name:current" --version
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
id=$(docker create "$name:current")
docker cp "$id:/bin/shellcheck" "shellcheck"
docker rm "$id"
ls -l shellcheck
./shellcheck myscript
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
done
# Linux Alpine based Docker image
name="$DOCKER_BASE-alpine"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
docker build -f Dockerfile.alpine -t "$name:current" .
docker run "$name:current" sh -c 'shellcheck --version'
_cleanup
}
build_aarch64() {
# Linux aarch64 static executable
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
done
}
build_armv6hf() {
# Linux armv6hf static executable
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
done
_cleanup
}
build_windows() {
# Windows .exe
docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
for tag in $TAGS
do
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
done
_cleanup
}
build_osx() {
# Darwin x86_64 static executable
brew install cabal-install pandoc gnu-tar
sudo ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
sudo ln -s /usr/local/bin/gtar /usr/local/bin/tar
export PATH="/usr/local/bin:$PATH"
cabal update
cabal install --dependencies-only
cabal build shellcheck
for tag in $TAGS
do
cp "dist/build/shellcheck/shellcheck" "deploy/shellcheck-$tag.darwin-x86_64";
done
_cleanup
}

View file

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

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

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

159
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,159 @@
name: Build ShellCheck
# Run this workflow every time a new commit pushed to your repository
on: push
jobs:
package_source:
name: Package Source Code
runs-on: ubuntu-latest
steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-mark manual ghc # Don't bother installing ghc just to tar up source
sudo apt-get install cabal-install
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Deduce tags
run: |
mkdir source
echo "latest" > source/tags
if tag=$(git describe --exact-match --tags)
then
echo "stable" >> source/tags
echo "$tag" >> source/tags
fi
cat source/tags
- name: Package Source
run: |
grep "stable" source/tags || ./setgitversion
cabal sdist
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: source
path: source/
run_tests:
name: Run tests
needs: package_source
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Install dependencies
run: |
sudo apt-get update && sudo apt-get install ghc cabal-install
cabal update
- name: Unpack source
run: |
cd source
tar xvf source.tar.gz --strip-components=1
- name: Build and run tests
run: |
cd source
cabal test
build_source:
name: Build
needs: package_source
strategy:
matrix:
build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Build source
run: |
mkdir -p bin
mkdir -p bin/${{matrix.build}}
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{matrix.build}}.bin
path: bin/
package_binary:
name: Package Binaries
needs: build_source
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Work around GitHub permissions bug
run: chmod +x *.bin/*/shellcheck*
- name: Package binaries
run: |
export TAGS="$(cat source/tags)"
mkdir -p deploy
cp -r *.bin/* deploy
cd deploy
../.prepare_deploy
rm -rf */ README* LICENSE*
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: deploy
path: deploy/
deploy:
name: Deploy binaries
needs: package_binary
runs-on: ubuntu-latest
environment: Deploy
steps:
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install hub
- name: Checkout repository
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Upload to GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
export TAGS="$(cat source/tags)"
./.github_deploy
- name: Waiting for GitHub to replicate uploaded releases
run: |
sleep 300
- name: Upload to Docker Hub
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }}
DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }}/shellcheck
run: |
export TAGS="$(cat source/tags)"
( source ./.multi_arch_docker && set -eux && multi_arch_docker::main )

28
.github_deploy Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash
set -x
shopt -s extglob
export EDITOR="touch"
# Sanity check
gh --version || exit 1
hub release show latest || exit 1
for tag in $TAGS
do
if ! hub release show "$tag"
then
echo "Creating new release $tag"
git show --no-patch --format='format:%B' > description
hub release create -F description "$tag"
fi
files=()
for file in deploy/*
do
[[ $file == *.@(xz|gz|zip) ]] || continue
[[ $file == *"$tag"* ]] || continue
files+=("$file")
done
gh release upload "$tag" "${files[@]}" --clobber || exit 1
done

1
.gitignore vendored
View file

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

89
.multi_arch_docker Executable file
View file

@ -0,0 +1,89 @@
#!/bin/bash
# This script builds and deploys multi-architecture docker images from the
# binaries previously built and deployed to GitHub.
function multi_arch_docker::install_docker_buildx() {
# Install QEMU multi-architecture support for docker buildx.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# Instantiate docker buildx builder with multi-architecture support.
docker buildx create --name mybuilder
docker buildx use mybuilder
# Start up buildx and verify that all is OK.
docker buildx inspect --bootstrap
}
# Log in to Docker Hub for deployment.
function multi_arch_docker::login_to_docker_hub() {
echo "$DOCKER_PASSWORD" | docker login -u="$DOCKER_USERNAME" --password-stdin
}
# Run buildx build and push. Passed in arguments augment the command line.
function multi_arch_docker::buildx() {
mkdir -p /tmp/empty
docker buildx build \
--platform "${DOCKER_PLATFORMS// /,}" \
--push \
--progress plain \
-f Dockerfile.multi-arch \
"$@" \
/tmp/empty
rmdir /tmp/empty
}
# Build and push plain and alpine docker images for all tags.
function multi_arch_docker::build_and_push_all() {
for tag in $TAGS; do
multi_arch_docker::buildx -t "$DOCKER_BASE:$tag" --build-arg "tag=$tag"
multi_arch_docker::buildx -t "$DOCKER_BASE-alpine:$tag" \
--build-arg "tag=$tag" --target alpine
done
}
# Test all pushed docker images.
function multi_arch_docker::test_all() {
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
for platform in $DOCKER_PLATFORMS; do
for tag in $TAGS; do
for ext in '-alpine' ''; do
image="${DOCKER_BASE}${ext}:${tag}"
msg="Testing docker image $image on platform $platform"
line="${msg//?/=}"
printf '\n%s\n%s\n%s\n' "${line}" "${msg}" "${line}"
docker pull -q --platform "$platform" "$image"
if [ -n "$ext" ]; then
echo -n "Image architecture: "
docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m'
version=$(docker run --rm "$image" shellcheck --version \
| grep 'version:')
else
version=$(docker run --rm "$image" --version | grep 'version:')
fi
version=${version/#version: /v}
echo "shellcheck version: $version"
if [[ ! ("$tag" =~ ^(latest|stable)$) && "$tag" != "$version" ]]; then
echo "Version mismatch: shellcheck $version tagged as $tag"
exit 1
fi
if [ -n "$ext" ]; then
docker run --rm -v "$PWD:/mnt" -w /mnt "$image" shellcheck myscript
else
docker run --rm -v "$PWD:/mnt" "$image" myscript
fi
done
done
done
}
function multi_arch_docker::main() {
export DOCKER_PLATFORMS='linux/amd64'
DOCKER_PLATFORMS+=' linux/arm64'
DOCKER_PLATFORMS+=' linux/arm/v6'
DOCKER_PLATFORMS+=' linux/riscv64'
multi_arch_docker::install_docker_buildx
multi_arch_docker::login_to_docker_hub
multi_arch_docker::build_and_push_all
multi_arch_docker::test_all
}

View file

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

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,66 +0,0 @@
sudo: required
language: sh
services:
- docker
matrix:
include:
- os: linux
env: BUILD=linux
- os: linux
env: BUILD=windows
- os: linux
env: BUILD=armv6hf
- os: linux
env: BUILD=aarch64
- os: osx
env: BUILD=osx
before_install: |
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
DOCKER_BUILDS=""
TAGS=""
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
echo "Tags are $TAGS"
script:
- mkdir -p deploy
- source ./.compile_binaries
- ./striptests
- set -x; build_"$BUILD"; set +x;
- ./.prepare_deploy
after_success: |
if [ "$BUILD" = "linux" ]; then
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
for repo in $DOCKER_BUILDS; do
for tag in $TAGS; do
echo "Deploying $repo:current as $repo:$tag...";
docker tag "$repo:current" "$repo:$tag" || exit 1;
docker push "$repo:$tag" || exit 1;
done;
done;
fi
after_failure: |
id
pwd
df -h
find . -name '*.log' -type f -exec grep "" /dev/null {} +
find . -ls
deploy:
provider: gcs
skip_cleanup: true
access_key_id: GOOG7MDN7WEH6IIGBDCA
secret_access_key:
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
bucket: shellcheck
local-dir: deploy
on:
repo: koalaman/shellcheck
all_branches: true

View file

@ -1,3 +1,156 @@
## 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
### Added
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
- SC2317: Warn about unreachable commands
- SC2318: Warn about backreferences in 'declare x=1 y=$x'
- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test
- SC2321: Suggest removing $((..)) in array[$((idx))]=val
- SC2322: Suggest collapsing double parentheses in arithmetic contexts
- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val
### Fixed
- SC2086: Now uses DFA to make more accurate predictions about values
- SC2086: No longer warns about values declared as integer with declare -i
### Changed
- ShellCheck now has a Data Flow Analysis engine to make smarter decisions
based on control flow rather than just syntax. Existing checks will
gradually start using it, which may cause them to trigger differently
(but more accurately).
- Values in directives/shellcheckrc can now be quoted with '' or ""
## v0.8.0 - 2021-11-06
### Added
- `disable=all` now conveniently disables all warnings
- `external-sources=true` directive can be added to .shellcheckrc to make
shellcheck behave as if `-x` was specified.
- Optional `check-extra-masked-returns` for pointing out commands with
suppressed exit codes (SC2312).
- Optional `require-double-brackets` for recommending \[\[ ]] (SC2292).
- SC2286-SC2288: Warn when command name ends in a symbol like `/.)'"`
- SC2289: Warn when command name contains tabs or linefeeds
- SC2291: Warn about repeated unquoted spaces between words in echo
- SC2292: Suggest [[ over [ in Bash/Ksh scripts (optional)
- SC2293/SC2294: Warn when calling `eval` with arrays
- SC2295: Warn about "${x#$y}" treating $y as a pattern when not quoted
- SC2296-SC2301: Improved warnings for bad parameter expansions
- SC2302/SC2303: Warn about loops over array values when using them as keys
- SC2304-SC2306: Warn about unquoted globs in expr arguments
- SC2307: Warn about insufficient number of arguments to expr
- SC2308: Suggest other approaches for non-standard expr extensions
- SC2313: Warn about `read` with unquoted, array indexed variable
### Fixed
- SC2102 about repetitions in ranges no longer triggers on [[ -v arr[xx] ]]
- SC2155 now recognizes `typeset` and local read-only `declare` statements
- SC2181 now tries to avoid triggering for error handling functions
- SC2290: Warn about misused = in declare & co, which were not caught by SC2270+
- The flag --color=auto no longer outputs color when TERM is "dumb" or unset
### Changed
- SC2048: Warning about $\* now also applies to ${array[\*]}
- SC2181 now only triggers on single condition tests like `[ $? = 0 ]`.
- Quote warnings are now emitted for declaration utilities in sh
- Leading `_` can now be used to suppress warnings about unused variables
- TTY output now includes warning level in text as well as color
### Removed
- SC1004: Literal backslash+linefeed in '' was found to be usually correct
## v0.7.2 - 2021-04-19
### Added
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
- SC1143: Warn about line continuations in comments
- SC2259/SC2260: Warn when redirections override pipes
- SC2261: Warn about multiple competing redirections
- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit
- SC2264: Warn about wrapper functions that blatantly recurse
- SC2265/SC2266: Warn when using & or | with test statements
- SC2267: Warn when using xargs -i instead of -I
- SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]`
### Fixed
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
is still purely cosmetic and does not allow ShellCheck to continue.
- Improved error reporting for trailing tokens after ]/]] and compound commands
- `#!/usr/bin/env -S shell` is now handled correctly
- Here docs with \r are now parsed correctly and give better warnings
### Changed
- Assignments are now parsed to spec, without leniency for leading $ or spaces
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files
- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
- SC2270-SC2285: Improved warnings about misused `=`, e.g. `${var}=42`
## v0.7.1 - 2020-04-04
### Fixed
- `-f diff` no longer claims that it found more issues when it didn't
- Known empty variables now correctly trigger SC2086
- ShellCheck should now be compatible with Cabal 3
- SC2154 and all command-specific checks now trigger for builtins
called with `builtin`
### Added
- SC1136: Warn about unexpected characters after ]/]]
- SC2254: Suggest quoting expansions in case statements
- SC2255: Suggest using `$((..))` in `[ 2*3 -eq 6 ]`
- SC2256: Warn about translated strings that are known variables
- SC2257: Warn about arithmetic mutation in redirections
- SC2258: Warn about trailing commas in for loop elements
### Changed
- SC2230: 'command -v' suggestion is now off by default (-i deprecate-which)
- SC1081: Keywords are now correctly parsed case sensitively, with a warning
## v0.7.0 - 2019-07-28
### Added
- Precompiled binaries for macOS and Linux aarch64
@ -117,7 +270,7 @@
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays in [/[[
- SC2196/SC2197: Warn about deprected egrep/fgrep
- SC2196/SC2197: Warn about deprecated egrep/fgrep
- SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
@ -134,7 +287,7 @@
### Fixed
- `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives
- Here doc delimters with esoteric quoting like `foo""` are now handled
- Here doc delimiters with esoteric quoting like `foo""` are now handled
- SC2095 about `ssh` in while read loops is now suppressed when using `-n`
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
- `grep -F` now suppresses regex related suggestions

View file

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

26
Dockerfile.multi-arch Normal file
View file

@ -0,0 +1,26 @@
# Alpine image
FROM alpine:latest AS alpine
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
ARG tag
# Put the right binary for each architecture into place for the
# multi-architecture docker image.
RUN set -x; \
arch="$(uname -m)"; \
echo "arch is $arch"; \
if [ "${arch}" = 'armv7l' ]; then \
arch='armv6hf'; \
fi; \
url_base='https://github.com/koalaman/shellcheck/releases/download/'; \
tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \
wget "${url_base}${tar_file}" -O - | tar xJf -; \
mv "shellcheck-${tag}/shellcheck" /bin/; \
rm -rf "shellcheck-${tag}"; \
ls -laF /bin/shellcheck
# ShellCheck image
FROM scratch
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
WORKDIR /mnt
COPY --from=alpine /bin/shellcheck /bin/
ENTRYPOINT ["/bin/shellcheck"]

12
LICENSE
View file

@ -1,13 +1,3 @@
Employer mandated disclaimer:
I am providing code in the repository to you under an open source license.
Because this is my personal repository, the license you receive to my code is
from me and other individual contributors, and not my employer (Facebook).
- Vidar "koala_man" Holen
----
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
@ -681,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

101
README.md
View file

@ -1,4 +1,5 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck)
[![Build Status](https://github.com/koalaman/shellcheck/actions/workflows/build.yml/badge.svg)](https://github.com/koalaman/shellcheck/actions/workflows/build.yml)
# ShellCheck - A shell script static analysis tool
@ -76,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
* 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).
@ -109,14 +110,15 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/)
Services and platforms with third party plugins:
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner)
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux)
* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
* [CodeRabbit](https://coderabbit.ai/)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
or by downloading and unpacking a [binary release](#installing-the-shellcheck-binary).
or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).
It's a good idea to manually install a specific ShellCheck version regardless. This avoids
any surprise build breaks when a new version with new warnings is published.
@ -141,13 +143,13 @@ On systems with Stack (installs to `~/.local/bin`):
On Debian based distros:
apt-get install shellcheck
sudo apt install shellcheck
On Arch Linux based distros:
pacman -S shellcheck
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR.
On Gentoo based distros:
@ -155,8 +157,8 @@ On Gentoo based distros:
On EPEL based distros:
yum -y install epel-release
yum install ShellCheck
sudo yum -y install epel-release
sudo yum install ShellCheck
On Fedora based distros:
@ -166,10 +168,14 @@ On FreeBSD:
pkg install hs-ShellCheck
On OS X with homebrew:
On macOS (OS X) with Homebrew:
brew install shellcheck
Or with MacPorts:
sudo port install shellcheck
On OpenBSD:
pkg_add shellcheck
@ -190,12 +196,22 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/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)):
```cmd
C:\> scoop install shellcheck
```
From [conda-forge](https://anaconda.org/conda-forge/shellcheck):
conda install -c conda-forge shellcheck
From Snap Store:
snap install --channel=edge shellcheck
@ -209,15 +225,30 @@ docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
Using the [nix package manager](https://nixos.org/nix):
```sh
nix-env -iA nixpkgs.shellcheck
```
Using the [Flox package manager](https://flox.dev/)
```sh
flox install shellcheck
```
Alternatively, you can download pre-compiled binaries for the latest release here:
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz) aka ARM64 (statically linked)
* [MacOS, x86_64](https://shellcheck.storage.googleapis.com/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz)
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
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).
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:
@ -226,6 +257,19 @@ pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
```
### pre-commit
To run ShellCheck via [pre-commit](https://pre-commit.com/), add the hook to your `.pre-commit-config.yaml`:
```
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.7.2
hooks:
- id: shellcheck
# args: ["--severity=warning"] # Optionally only show errors and warnings
```
### Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
@ -244,7 +288,7 @@ A simple installer may do something like:
```bash
scversion="stable" # or "v0.4.7", or "latest"
wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
cp "shellcheck-${scversion}/shellcheck" /usr/bin/
shellcheck --version
```
@ -257,7 +301,7 @@ This section describes how to build ShellCheck from a source directory. ShellChe
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
On macOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
$ brew install cabal-install
@ -273,10 +317,6 @@ Verify that `cabal` is installed and update its dependency list with
$ cabal install
Or if you intend to run the tests:
$ cabal install --enable-tests
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
@ -332,6 +372,7 @@ echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
unset var[i] # Array index treated as glob
```
### Conditionals
@ -350,6 +391,7 @@ ShellCheck can recognize many types of incorrect test statements.
[ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..))
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
```
### Frequently misused commands
@ -421,6 +463,8 @@ echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count
eval "${array[@]}" # Lost word boundaries in array eval
for i in "${x[@]}"; do ${x[$i]} # Using array value as key
```
### Robustness
@ -445,6 +489,7 @@ ShellCheck will warn when using features not supported by the shebang. For examp
echo {1..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh
expr match str regex # Unportable alias for `expr str : regex`
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
@ -465,10 +510,15 @@ rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors
!# bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input
var2=$var2 # Variable assigned to itself
[ x$var = xval ] # Antiquated x-comparisons
ls() { ls -l "$@"; } # Infinitely recursive wrapper
alias ls='ls -l'; ls foo # Alias used before it takes effect
for x; do for x; do # Nested loop uses same variable
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
```
@ -512,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).
* 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,36 +0,0 @@
import Distribution.PackageDescription (
HookedBuildInfo,
emptyHookedBuildInfo )
import Distribution.Simple (
Args,
UserHooks ( preSDist ),
defaultMainWithHooks,
simpleUserHooks )
import Distribution.Simple.Setup ( SDistFlags )
import System.Process ( system )
main = defaultMainWithHooks myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
-- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message:
--
-- /bin/sh: pandoc: command not found
--
-- Since the man page is listed in the Extra-Source-Files section of
-- our cabal file, a failure here should result in a failure to
-- create the distribution tarball (that's a good thing).
--
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
return emptyHookedBuildInfo
where
pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"

View file

@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.7.0
Version: 0.10.0
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@ -7,8 +7,8 @@ Category: Static Analysis
Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: https://www.shellcheck.net/
Build-Type: Custom
Cabal-Version: >= 1.8
Build-Type: Simple
Cabal-Version: 1.18
Bug-reports: https://github.com/koalaman/shellcheck/issues
Description:
The goals of ShellCheck are:
@ -22,23 +22,19 @@ Description:
* To point out subtle caveats, corner cases and pitfalls, that may cause an
advanced user's otherwise working script to fail under future circumstances.
Extra-Doc-Files:
README.md
CHANGELOG.md
Extra-Source-Files:
-- documentation
README.md
shellcheck.1.md
-- built with a cabal sdist hook
shellcheck.1
-- A script to build the man page using pandoc
manpage
-- convenience script for stripping tests
striptests
-- tests
test/shellcheck.hs
custom-setup
setup-depends:
base >= 4 && <5,
process >= 1.0 && <1.7,
Cabal >= 1.10 && <2.5
source-repository head
type: git
location: git://github.com/koalaman/shellcheck.git
@ -49,21 +45,26 @@ library
build-depends:
semigroups
build-depends:
aeson,
array,
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-- Just disable that version entirely to fail fast.
base > 4.6.0.1 && < 5,
bytestring,
containers >= 0.5,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4,
-- The lower bounds are based on GHC 7.10.3
-- The upper bounds are based on GHC 9.8.1
aeson >= 1.4.0 && < 2.3,
array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5,
bytestring >= 0.10.6 && < 0.13,
containers >= 0.5.6 && < 0.8,
deepseq >= 1.4.1 && < 1.6,
Diff >= 0.4.0 && < 1.1,
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
filepath >= 1.4.0 && < 1.6,
mtl >= 2.2.2 && < 2.4,
parsec >= 3.1.14 && < 3.2,
QuickCheck >= 2.14.2 && < 2.16,
regex-tdfa >= 1.2.0 && < 1.4,
transformers >= 0.4.2 && < 0.7,
-- getXdgDirectory from 1.2.3.0
directory >= 1.2.3 && < 1.4,
-- When cabal supports it, move this to setup-depends:
process
exposed-modules:
@ -72,11 +73,15 @@ library
ShellCheck.Analytics
ShellCheck.Analyzer
ShellCheck.AnalyzerLib
ShellCheck.CFG
ShellCheck.CFGAnalysis
ShellCheck.Checker
ShellCheck.Checks.Commands
ShellCheck.Checks.ControlFlow
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport
ShellCheck.Data
ShellCheck.Debug
ShellCheck.Fixer
ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle
@ -88,9 +93,11 @@ library
ShellCheck.Formatter.Quiet
ShellCheck.Interface
ShellCheck.Parser
ShellCheck.Prelude
ShellCheck.Regex
other-modules:
Paths_ShellCheck
default-language: Haskell98
executable shellcheck
if impl(ghc < 8.0)
@ -99,18 +106,21 @@ executable shellcheck
build-depends:
aeson,
array,
base >= 4 && < 5,
base,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
deepseq,
Diff,
directory,
fgl,
mtl,
filepath,
parsec >= 3.0,
QuickCheck >= 2.7.4,
parsec,
QuickCheck,
regex-tdfa,
transformers,
ShellCheck
default-language: Haskell98
main-is: shellcheck.hs
test-suite test-shellcheck
@ -118,17 +128,19 @@ test-suite test-shellcheck
build-depends:
aeson,
array,
base >= 4 && < 5,
base,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
deepseq,
Diff,
directory,
fgl,
filepath,
mtl,
parsec,
QuickCheck >= 2.7.4,
QuickCheck,
regex-tdfa,
transformers,
ShellCheck
default-language: Haskell98
main-is: test/shellcheck.hs

17
build/README.md Normal file
View file

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

12
build/build_builder Executable file
View file

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

View file

@ -0,0 +1,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

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

13
build/darwin.x86_64/build Executable file
View file

@ -0,0 +1,13 @@
#!/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"
"$TARGET-strip" -Sx "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
} >&2
tar czv "$TARGETNAME"

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

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

View file

@ -0,0 +1,40 @@
FROM ubuntu:20.04
ENV TARGET aarch64-linux-gnu
ENV TARGETNAME linux.aarch64
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
# The rest are from 22.10
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
# 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
WORKDIR /ghc
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 cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
# Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

14
build/linux.aarch64/build Executable file
View file

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

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

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

View file

@ -0,0 +1,42 @@
# This Docker file uses a custom QEmu fork with patches to follow execve
# to build all of ShellCheck emulated.
FROM ubuntu:24.04
ENV TARGETNAME linux.armv6hf
# Build QEmu with execve follow support
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap
WORKDIR /qemu
RUN git clone --depth 1 https://github.com/koalaman/qemu .
RUN ./configure --static --disable-werror && cd build && ninja qemu-arm
ENV QEMU_EXECVE 1
# Convenience utility
COPY scutil /bin/scutil
COPY scutil /chroot/bin/scutil
RUN chmod +x /bin/scutil /chroot/bin/scutil
# Set up an armv6 userspace
WORKDIR /
RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian
RUN cp /qemu/build/qemu-arm /chroot/bin/qemu
RUN scutil emu /debootstrap/debootstrap --second-stage
# Install deps in the chroot
RUN scutil emu apt-get update
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
RUN scutil emu cabal update
# Finally we can build the current dependencies. This takes hours.
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
# Generated with `cabal freeze --constraint 'hashable -arch-native'`
COPY cabal.project.freeze /chroot/etc
RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS
# Copy the build script
COPY build /chroot/bin
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]

17
build/linux.armv6hf/build Executable file
View file

@ -0,0 +1,17 @@
#!/bin/sh
set -xe
mkdir /scratch && cd /scratch
{
tar xzv --strip-components=1
cp /etc/cabal.project.freeze .
chmod +x striptests && ./striptests
mkdir "$TARGETNAME"
# This script does not cabal update because compiling anything new is slow
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
ls -l "$TARGETNAME"
strip -s "$TARGETNAME/shellcheck"
ls -l "$TARGETNAME"
"$TARGETNAME/shellcheck" --version
} >&2
tar czv "$TARGETNAME"

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 "$@"
}
"$@"

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

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

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

@ -0,0 +1,24 @@
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
# Install GHC and cabal
USER root
RUN apk add ghc cabal g++ libffi-dev curl bash
# Use ld.bfd instead of ld.gold due to
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
# relocation refers to local symbol "" [2], which is defined in a discarded section
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections"
# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

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

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

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

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

30
build/run_builder Executable file
View file

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

View file

@ -0,0 +1,27 @@
FROM ubuntu:20.04
ENV TARGETNAME windows.x86_64
# We don't need wine32, even though it complains
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y curl busybox wine winbind
# Fetch Windows version, will be available under z:\haskell
WORKDIR /haskell
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue
# that necessitated this but I don't care enough to find out
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

18
build/windows.x86_64/build Executable file
View file

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

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

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

294
doc/shellcheck_logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 244 KiB

4
manpage Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
echo >&2 "Generating man page using pandoc"
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit
echo >&2 "Done. You can read it with: man ./shellcheck.1"

View file

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

View file

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

View file

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

11
setgitversion Executable file
View file

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

View file

@ -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,
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*
: 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.
**--rcfile** *RCFILE*
: Prefer the specified configuration file over searching for one
in the default locations.
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: 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*
: 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,
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.
@ -107,11 +120,14 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input.
: Follow `source` statements even when the file is not specified as input.
By default, `shellcheck` will only follow files specified on the command
line (plus `/dev/null`). This option allows following any file the script
may `source`.
This option may also be enabled using `external-sources=true` in
`.shellcheckrc`. This flag takes precedence.
**FILES...**
: One or more script files to check, or "-" for standard input.
@ -232,12 +248,28 @@ Valid keys are:
**disable**
: Disables a comma separated list of error codes for the following command.
The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop.
like a function definition, subshell block or loop. A range can be
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
All warnings can be disabled with `disable=all`.
**enable**
: Enable an optional check by name, as listed with **--list-optional**.
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**
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
arbitrary files from 'source' statements (the way most tools do).
This option defaults to `false` only due to ShellCheck's origin as a
remote service for checking untrusted scripts. It can safely be enabled
for normal development.
**source**
: Overrides the filename included by a `source`/`.` statement. This can be
used to tell shellcheck where to look for a file whose name is determined
@ -254,7 +286,7 @@ Valid keys are:
**shell**
: Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'.
as a more targeted alternative to 'disable=SC2039'.
# RC FILES
@ -269,17 +301,23 @@ Here is an example `.shellcheckrc`:
source-path=SCRIPTDIR
source-path=/mnt/chroot
# Since 0.9.0, values can be quoted with '' or "" to allow spaces
source-path="My Documents/scripts"
# Allow opening any 'source'd file, even if not specified as input
external-sources=true
# Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables
# Turn on warnings for unassigned uppercase variables
enable=check-unassigned-uppercase
# Allow using `which` since it gives full paths and is common enough
disable=SC2230
# Allow [ ! -z foo ] instead of suggesting -n
disable=SC2236
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the XDG config directory
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME`
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used.
@ -301,7 +339,7 @@ invocation.
# RETURN VALUES
ShellCheck uses the follow exit codes:
ShellCheck uses the following exit codes:
+ 0: All files successfully scanned with no issues.
+ 1: All files successfully scanned with some issues.
@ -319,10 +357,32 @@ locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHORS
# KNOWN INCOMPATIBILITIES
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
(If nothing in this section makes sense, you are unlikely to be affected by it)
To avoid confusing and misguided suggestions, ShellCheck requires function
bodies to be either `{ brace groups; }` or `( subshells )`, and function names
containing `[]*=!` are only recognized after a `function` keyword.
The following unconventional function definitions are identical in Bash,
but ShellCheck only recognizes the latter.
[x!=y] () [[ $1 ]]
function [x!=y] () { [[ $1 ]]; }
Shells without the `function` keyword do not allow these characters in function
names to begin with. Function names containing `{}` are not supported at all.
Further, if ShellCheck sees `[x!=y]` it will assume this is an invalid
comparison. To invoke the above function, quote the command as in `'[x!=y]'`,
or to retain the same globbing behavior, use `command [x!=y]`.
ShellCheck imposes additional restrictions on the `[` command to help diagnose
common invalid uses. While `[ $x= 1 ]` is defined in POSIX, ShellCheck will
assume it was intended as the much more likely comparison `[ "$x" = 1 ]` and
fail accordingly. For unconventional or dynamic uses of the `[` command, use
`test` or `\[` instead.
# REPORTING BUGS
@ -330,12 +390,17 @@ Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues
# AUTHORS
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
# COPYRIGHT
Copyright 2012-2019, Vidar Holen and contributors.
Copyright 2012-2024, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html
# SEE ALSO
sh(1) bash(1)
sh(1) bash(1) dash(1) ksh(1)

View file

@ -34,6 +34,8 @@ import qualified ShellCheck.Formatter.Quiet
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Except
import Data.Bits
import Data.Char
@ -74,7 +76,8 @@ data Options = Options {
externalSources :: Bool,
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions,
minSeverity :: Severity
minSeverity :: Severity,
rcfile :: Maybe FilePath
}
defaultOptions = Options {
@ -84,7 +87,8 @@ defaultOptions = Options {
formatterOptions = newFormatterOptions {
foColorOption = ColorAuto
},
minSeverity = StyleC
minSeverity = StyleC,
rcfile = Nothing
}
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@ -98,6 +102,8 @@ options = [
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"]
(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"]
(ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")",
@ -105,6 +111,9 @@ options = [
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"]
(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"]
(ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')",
@ -113,7 +122,7 @@ options = [
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)",
"Specify dialect (sh, bash, dash, ksh, busybox)",
Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)",
@ -225,7 +234,7 @@ runFormatter sys format options files = do
f :: Status -> FilePath -> IO Status
f status file = do
newStatus <- process file `catch` handler file
return $ status `mappend` newStatus
return $! status `mappend` newStatus
handler :: FilePath -> IOException -> IO Status
handler file e = reportFailure file (show e)
reportFailure file str = do
@ -234,7 +243,7 @@ runFormatter sys format options files = do
process :: FilePath -> IO Status
process filename = do
input <- siReadFile sys filename
input <- siReadFile sys Nothing filename
either (reportFailure filename) check input
where
check contents = do
@ -250,9 +259,9 @@ runFormatter sys format options files = do
else SomeProblems
parseEnum name value list =
case filter ((== value) . fst) list of
[(name, value)] -> return value
[] -> do
case lookup value list of
Just value -> return value
Nothing -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure
@ -365,6 +374,11 @@ parseOption flag options =
}
}
Flag "rcfile" str -> do
return options {
rcfile = Just str
}
Flag "enable" value ->
let cs = checkSpec options in return options {
checkSpec = cs {
@ -372,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'
Flag "format" _ -> return options
@ -389,11 +411,20 @@ parseOption flag options =
throwError SyntaxFailure
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 files = do
inputs <- mapM normalize files
cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing)
return SystemInterface {
return (newSystemInterface :: SystemInterface IO) {
siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache
@ -402,14 +433,14 @@ ioInterface options files = do
emptyCache :: Map.Map FilePath String
emptyCache = Map.empty
get cache inputs file = do
get cache inputs rcSuggestsExternal file = do
map <- readIORef cache
case Map.lookup file map of
Just x -> return $ Right x
Nothing -> fetch cache inputs file
Nothing -> fetch cache inputs rcSuggestsExternal file
fetch cache inputs file = do
ok <- allowable inputs file
fetch cache inputs rcSuggestsExternal file = do
ok <- allowable rcSuggestsExternal inputs file
if ok
then (do
(contents, shouldCache) <- inputFile file
@ -417,13 +448,16 @@ ioInterface options files = do
modifyIORef cache $ Map.insert file contents
return $ Right contents
) `catch` handler
else
if rcSuggestsExternal == Just False
then return $ Left (file ++ " was not specified as input, and external files were disabled via directive.")
else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where
handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex
allowable inputs x =
if externalSources options
allowable rcSuggestsExternal inputs x =
if fromMaybe (externalSources options) rcSuggestsExternal
then return True
else do
path <- normalize x
@ -435,8 +469,23 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path
-- 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
let dir = takeDirectory path
(previousPath, result) <- readIORef cache
@ -484,14 +533,20 @@ ioInterface options files = do
where
handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do
putStrLn $ file ++ ": " ++ show err
hPutStrLn stderr $ file ++ ": " ++ show err
return ("", True)
andM a b arg = do
first <- a arg
if not first then return False else b arg
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
findM p = foldr go (pure Nothing)
where
go x acc = do
b <- p x
if b then pure (Just x) else acc
findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original =
if isAbsolute original
then
let (_, relative) = splitDrive original
@ -500,11 +555,11 @@ ioInterface options files = do
find original original
where
find filename deflt = do
sources <- filterM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
[] -> return deflt
(first:_) -> return first
Nothing -> return deflt
Just first -> return first
scriptdir = dropFileName currentScript
adjustPath str =
case (splitDirectories str) of

View file

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

View file

@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-}
module ShellCheck.AST where
import GHC.Generics (Generic)
@ -37,354 +37,245 @@ newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
newtype Root = Root Token
data Token =
TA_Binary Id String Token Token
| TA_Assignment Id String Token Token
| TA_Variable Id String [Token]
| TA_Expansion Id [Token]
| TA_Sequence Id [Token]
| TA_Trinary Id Token Token Token
| TA_Unary Id String Token
| TC_And Id ConditionType String Token Token
| TC_Binary Id ConditionType String Token Token
| TC_Group Id ConditionType Token
| TC_Nullary Id ConditionType Token
| TC_Or Id ConditionType String Token Token
| TC_Unary Id ConditionType String Token
| TC_Empty Id ConditionType
| T_AND_IF Id
| T_AndIf Id Token Token
| T_Arithmetic Id Token
| T_Array Id [Token]
| T_IndexedElement Id [Token] Token
data Token = OuterToken Id (InnerToken Token) deriving (Show)
data InnerToken t =
Inner_TA_Binary String t t
| Inner_TA_Assignment String t t
| Inner_TA_Variable String [t]
| Inner_TA_Expansion [t]
| Inner_TA_Sequence [t]
| Inner_TA_Parenthesis t
| Inner_TA_Trinary t t t
| Inner_TA_Unary String t
| Inner_TC_And ConditionType String t t
| Inner_TC_Binary ConditionType String t t
| Inner_TC_Group ConditionType t
| Inner_TC_Nullary ConditionType t
| Inner_TC_Or ConditionType String t t
| Inner_TC_Unary ConditionType String t
| Inner_TC_Empty ConditionType
| Inner_T_AND_IF
| Inner_T_AndIf t t
| Inner_T_Arithmetic t
| Inner_T_Array [t]
| Inner_T_IndexedElement [t] t
-- Store the index as string, and parse as arithmetic or string later
| T_UnparsedIndex Id SourcePos String
| T_Assignment Id AssignmentMode String [Token] Token
| T_Backgrounded Id Token
| T_Backticked Id [Token]
| T_Bang Id
| T_Banged Id Token
| T_BraceExpansion Id [Token]
| T_BraceGroup Id [Token]
| T_CLOBBER Id
| T_Case Id
| T_CaseExpression Id Token [(CaseType, [Token], [Token])]
| T_Condition Id ConditionType Token
| T_DGREAT Id
| T_DLESS Id
| T_DLESSDASH Id
| T_DSEMI Id
| T_Do Id
| T_DollarArithmetic Id Token
| T_DollarBraced Id Bool Token
| T_DollarBracket Id Token
| T_DollarDoubleQuoted Id [Token]
| T_DollarExpansion Id [Token]
| T_DollarSingleQuoted Id String
| T_DollarBraceCommandExpansion Id [Token]
| T_Done Id
| T_DoubleQuoted Id [Token]
| T_EOF Id
| T_Elif Id
| T_Else Id
| T_Esac Id
| T_Extglob Id String [Token]
| T_FdRedirect Id String Token
| T_Fi Id
| T_For Id
| T_ForArithmetic Id Token Token Token [Token]
| T_ForIn Id String [Token] [Token]
| T_Function Id FunctionKeyword FunctionParentheses String Token
| T_GREATAND Id
| T_Glob Id String
| T_Greater Id
| T_HereDoc Id Dashed Quoted String [Token]
| T_HereString Id Token
| T_If Id
| T_IfExpression Id [([Token],[Token])] [Token]
| T_In Id
| T_IoFile Id Token Token
| T_IoDuplicate Id Token String
| T_LESSAND Id
| T_LESSGREAT Id
| T_Lbrace Id
| T_Less Id
| T_Literal Id String
| T_Lparen Id
| T_NEWLINE Id
| T_NormalWord Id [Token]
| T_OR_IF Id
| T_OrIf Id Token Token
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token]
| T_Rbrace Id
| T_Redirecting Id [Token] Token
| T_Rparen Id
| T_Script Id Token [Token] -- Shebang T_Literal, followed by script.
| T_Select Id
| T_SelectIn Id String [Token] [Token]
| T_Semi Id
| T_SimpleCommand Id [Token] [Token]
| T_SingleQuoted Id String
| T_Subshell Id [Token]
| T_Then Id
| T_Until Id
| T_UntilExpression Id [Token] [Token]
| T_While Id
| T_WhileExpression Id [Token] [Token]
| T_Annotation Id [Annotation] Token
| T_Pipe Id String
| T_CoProc Id (Maybe String) Token
| T_CoProcBody Id Token
| T_Include Id Token
| T_SourceCommand Id Token Token
| T_BatsTest Id Token Token
deriving (Show)
| Inner_T_UnparsedIndex SourcePos String
| Inner_T_Assignment AssignmentMode String [t] t
| Inner_T_Backgrounded t
| Inner_T_Backticked [t]
| Inner_T_Bang
| Inner_T_Banged t
| Inner_T_BraceExpansion [t]
| Inner_T_BraceGroup [t]
| Inner_T_CLOBBER
| Inner_T_Case
| Inner_T_CaseExpression t [(CaseType, [t], [t])]
| Inner_T_Condition ConditionType t
| Inner_T_DGREAT
| Inner_T_DLESS
| Inner_T_DLESSDASH
| Inner_T_DSEMI
| Inner_T_Do
| Inner_T_DollarArithmetic t
| Inner_T_DollarBraced Bool t
| Inner_T_DollarBracket t
| Inner_T_DollarDoubleQuoted [t]
| Inner_T_DollarExpansion [t]
| Inner_T_DollarSingleQuoted String
| Inner_T_DollarBraceCommandExpansion [t]
| Inner_T_Done
| Inner_T_DoubleQuoted [t]
| Inner_T_EOF
| Inner_T_Elif
| Inner_T_Else
| Inner_T_Esac
| Inner_T_Extglob String [t]
| Inner_T_FdRedirect String t
| Inner_T_Fi
| Inner_T_For
| Inner_T_ForArithmetic t t t [t]
| Inner_T_ForIn String [t] [t]
| Inner_T_Function FunctionKeyword FunctionParentheses String t
| Inner_T_GREATAND
| Inner_T_Glob String
| Inner_T_Greater
| Inner_T_HereDoc Dashed Quoted String [t]
| Inner_T_HereString t
| Inner_T_If
| Inner_T_IfExpression [([t],[t])] [t]
| Inner_T_In
| Inner_T_IoFile t t
| Inner_T_IoDuplicate t String
| Inner_T_LESSAND
| Inner_T_LESSGREAT
| Inner_T_Lbrace
| Inner_T_Less
| Inner_T_Literal String
| Inner_T_Lparen
| Inner_T_NEWLINE
| Inner_T_NormalWord [t]
| Inner_T_OR_IF
| Inner_T_OrIf t t
| Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
| Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands]
| Inner_T_ProcSub String [t]
| Inner_T_Rbrace
| Inner_T_Redirecting [t] t
| Inner_T_Rparen
| Inner_T_Script t [t] -- Shebang T_Literal, followed by script.
| Inner_T_Select
| Inner_T_SelectIn String [t] [t]
| Inner_T_Semi
| Inner_T_SimpleCommand [t] [t]
| Inner_T_SingleQuoted String
| Inner_T_Subshell [t]
| Inner_T_Then
| Inner_T_Until
| Inner_T_UntilExpression [t] [t]
| Inner_T_While
| Inner_T_WhileExpression [t] [t]
| Inner_T_Annotation [Annotation] t
| Inner_T_Pipe String
| Inner_T_CoProc (Maybe Token) t
| Inner_T_CoProcBody t
| Inner_T_Include t
| Inner_T_SourceCommand t t
| Inner_T_BatsTest String t
deriving (Show, Eq, Functor, Foldable, Traversable)
data Annotation =
DisableComment Integer
DisableComment Integer Integer -- [from, to)
| EnableComment String
| SourceOverride String
| ShellOverride String
| SourcePath String
| ExternalSources Bool
| ExtendedAnalysis Bool
deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
-- This is an abomination.
tokenEquals :: Token -> Token -> Bool
tokenEquals a b = kludge a == kludge b
where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"
pattern T_AND_IF id = OuterToken id Inner_T_AND_IF
pattern T_Bang id = OuterToken id Inner_T_Bang
pattern T_Case id = OuterToken id Inner_T_Case
pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ)
pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER
pattern T_DGREAT id = OuterToken id Inner_T_DGREAT
pattern T_DLESS id = OuterToken id Inner_T_DLESS
pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH
pattern T_Do id = OuterToken id Inner_T_Do
pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str)
pattern T_Done id = OuterToken id Inner_T_Done
pattern T_DSEMI id = OuterToken id Inner_T_DSEMI
pattern T_Elif id = OuterToken id Inner_T_Elif
pattern T_Else id = OuterToken id Inner_T_Else
pattern T_EOF id = OuterToken id Inner_T_EOF
pattern T_Esac id = OuterToken id Inner_T_Esac
pattern T_Fi id = OuterToken id Inner_T_Fi
pattern T_For id = OuterToken id Inner_T_For
pattern T_Glob id str = OuterToken id (Inner_T_Glob str)
pattern T_GREATAND id = OuterToken id Inner_T_GREATAND
pattern T_Greater id = OuterToken id Inner_T_Greater
pattern T_If id = OuterToken id Inner_T_If
pattern T_In id = OuterToken id Inner_T_In
pattern T_Lbrace id = OuterToken id Inner_T_Lbrace
pattern T_Less id = OuterToken id Inner_T_Less
pattern T_LESSAND id = OuterToken id Inner_T_LESSAND
pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT
pattern T_Literal id str = OuterToken id (Inner_T_Literal str)
pattern T_Lparen id = OuterToken id Inner_T_Lparen
pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE
pattern T_OR_IF id = OuterToken id Inner_T_OR_IF
pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str)
pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str)
pattern T_Rbrace id = OuterToken id Inner_T_Rbrace
pattern T_Rparen id = OuterToken id Inner_T_Rparen
pattern T_Select id = OuterToken id Inner_T_Select
pattern T_Semi id = OuterToken id Inner_T_Semi
pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str)
pattern T_Then id = OuterToken id Inner_T_Then
pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str)
pattern T_Until id = OuterToken id Inner_T_Until
pattern T_While id = OuterToken id Inner_T_While
pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2)
pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2)
pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t)
pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u)
pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t)
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t)
pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l)
pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list)
pattern T_Banged id l = OuterToken id (Inner_T_Banged l)
pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t)
pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list)
pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l)
pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2)
pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases)
pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs)
pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token)
pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token)
pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token)
pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t)
pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)
pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)
pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)
pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)
pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)
pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)
pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)
pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)
pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list)
pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list)
pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l)
pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t)
pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group)
pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l)
pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body)
pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l)
pattern T_HereString id word = OuterToken id (Inner_T_HereString word)
pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses)
pattern T_Include id script = OuterToken id (Inner_T_Include script)
pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t)
pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num)
pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file)
pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list)
pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u)
pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2)
pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l)
pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd)
pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list)
pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l)
pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds)
pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include)
pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_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
(==) = tokenEquals
OuterToken _ a == OuterToken _ b = a == b
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
analyze f g i =
round
where
round t = do
round t@(OuterToken id it) = do
f t
newT <- delve t
newIt <- traverse round it
g t
i newT
roundAll = mapM round
dl l v = do
x <- roundAll l
return $ v x
dll l m v = do
x <- roundAll l
y <- roundAll m
return $ v x y
d1 t v = do
x <- round t
return $ v x
d2 t1 t2 v = do
x <- round t1
y <- round t2
return $ v x y
delve (T_NormalWord id list) = dl list $ T_NormalWord id
delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
delve (T_DollarBraceCommandExpansion id list) = dl list $ T_DollarBraceCommandExpansion id
delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id
delve (T_Backticked id list) = dl list $ T_Backticked id
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
delve (T_HereString id word) = d1 word $ T_HereString id
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
delve (T_Assignment id mode var indices value) = do
a <- roundAll indices
b <- round value
return $ T_Assignment id mode var a b
delve (T_Array id t) = dl t $ T_Array id
delve (T_IndexedElement id indices t) = do
a <- roundAll indices
b <- round t
return $ T_IndexedElement id a b
delve (T_Redirecting id redirs cmd) = do
a <- roundAll redirs
b <- round cmd
return $ T_Redirecting id a b
delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id
delve (T_Banged id l) = d1 l $ T_Banged id
delve (T_AndIf id t u) = d2 t u $ T_AndIf id
delve (T_OrIf id t u) = d2 t u $ T_OrIf id
delve (T_Backgrounded id l) = d1 l $ T_Backgrounded id
delve (T_Subshell id l) = dl l $ T_Subshell id
delve (T_ProcSub id typ l) = dl l $ T_ProcSub id typ
delve (T_Arithmetic id c) = d1 c $ T_Arithmetic id
delve (T_IfExpression id conditions elses) = do
newConds <- mapM (\(c, t) -> do
x <- mapM round c
y <- mapM round t
return (x,y)
) conditions
newElses <- roundAll elses
return $ T_IfExpression id newConds newElses
delve (T_BraceGroup id l) = dl l $ T_BraceGroup id
delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id
delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id
delve (T_ForIn id v w l) = dll w l $ T_ForIn id v
delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
delve (T_CaseExpression id word cases) = do
newWord <- round word
newCases <- mapM (\(o, c, t) -> do
x <- mapM round c
y <- mapM round t
return (o, x,y)
) cases
return $ T_CaseExpression id newWord newCases
delve (T_ForArithmetic id a b c group) = do
x <- round a
y <- round b
z <- round c
list <- mapM round group
return $ T_ForArithmetic id x y z list
delve (T_Script id s l) = dl l $ T_Script id s
delve (T_Function id a b name body) = d1 body $ T_Function id a b name
delve (T_Condition id typ token) = d1 token $ T_Condition id typ
delve (T_Extglob id str l) = dl l $ T_Extglob id str
delve (T_DollarBraced id braced op) = d1 op $ T_DollarBraced id braced
delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
delve (TA_Sequence id l) = dl l $ TA_Sequence id
delve (TA_Trinary id t1 t2 t3) = do
a <- round t1
b <- round t2
c <- round t3
return $ TA_Trinary id a b c
delve (TA_Expansion id t) = dl t $ TA_Expansion id
delve (TA_Variable id str t) = dl t $ TA_Variable id str
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
delve (T_Include id script) = d1 script $ T_Include id
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id
delve t = return t
i (OuterToken id newIt)
getId :: Token -> Id
getId t = case t of
T_AND_IF id -> id
T_OR_IF id -> id
T_DSEMI id -> id
T_Semi id -> id
T_DLESS id -> id
T_DGREAT id -> id
T_LESSAND id -> id
T_GREATAND id -> id
T_LESSGREAT id -> id
T_DLESSDASH id -> id
T_CLOBBER id -> id
T_If id -> id
T_Then id -> id
T_Else id -> id
T_Elif id -> id
T_Fi id -> id
T_Do id -> id
T_Done id -> id
T_Case id -> id
T_Esac id -> id
T_While id -> id
T_Until id -> id
T_For id -> id
T_Select id -> id
T_Lbrace id -> id
T_Rbrace id -> id
T_Lparen id -> id
T_Rparen id -> id
T_Bang id -> id
T_In id -> id
T_NEWLINE id -> id
T_EOF id -> id
T_Less id -> id
T_Greater id -> id
T_SingleQuoted id _ -> id
T_Literal id _ -> id
T_NormalWord id _ -> id
T_DoubleQuoted id _ -> id
T_DollarExpansion id _ -> id
T_DollarBraced id _ _ -> id
T_DollarArithmetic id _ -> id
T_BraceExpansion id _ -> id
T_ParamSubSpecialChar id _ -> id
T_DollarBraceCommandExpansion id _ -> id
T_IoFile id _ _ -> id
T_IoDuplicate id _ _ -> id
T_HereDoc id _ _ _ _ -> id
T_HereString id _ -> id
T_FdRedirect id _ _ -> id
T_Assignment id _ _ _ _ -> id
T_Array id _ -> id
T_IndexedElement id _ _ -> id
T_Redirecting id _ _ -> id
T_SimpleCommand id _ _ -> id
T_Pipeline id _ _ -> id
T_Banged id _ -> id
T_AndIf id _ _ -> id
T_OrIf id _ _ -> id
T_Backgrounded id _ -> id
T_IfExpression id _ _ -> id
T_Subshell id _ -> id
T_BraceGroup id _ -> id
T_WhileExpression id _ _ -> id
T_UntilExpression id _ _ -> id
T_ForIn id _ _ _ -> id
T_SelectIn id _ _ _ -> id
T_CaseExpression id _ _ -> id
T_Function id _ _ _ _ -> id
T_Arithmetic id _ -> id
T_Script id _ _ -> id
T_Condition id _ _ -> id
T_Extglob id _ _ -> id
T_Backticked id _ -> id
TC_And id _ _ _ _ -> id
TC_Or id _ _ _ _ -> id
TC_Group id _ _ -> id
TC_Binary id _ _ _ _ -> id
TC_Unary id _ _ _ -> id
TC_Nullary id _ _ -> id
TA_Binary id _ _ _ -> id
TA_Assignment id _ _ _ -> id
TA_Unary id _ _ -> id
TA_Sequence id _ -> id
TA_Trinary id _ _ _ -> id
TA_Expansion id _ -> id
T_ProcSub id _ _ -> id
T_Glob id _ -> id
T_ForArithmetic id _ _ _ _ -> id
T_DollarSingleQuoted id _ -> id
T_DollarDoubleQuoted id _ -> id
T_DollarBracket id _ -> id
T_Annotation id _ _ -> id
T_Pipe id _ -> id
T_CoProc id _ _ -> id
T_CoProcBody id _ -> id
T_Include id _ -> id
T_SourceCommand id _ _ -> id
T_UnparsedIndex id _ _ -> id
TC_Empty id _ -> id
TA_Variable id _ _ -> id
T_BatsTest id _ _ -> id
getId (OuterToken id _) = id
blank :: Monad m => Token -> m ()
blank = const $ return ()

View file

@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@ -17,16 +17,27 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.ASTLib where
import ShellCheck.AST
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Monad.Writer
import Control.Monad
import Data.Char
import Data.Functor
import Data.Functor.Identity
import Data.List
import Data.Maybe
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Numeric (showHex)
import Test.QuickCheck
arguments (T_SimpleCommand _ _ (cmd:args)) = args
-- Is this a type of loop?
isLoop t = case t of
@ -46,13 +57,32 @@ willSplit x =
T_BraceExpansion {} -> True
T_Glob {} -> True
T_Extglob {} -> True
T_DoubleQuoted _ l -> any willBecomeMultipleArgs l
T_NormalWord _ l -> any willSplit l
_ -> False
isGlob T_Extglob {} = True
isGlob T_Glob {} = True
isGlob (T_NormalWord _ l) = any isGlob l
isGlob _ = False
isGlob t = case t of
T_Extglob {} -> True
T_Glob {} -> True
T_NormalWord _ l -> any isGlob l || hasSplitRange l
_ -> False
where
-- foo[x${var}y] gets parsed as foo,[,x,$var,y],
-- so check if there's such an interval
hasSplitRange l =
let afterBracket = dropWhile (not . isHalfOpenRange) l
in any isClosingRange afterBracket
isHalfOpenRange t =
case t of
T_Literal _ "[" -> True
_ -> False
isClosingRange t =
case t of
T_Literal _ str -> ']' `elem` str
_ -> False
-- Is this shell word a constant?
isConstant token =
@ -110,7 +140,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args
flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"
-- Get all flags in a GNU way, up until --
getAllFlags :: Token -> [(Token, String)]
@ -128,30 +158,146 @@ isFlag token =
_ -> False
-- Is this token a flag where the - is unquoted?
isUnquotedFlag token = fromMaybe False $ do
str <- getLeadingUnquotedString token
return $ "-" `isPrefixOf` str
isUnquotedFlag token =
case getLeadingUnquotedString token of
Just ('-':_) -> True
_ -> False
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
-- -re -d : -u 3 bar
-- into
-- Just [("r", (-re, -re)), ("e", (-re, -re)), ("d", (-d,:)), ("u", (-u,3)), ("", (bar,bar))]
--
-- Each string flag maps to a tuple of (flag, argument), where argument=flag if it
-- doesn't take a specific one.
--
-- Any unrecognized flag will result in Nothing. The exception is if arbitraryLongOpts
-- is set, in which case --anything will map to "anything".
getGnuOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getGnuOpts str args = getOpts (True, False) str [] args
-- As above, except the first non-arg string will treat the rest as arguments
getBsdOpts :: String -> [Token] -> Maybe [(String, (Token, Token))]
getBsdOpts str args = getOpts (False, False) str [] args
-- Tests for this are in Commands.hs where it's more frequently used
getOpts ::
-- Behavioral config: gnu style, allow arbitrary long options
(Bool, Bool)
-- A getopts style string
-> String
-- List of long options and whether they take arguments
-> [(String, Bool)]
-- List of arguments (excluding command)
-> [Token]
-- List of flags to tuple of (optionToken, valueToken)
-> Maybe [(String, (Token, Token))]
getOpts (gnu, arbitraryLongOpts) string longopts args = process args
where
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = longopts
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process (token:rest) = do
case getLiteralStringDef "\0" token of
"--" -> return $ listToArgs rest
'-':'-':word -> do
let (name, arg) = span (/= '=') word
needsArg <-
if arbitraryLongOpts
then return $ Map.findWithDefault False name flagMap
else Map.lookup name flagMap
if needsArg && null arg
then
case rest of
(arg:rest2) -> do
more <- process rest2
return $ (name, (token, arg)) : more
_ -> fail "Missing arg"
else do
more <- process rest
-- Consider splitting up token to get arg
return $ (name, (token, token)) : more
'-':opts -> shortToOpts opts token rest
arg ->
if gnu
then do
more <- process rest
return $ ("", (token, token)):more
else return $ listToArgs (token:rest)
shortToOpts opts token args =
case opts of
c:rest -> do
needsArg <- Map.lookup [c] flagMap
case () of
_ | needsArg && null rest -> do
(next:restArgs) <- return args
more <- process restArgs
return $ ([c], (token, next)):more
_ | needsArg -> do
more <- process args
return $ ([c], (token, token)):more
_ -> do
more <- shortToOpts rest token args
return $ ([c], (token, token)):more
[] -> process args
listToArgs = map (\x -> ("", (x, x)))
-- Generic getOpts that doesn't rely on a format string, but may also be inaccurate.
-- This provides a best guess interpretation instead of failing when new options are added.
--
-- "--" is treated as end of arguments
-- "--anything[=foo]" is treated as a long option without argument
-- "-any" is treated as -a -n -y, with the next arg as an option to -y unless it starts with -
-- anything else is an argument
getGenericOpts :: [Token] -> [(String, (Token, Token))]
getGenericOpts = process
where
process (token:rest) =
case getLiteralStringDef "\0" token of
"--" -> map (\c -> ("", (c,c))) rest
'-':'-':word -> (takeWhile (`notElem` "\0=") word, (token, token)) : process rest
'-':optString ->
let opts = takeWhile (/= '\0') optString
in
case rest of
next:_ | "-" `isPrefixOf` getLiteralStringDef "\0" next ->
map (\c -> ([c], (token, token))) opts ++ process rest
next:remainder ->
case reverse opts of
last:initial ->
map (\c -> ([c], (token, token))) (reverse initial)
++ [([last], (token, next))]
++ process remainder
[] -> process remainder
[] -> map (\c -> ([c], (token, token))) opts
_ -> ("", (token, token)) : process rest
process [] = []
-- Given a T_DollarBraced, return a simplified version of the string contents.
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
-- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _ _) =
let string = bracedString t in
isArrayExpansion (T_DollarBraced _ _ l) =
let string = concat $ oversimplify l in
"@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
where
f t@(T_DollarBraced _ _ _) =
let string = bracedString t in
"!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
f quoted (T_DollarBraced _ _ l) =
let string = concat $ oversimplify l in
not quoted || "!" `isPrefixOf` string
f quoted (T_DoubleQuoted _ parts) = any (f True) parts
f quoted (T_NormalWord _ parts) = any (f quoted) parts
f _ _ = False
-- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t
@ -159,7 +305,6 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
f T_Extglob {} = True
f T_Glob {} = True
f T_BraceExpansion {} = True
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts
f _ = False
@ -175,9 +320,13 @@ willConcatInAssignment token =
getLiteralString :: Token -> Maybe String
getLiteralString = getLiteralStringExt (const Nothing)
-- Definitely get a literal string, with a given default for all non-literals
getLiteralStringDef :: String -> Token -> String
getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)
-- Definitely get a literal string, skipping over all non-literals
onlyLiteralString :: Token -> String
onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
onlyLiteralString = getLiteralStringDef ""
-- Maybe get a literal string, but only if it's an unquoted argument.
getUnquotedLiteral (T_NormalWord _ list) =
@ -187,6 +336,12 @@ getUnquotedLiteral (T_NormalWord _ list) =
str _ = Nothing
getUnquotedLiteral _ = Nothing
isQuotes t =
case t of
T_DoubleQuoted {} -> True
T_SingleQuoted {} -> True
_ -> False
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
-- or nothing if the word does not end in an unquoted literal.
getTrailingUnquotedLiteral :: Token -> Maybe Token
@ -205,8 +360,11 @@ getTrailingUnquotedLiteral t =
getLeadingUnquotedString :: Token -> Maybe String
getLeadingUnquotedString t =
case t of
T_NormalWord _ ((T_Literal _ s) : _) -> return s
T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest
_ -> Nothing
where
from ((T_Literal _ s):rest) = s ++ from rest
from _ = ""
-- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f
@ -214,9 +372,24 @@ getGlobOrLiteralString = getLiteralStringExt f
f (T_Glob _ str) = return str
f _ = Nothing
prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1"
prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz"
prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1"
prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y"
prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy"
prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x"
prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x"
prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x"
prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x"
prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4"
prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1"
prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12"
prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123"
-- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings.
getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
getLiteralStringExt more = g
where
allInList = fmap concat . mapM g
@ -246,14 +419,15 @@ getLiteralStringExt more = g
'\\' -> '\\' : rest
'x' ->
case cs of
(x:y:more) ->
if isHexDigit x && isHexDigit y
then chr (16*(digitToInt x) + (digitToInt y)) : rest
else '\\':c:rest
(x:y:more) | isHexDigit x && isHexDigit y ->
chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
(x:more) | isHexDigit x ->
chr (digitToInt x) : decodeEscapes more
more -> '\\' : 'x' : decodeEscapes more
_ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs)
num = parseOct digits
in (if num < 256 then chr num else '?') : rest
let (digits, more) = spanMax isOctDigit 3 (c:cs)
num = (parseOct digits) `mod` 256
in (chr num) : decodeEscapes more
_ -> '\\' : c : rest
where
rest = decodeEscapes cs
@ -261,12 +435,54 @@ getLiteralStringExt more = g
where
f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest
spanMax f n list =
let (first, second) = span f list
(prefix, suffix) = splitAt n first
in
(prefix, suffix ++ second)
decodeEscapes (c:cs) = c : decodeEscapes cs
decodeEscapes [] = []
-- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t
-- Is this token a string literal number?
isLiteralNumber t = fromMaybe False $ do
s <- getLiteralString t
guard $ all isDigit s
return True
-- Escape user data for messages.
-- Messages generally avoid repeating user data, but sometimes it's helpful.
e4m = escapeForMessage
escapeForMessage :: String -> String
escapeForMessage str = concatMap f str
where
f '\\' = "\\\\"
f '\n' = "\\n"
f '\r' = "\\r"
f '\t' = "\\t"
f '\x1B' = "\\e"
f c =
if shouldEscape c
then
if ord c < 256
then "\\x" ++ (pad0 2 $ toHex c)
else "\\U" ++ (pad0 4 $ toHex c)
else [c]
shouldEscape c =
(not $ isPrint c)
|| (not (isAscii c) && not (isLetter c))
pad0 :: Int -> String -> String
pad0 n s =
let l = length s in
if l < n
then (replicate (n-l) '0') ++ s
else s
toHex :: Char -> String
toHex c = map toUpper $ showHex (ord c) ""
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
@ -295,25 +511,48 @@ getCommand t =
-- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken
getCommandName = fst . getCommandNameAndToken False
-- Maybe get the name+arguments of a command.
getCommandArgv t = do
(T_SimpleCommand _ _ args@(_:_)) <- getCommand t
return args
-- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken
getCommandTokenOrThis = snd . getCommandNameAndToken False
getCommandNameAndToken :: Token -> (Maybe String, Token)
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
-- Given a command, get the string and token that represents the command name.
-- If direct, return the actual command (e.g. exec in 'exec ls')
-- If not, return the logical command (e.g. 'ls' in 'exec ls')
getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token)
getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w
if "busybox" `isSuffixOf` s || "builtin" == s
then
case rest of
(applet:_) -> return (getLiteralString applet, applet)
_ -> return (Just s, w)
else
return (Just s, w)
return $ fromMaybe (Just s, w) $ do
guard $ not direct
actual <- getEffectiveCommandToken s cmd rest
return (getLiteralString actual, actual)
where
getEffectiveCommandToken str cmd args =
let
firstArg = do
arg <- listToMaybe args
guard . not $ isFlag arg
return arg
in
case str of
"busybox" -> firstArg
"builtin" -> firstArg
"command" -> firstArg
"run" -> firstArg -- Used by bats
"exec" -> do
opts <- getBsdOpts "cla:" args
(_, (t, _)) <- find (null . fst) opts
return t
_ -> fail ""
-- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date"
@ -330,7 +569,7 @@ getCommandNameFromExpansion t =
-- Get the basename of a token representing a command
getCommandBasename = fmap basename . getCommandName
where
basename = reverse . takeWhile (/= '/') . reverse
isAssignment t =
@ -362,19 +601,23 @@ isFunctionLike t =
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements.
-- the conditions and bodies of while loops or branches of if statements.
getCommandSequences :: Token -> [[Token]]
getCommandSequences t =
case t of
T_Script _ _ cmds -> [cmds]
T_BraceGroup _ cmds -> [cmds]
T_Subshell _ cmds -> [cmds]
T_WhileExpression _ _ cmds -> [cmds]
T_UntilExpression _ _ cmds -> [cmds]
T_WhileExpression _ cond cmds -> [cond, cmds]
T_UntilExpression _ cond cmds -> [cond, cmds]
T_ForIn _ _ _ cmds -> [cmds]
T_ForArithmetic _ _ _ _ cmds -> [cmds]
T_IfExpression _ thens elses -> map snd thens ++ [elses]
T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]
T_Annotation _ _ t -> getCommandSequences t
T_DollarExpansion _ cmds -> [cmds]
T_DollarBraceCommandExpansion _ cmds -> [cmds]
T_Backticked _ cmds -> [cmds]
_ -> []
-- Get a list of names of associative arrays
@ -382,13 +625,13 @@ getAssociativeArrays t =
nub . execWriter $ doAnalysis f t
where
f :: Token -> Writer [String] ()
f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
f t@T_SimpleCommand {} = sequence_ $ do
name <- getCommandName t
let assocNames = ["declare","local","typeset"]
guard $ elem name assocNames
guard $ name `elem` assocNames
let flags = getAllFlags t
guard $ elem "A" $ map snd flags
let args = map fst . filter ((==) "" . snd) $ flags
guard $ "A" `elem` map snd flags
let args = [arg | (arg, "") <- flags]
let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names
f _ = return ()
@ -406,38 +649,36 @@ data PseudoGlob = PGAny | PGMany | PGChar Char
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
-- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToPseudoGlob word =
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_DollarBraced {} -> return [PGMany]
T_DollarExpansion {} -> return [PGMany]
T_Backticked {} -> return [PGMany]
T_Glob _ "?" -> return [PGAny]
T_Glob _ ('[':_) -> return [PGAny]
T_Glob {} -> return [PGMany]
T_Extglob {} -> return [PGMany]
_ -> return [PGMany]
wordToPseudoGlob :: Token -> [PseudoGlob]
wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False
-- Turn a word into a PG pattern, but only if we can preserve
-- exact semantics.
wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToExactPseudoGlob word =
simplifyPseudoGlob . concat <$> mapM f (getWordParts word)
wordToExactPseudoGlob = wordToPseudoGlob' True
wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob]
wordToPseudoGlob' exact word =
simplifyPseudoGlob <$> toGlob word
where
toGlob :: Token -> Maybe [PseudoGlob]
toGlob word =
case word of
T_NormalWord _ (T_Literal _ ('~':str):rest) -> do
guard $ not exact
let this = (PGMany : (map PGChar $ dropWhile (/= '/') str))
tail <- concat <$> (mapM f $ concatMap getWordParts rest)
return $ this ++ tail
_ -> concat <$> (mapM f $ getWordParts word)
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_Glob _ "?" -> return [PGAny]
T_Glob _ "*" -> return [PGMany]
_ -> fail "Unknown token type"
T_Glob _ ('[':_) | not exact -> return [PGAny]
_ -> if exact then fail "" else return [PGMany]
-- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g
@ -487,8 +728,7 @@ pseudoGlobIsSuperSetof = matchable
matchable (PGMany : rest) [] = matchable rest []
matchable _ _ = False
wordsCanBeEqual x y = fromMaybe True $
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
-- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})?
@ -502,6 +742,11 @@ isCommandSubstitution t = case t of
T_Backticked {} -> True
_ -> False
-- Is this an expansion that results in a simple string?
isStringExpansion t = isCommandSubstitution t || case t of
T_DollarArithmetic {} -> True
T_DollarBraced {} -> not (isArrayExpansion t)
_ -> False
-- Is this a T_Annotation that ignores a specific code?
isAnnotationIgnoringCode code t =
@ -509,5 +754,173 @@ isAnnotationIgnoringCode code t =
T_Annotation _ anns _ -> any hasNum anns
_ -> False
where
hasNum (DisableComment ts) = code == ts
hasNum (DisableComment from to) = code >= from && code < to
hasNum _ = False
prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
-- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
re = mkRegex "/env +(-S|--split-string=?)? *(.*)"
shellFor s | s `matches` re =
case matchRegex re s of
Just [flag, shell] -> fromEnvArgs (words shell)
_ -> ""
shellFor sb =
case words sb of
[] -> ""
[x] -> basename x
(first:second:args) | basename first == "busybox" ->
case basename second of
"sh" -> "busybox sh"
"ash" -> "busybox ash"
x -> x
(first:args) | basename first == "env" ->
fromEnvArgs args
(first:_) -> basename first
fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
-- Determining if a name is a variable
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
isSpecialVariableChar = (`elem` "*@#?-$!")
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10 = getBracedReference "foo: -1" == "foo"
prop_getBracedReference11 = getBracedReference "!os*" == ""
prop_getBracedReference11b = getBracedReference "!os@" == ""
prop_getBracedReference12 = getBracedReference "!os?bar**" == ""
prop_getBracedReference13 = getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) | c `elem` "!#" = rest
dropPrefix cs = cs
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) | isSpecialVariableChar c = return [c]
getSpecial _ = fail "empty or not special"
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
guard $ isVariableChar next -- e.g. ${!@}
first <- find (not . isVariableChar) rest
guard $ first `elem` "*?@"
return ""
nameExpansion _ = Nothing
-- Get the variable modifier like /a/b in ${var/a/b}
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
getBracedModifier s = headOrDefault "" $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
getIndexReferences s = fromMaybe [] $ do
index:_ <- matchRegex re s
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
_:offsets:_ <- matchRegex re mods
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
-- Returns whether a token is a parameter expansion without any modifiers.
-- True for $var ${var} $1 $#
-- False for ${#var} ${var[x]} ${var:-0}
isUnmodifiedParameterExpansion t =
case t of
T_DollarBraced _ False _ -> True
T_DollarBraced _ _ list ->
let str = concat $ oversimplify list
in getBracedReference str == str
_ -> False
-- 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.
getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
getEnableDirectives root =
case root of
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 []
runTests = $quickCheckAll

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@ -25,6 +25,7 @@ import ShellCheck.Interface
import Data.List
import Data.Monoid
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
@ -34,18 +35,21 @@ analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runAnalytics spec
++ runChecker params (checkers params)
runChecker params (checkers spec params)
}
where
params = makeParameters spec
checkers params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker,
checkers spec params = mconcat $ map ($ params) [
ShellCheck.Analytics.checker spec,
ShellCheck.Checks.Commands.checker spec,
ShellCheck.Checks.ControlFlow.checker spec,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker
]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks
ShellCheck.Analytics.optionalChecks,
ShellCheck.Checks.Commands.optionalChecks,
ShellCheck.Checks.ControlFlow.optionalChecks
]

View file

@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@ -23,13 +23,16 @@ module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import qualified ShellCheck.CFGAnalysis as CF
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Arrow (first)
import Control.DeepSeq
import Control.Monad
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.State
@ -38,6 +41,7 @@ import Data.Char
import Data.List
import Data.Maybe
import Data.Semigroup
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
@ -79,10 +83,18 @@ composeAnalyzers f g x = f x >> g x
data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool,
-- Whether this script has the 'inherit_errexit' option set/default.
hasInheritErrexit :: Bool,
-- Whether this script has 'set -e' anywhere.
hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- Whether this script has 'shopt -s execfail' anywhere.
hasExecfail :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- A map from Id to Token
idMap :: Map.Map Id Token,
-- A map from Id to parent Token
parentMap :: Map.Map Id Token,
-- The shell type, such as Bash or Ksh
@ -92,7 +104,9 @@ data Parameters = Parameters {
-- The root node of the AST
rootNode :: Token,
-- 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)
cfgAnalysis :: Maybe CF.CFGAnalysis
} deriving (Show)
-- TODO: Cache results of common AST ops here
@ -142,7 +156,7 @@ producesComments c s = do
prRoot pr
let spec = defaultSpec pr
let params = makeParameters spec
return . not . null $ runChecker params c
return . not . null $ filterByAnnotation spec params $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
@ -163,8 +177,12 @@ err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str
errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
errWithFix = addCommentWithFix ErrorC
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
warnWithFix = addCommentWithFix WarningC
infoWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
infoWithFix = addCommentWithFix InfoC
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
styleWithFix = addCommentWithFix StyleC
@ -176,28 +194,58 @@ makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str
withFix = comment {
tcFix = Just fix
-- If fix is empty, pretend it wasn't there.
tcFix = if null (fixReplacements fix) then Nothing else Just fix
}
in withFix `deepseq` withFix
in force withFix
makeParameters spec =
let params = Parameters {
-- makeParameters :: CheckSpec -> Parameters
makeParameters spec = params
where
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
hasSetE = containsSetE root,
hasLastpipe =
case shellType params of
Bash -> containsLastpipe root
Bash -> isOptionSet "lastpipe" root
Dash -> False
BusyboxSh -> False
Sh -> False
Ksh -> True,
hasInheritErrexit =
case shellType params of
Bash -> isOptionSet "inherit_errexit" root
Dash -> True
BusyboxSh -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> isOptionSet "pipefail" root
Dash -> True
BusyboxSh -> isOptionSet "pipefail" root
Sh -> True
Ksh -> isOptionSet "pipefail" root,
hasExecfail =
case shellType params of
Bash -> isOptionSet "execfail" root
_ -> False,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
idMap = getTokenMap root,
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec
} in params
where root = asScript spec
tokenPositions = asTokenPositions spec,
cfgAnalysis = do
guard extendedAnalysis
return $ CF.analyzeControlFlow cfParams root
}
cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params,
CF.cfPipefail = hasPipefail params
}
root = asScript spec
-- Does this script mention 'set -e' anywhere?
@ -214,18 +262,30 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack.
containsLastpipe root =
containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root
where
isPipefail t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
(opt `elem` oversimplify t ||
"o" `elem` map snd (getAllFlags t))
_ -> False
containsShopt shopt root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where
isShoptLastPipe t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" &&
("lastpipe" `elem` oversimplify t)
(shopt `elem` oversimplify t)
_ -> False
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
@ -236,40 +296,28 @@ prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
determineShell fallbackShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
determineShell fallbackShell t = fromMaybe Bash $
shellForExecutable shellString `mplus` fallbackShell
where
forAnnotation t =
case t of
(ShellOverride s) -> return s
_ -> fail ""
getCandidates :: Token -> [Maybe String]
getCandidates t@T_Script {} = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++
[Just $ fromShebang s]
shellString = getCandidate t
getCandidate :: Token -> String
getCandidate t@T_Script {} = fromShebang t
getCandidate (T_Annotation _ annotations s) =
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash"
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
-- Given a root node, make a map from Id to parent Token.
-- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token
getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
snd $ execState (doStackAnalysis pre post t) ([], Map.empty)
where
pre t = modify (first ((:) t))
post t = do
@ -297,16 +345,16 @@ isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
isQuoteFreeNode strict shell tree t =
isQuoteFreeElement t ||
(fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t)
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment {} -> return True
T_FdRedirect {} -> return True
_ -> Nothing
T_Assignment id _ _ _ _ -> assignmentIsQuoting id
T_FdRedirect {} -> True
_ -> False
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
@ -316,7 +364,7 @@ isQuoteFreeNode strict tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id
T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
@ -328,6 +376,18 @@ isQuoteFreeNode strict tree t =
T_SelectIn {} -> return (not strict)
_ -> Nothing
-- Check whether this assignment is self-quoting due to being a recognized
-- assignment passed to a Declaration Utility. This will soon be required
-- by POSIX: https://austingroupbugs.net/view.php?id=351
assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
shellParsesParamsAsAssignments = shell /= Sh
-- Is this assignment a parameter to a command like export/typeset/etc?
isAssignmentParamToCommand id =
case Map.lookup id tree of
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
_ -> False
-- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
@ -349,7 +409,7 @@ isParamTo tree cmd =
-- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
getClosestCommand tree t =
findFirst findCommand $ getPath tree t
findFirst findCommand $ NE.toList $ getPath tree t
where
findCommand t =
case t of
@ -359,48 +419,43 @@ getClosestCommand tree t =
-- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do
tree <- asks parentMap
return $ getClosestCommand tree t
params <- ask
return $ getClosestCommand (parentMap params) t
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
where
go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_)
| currentId == getId word = True
go currentId (t@(T_SimpleCommand _ _ (word:_)):_) =
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
go _ _ = False
-- A list of the element and all its parents up to the root node.
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
-- Version of the above taking the map from the current context
-- Todo: give this the name "getPath"
getPathM t = do
map <- asks parentMap
return $ getPath map t
params <- ask
return $ getPath (parentMap params) t
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)
-- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
findFirst p l =
case l of
[] -> Nothing
(x:xs) ->
findFirst p = foldr go Nothing
where
go x acc =
case p x of
Just True -> return x
Just False -> Nothing
Nothing -> findFirst p xs
Nothing -> acc
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
@ -415,8 +470,7 @@ tokenIsJustCommandOutput t = case t of
-- TODO: Replace this with a proper Control Flow Graph
getVariableFlow params t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack
reverse $ execState (doStackAnalysis startScope endScope t) []
where
startScope t =
let scopeType = leadType params t
@ -454,7 +508,7 @@ leadType params t =
T_BatsTest {} -> SubshellScope "@bats test"
T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} ->
if fromMaybe False causesSubshell
if causesSubshell == Just True
then SubshellScope "pipeline"
else NoneScope
_ -> NoneScope
@ -467,91 +521,98 @@ leadType params t =
causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline
if length list <= 1
then return False
else if not $ hasLastpipe params
then return True
else return . not $ (getId . head $ reverse list) == getId t
return $ case list of
_:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t
_ -> False
getModifiedVariables t =
case t of
T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of
T_Assignment id _ name _ w ->
[(x, x, name, dataTypeFrom DataString w)]
_ -> []
) vars
c@T_SimpleCommand {} ->
getModifiedVariableCommand c
[(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars]
T_SimpleCommand {} ->
getModifiedVariableCommand t
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->
[(t, v, name, DataString SourceInteger)]
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString $ SourceFrom [rhs])
return (t, t, name, DataString SourceInteger)
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal)
(t, t, "output", DataString SourceExternal),
(t, t, "stderr", DataString SourceExternal),
(t, t, "stderr_lines", DataArray SourceExternal)
]
-- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> Nothing
guard . not . null $ str
str <- getVariableForTestDashV token
return (t, token, str, DataString SourceChecked)
TC_Unary _ _ "-n" token -> markAsChecked t token
TC_Unary _ _ "-z" token -> markAsChecked t token
TC_Nullary _ _ token -> markAsChecked t token
T_DollarBraced _ _ l -> maybeToList $ do
let string = bracedString t
let string = concat $ oversimplify l
let modifier = getBracedModifier string
guard $ ":=" `isPrefixOf` modifier
guard $ any (`isPrefixOf` modifier) ["=", ":="]
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
T_CoProc _ Nothing _ ->
[(t, t, "COPROC", DataArray SourceInteger)]
T_CoProc _ (Just token) _ -> do
name <- maybeToList $ getLiteralString token
[(t, t, name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> []
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
where
markAsChecked place token = mapMaybe (f place) $ getWordParts token
f place t = case t of
T_DollarBraced _ _ l ->
let str = getBracedReference $ concat $ oversimplify l in do
guard $ isVariableName str
return (place, t, str, DataString SourceChecked)
_ -> Nothing
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of
"declare" -> forDeclare
"typeset" -> forDeclare
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
"local" -> if "x" `elem` flags
then concatMap getReference rest
else []
"trap" ->
case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
_ -> []
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
_ -> []
where
forDeclare =
if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = []
@ -567,14 +628,20 @@ getReferencedVariableCommand _ = []
-- VariableName :: String, -- The variable name, i.e. foo
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
-- )
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"builtin" ->
getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
"read" ->
let params = map getLiteral rest
readArrayVars = getReadArrayVariables rest
in
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest)
in fromMaybe fallback $ do
parsed <- getGnuOpts flagsForRead rest
case lookup "a" parsed of
Just (_, var) -> (:[]) <$> getLiteralArray var
Nothing -> return $ catMaybes $
map (getLiteral . snd . snd) $ filter (null . fst) parsed
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
@ -585,8 +652,8 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
"export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> declaredVars
"declare" -> forDeclare
"typeset" -> forDeclare
"local" -> concatMap getModifierParamString rest
"readonly" ->
@ -598,6 +665,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest
"wait" -> maybeToList $ getWaitVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest
@ -610,14 +678,15 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
_ -> []
where
flags = map snd $ getAllFlags base
stripEquals s = let rest = dropWhile (/= '=') s in
if rest == "" then "" else tail rest
stripEquals s = drop 1 $ dropWhile (/= '=') s
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t
forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
declaredVars = concatMap (getModifierParam defaultType) rest
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
@ -642,7 +711,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getModifierParam _ _ = []
letParamToLiteral token =
if var == ""
if null var
then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
@ -656,36 +725,39 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
where
(varName, varType) = case elemIndex '[' var of
Just i -> (take i var, DataArray)
Nothing -> (var, DataString)
f (_:rest) = f rest
f [] = fail "not found"
getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list
getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list
getFlagAssignedVariable str dataSource maybeFlags = do
flags <- maybeFlags
(_, (flag, value)) <- find ((== str) . fst) flags
variableName <- getLiteralStringExt (const $ return "!") value
let (baseName, index) = span (/= '[') variableName
return (base, value, baseName, (if null index then DataString else DataArray) dataSource)
-- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. Here we cheat and
-- just get the last one, if it's a variable name.
getMapfileArray base arguments = do
lastArg <- listToMaybe (reverse arguments)
name <- getLiteralString lastArg
-- where only the first non-option one is used if any.
getMapfileArray base rest = parseArgs `mplus` fallback
where
parseArgs :: Maybe (Token, Token, String, DataType)
parseArgs = do
args <- getGnuOpts "d:n:O:s:u:C:c:t" rest
case [y | ("",(_,y)) <- args] of
[] ->
return (base, base, "MAPFILE", DataArray SourceExternal)
first:_ -> do
name <- getLiteralString first
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args =
map (getLiteralArray . snd)
(filter (isArrayFlag . fst) (zip args (tail args)))
isArrayFlag x = fromMaybe False $ do
str <- getLiteralString x
return $ case str of
'-':'-':_ -> False
'-':str -> 'a' `elem` str
_ -> False
return (base, first, name, DataArray SourceExternal)
-- If arg parsing fails (due to bad or new flags), get the last variable name
fallback :: Maybe (Token, Token, String, DataType)
fallback = do
(name, token) <- listToMaybe . mapMaybe f $ reverse rest
return (base, token, name, DataArray SourceExternal)
f arg = do
name <- getLiteralString arg
guard $ isVariableName name
return (name, arg)
-- get the FLAGS_ variable created by a shflags DEFINE_ call
getFlagVariable (n:v:_) = do
@ -695,28 +767,23 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
-- Given a NormalWord like foo or foo[$bar], get foo.
-- Primarily used to get references for [[ -v foo[bar] ]]
getVariableForTestDashV :: Token -> Maybe String
getVariableForTestDashV t = do
str <- takeWhile ('[' /=) <$> getLiteralStringExt toStr t
guard $ isVariableName str
return str
where
re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ]
match <- matchRegex re mods
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
-- foo[bar] gets parsed with [bar] as a glob, so undo that
toStr (T_Glob _ s) = return s
-- Turn foo[$x] into foo[\0] so that we can get the constant array name
-- in a non-constant expression (while filtering out foo$x[$y])
toStr _ = return "\0"
getReferencedVariables parents t =
case t of
T_DollarBraced id _ l -> let str = bracedString t in
T_DollarBraced id _ l -> let str = concat $ oversimplify l in
(t, t, getBracedReference str) :
map (\x -> (l, l, x)) (
getIndexReferences str
@ -731,7 +798,7 @@ getReferencedVariables parents t =
TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
if isDereferencingBinaryOp op
then concatMap (getIfReference t) [lhs, rhs]
else []
@ -741,7 +808,7 @@ getReferencedVariables parents t =
(t, t, "output")
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x
where
@ -758,20 +825,18 @@ getReferencedVariables parents t =
literalizer t = case t of
T_Glob _ s -> return s -- Also when parsed as globs
_ -> Nothing
_ -> []
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
guard . not $ null str
when (isDigit $ head str) $ fail "is a number"
str <- getVariableForTestDashV token
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
@ -783,8 +848,8 @@ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `
-- Compare a command to a literal. Like above, but checks full path.
isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $
fmap matcher (getCommandName token)
isCommandMatch token matcher = maybe False
matcher (getCommandName token)
-- Does this regex look like it was intended as a glob?
-- True: *foo*
@ -794,104 +859,23 @@ isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
getVariablesFromLiteral (getLiteralStringDef " " token)
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string
map head $ matchAllSubgroups variableRegex string
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) =
if c `elem` "*@#?-$!" then return [c] else fail "not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest
guard $ suffix /= rest -- e.g. ${!@}
first <- suffix !!! 0
guard $ first `elem` "*?"
return ""
nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
-- Useful generic functions.
-- Run an action in a Maybe (or do nothing).
-- Example:
-- potentially $ do
-- s <- getLiteralString cmd
-- guard $ s `elem` ["--recursive", "-r"]
-- return $ warn .. "Something something recursive"
potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ())
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Run a command if the shell is in the given list
whenShell l c = do
shell <- asks shellType
when (shell `elem` l ) c
params <- ask
when (shellType params `elem` l ) c
filterByAnnotation asSpec params =
@ -920,55 +904,41 @@ isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t =
case t of
T_DollarBraced _ _ _ ->
getBracedModifier (bracedString t) `matches` re
T_DollarBraced _ _ l ->
getBracedModifier (concat $ oversimplify l) `matches` re
_ -> False
where
re = mkRegex "(^|\\]):?\\+"
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
-- read -re -d : -u 3 bar
-- into
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
-- where flags with arguments map to arguments, while others map to themselves.
-- Any unrecognized flag will result in Nothing.
getGnuOpts = getOpts getAllFlags
getBsdOpts = getOpts getLeadingFlags
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
getOpts flagTokenizer string cmd = process flags
supportsArrays Bash = True
supportsArrays Ksh = True
supportsArrays _ = False
isTrueAssignmentSource c =
case c of
DataString SourceChecked -> False
DataString SourceDeclaration -> False
DataArray SourceChecked -> False
DataArray SourceDeclaration -> False
_ -> True
modifiesVariable params token name =
or $ map check flow
where
flags = flagTokenizer cmd
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = []
flagMap = Map.fromList $ ("", False) : flagList string
flow = getVariableFlow params token
check t =
case t of
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
_ -> False
process [] = return []
process [(token, flag)] = do
takesArg <- Map.lookup flag flagMap
guard $ not takesArg
return [(flag, token)]
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
takesArg <- Map.lookup flag1 flagMap
if takesArg
then do
guard $ flag2 == ""
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
Sh -> False
isTestCommand t =
case t of
T_Condition {} -> True
T_SimpleCommand {} -> t `isCommand` "test"
T_Redirecting _ _ t -> isTestCommand t
T_Annotation _ _ t -> isTestCommand t
T_Pipeline _ _ [t] -> isTestCommand t
_ -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

1316
src/ShellCheck/CFG.hs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@ -20,10 +20,12 @@
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Analyzer
import ShellCheck.ASTLib
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Analyzer
import Debug.Trace -- DO NOT SUBMIT
import Data.Either
import Data.Functor
import Data.List
@ -48,7 +50,7 @@ tokenToPosition startMap t = fromMaybe fail $ do
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
shellFromFilename filename = foldl mplus Nothing candidates
shellFromFilename filename = listToMaybe candidates
where
shellExtensions = [(".ksh", Ksh)
,(".bash", Bash)
@ -57,7 +59,7 @@ shellFromFilename filename = foldl mplus Nothing candidates
-- The `.sh` is too generic to determine the shell:
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
candidates =
map (\(ext,sh) -> if ext `isSuffixOf` filename then Just sh else Nothing) shellExtensions
[sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do
@ -85,12 +87,13 @@ checkScript sys spec = do
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions,
asOptionalChecks = csOptionalChecks spec
asExtendedAnalysis = csExtendedAnalysis spec,
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
} where as = newAnalysisSpec root
let analysisMessages =
fromMaybe [] $
maybe []
(arComments . analyzeScript . analysisSpec)
<$> prRoot result
$ prRoot result
let translator = tokenToPosition tokenPositions
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
@ -104,7 +107,7 @@ checkScript sys spec = do
code = cCode (pcComment pc)
severity = cSeverity (pcComment pc)
sortMessages = sortBy (comparing order)
sortMessages = sortOn order
order pc =
let pos = pcStartPos pc
comment = pcComment pc in
@ -156,6 +159,11 @@ checkWithIncludesAndSourcePath includes mapper = getErrors
siFindSource = mapper
}
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
(mockRcFile rc $ mockedSystemInterface includes) {
siFindSource = mapper
}
prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 =
@ -198,11 +206,11 @@ prop_optionDisablesBadShebang =
}
prop_annotationDisablesBadShebang =
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
prop_canParseDevNull =
[] == check "source /dev/null"
null $ check "source /dev/null"
prop_failsWhenNotSourcing =
[1091, 2154] == check "source lol; echo \"$bar\""
@ -218,7 +226,7 @@ prop_worksWhenDotting =
-- FIXME: This should really be giving [1093], "recursively sourced"
prop_noInfiniteSourcing =
[] == checkWithIncludes [("lib", "source lib")] "source lib"
null $ checkWithIncludes [("lib", "source lib")] "source lib"
prop_canSourceBadSyntax =
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
@ -229,9 +237,18 @@ prop_cantSourceDynamic =
prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canStripPrefixAndSource =
null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
prop_canStripPrefixAndSource2 =
null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_canRedirectWithSpaces =
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
@ -239,10 +256,10 @@ prop_recursiveParsing =
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
prop_nonRecursiveAnalysis =
[] == checkWithIncludes [("lib", "echo $1")] "source lib"
null $ checkWithIncludes [("lib", "echo $1")] "source lib"
prop_nonRecursiveParsing =
[] == checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
prop_sourceDirectiveDoesntFollowFile =
null $ checkWithIncludes
@ -270,7 +287,7 @@ prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
@ -288,6 +305,27 @@ prop_deducesTypeFromExtension2 = result == [2079]
csScript = "(( 3.14 ))"
}
prop_canDisableShebangWarning = null $ result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#shellcheck disable=SC2148\nfoo"
}
prop_canDisableAllWarnings = result == [2086]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`"
}
prop_canDisableParseErrors = null $ result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
}
prop_shExtensionDoesntMatter = result == [2148]
where
result = checkWithSpec [] emptyCheckSpec {
@ -328,7 +366,7 @@ prop_optionIncludes4 =
[2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
prop_readsRcFile = result == []
prop_readsRcFile = null result
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
@ -364,7 +402,7 @@ prop_canEnableOptionalsWithRc = result == [2244]
prop_sourcePathRedirectsName = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f "dir/myscript" _ _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript",
@ -373,22 +411,154 @@ prop_sourcePathRedirectsName = result == [2086]
prop_sourcePathAddsAnnotation = result == [2086]
where
f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathWorksWithSpaces = result == [2086]
where
f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathRedirectsDirective = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null"
f "dir/myscript" _ _ "lib" = return "foo/lib"
f _ _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanAllowExternalSources = result == [2086]
where
f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanDenyExternalSources = result == [2086]
where
f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanLeaveExternalSourcesUnspecified = result == [2086]
where
f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCanDisableExternalSources = result == [2006, 2086]
where
f "dir/myscript" (Just True) _ "withExternal" = return "withExternal"
f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec {
csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources = result == [1144]
where
f "dir/myscript" Nothing _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources2 = result == [1144]
where
f "dir/myscript" (Just False) _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanSuppressEarlyProblems1 = null result
where
result = checkWithRc "disable=1071" emptyCheckSpec {
csScript = "#!/bin/zsh\necho $1"
}
prop_rcCanSuppressEarlyProblems2 = null result
where
result = checkWithRc "disable=1104" emptyCheckSpec {
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 []
runTests = $quickCheckAll

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,101 @@
{-
Copyright 2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
-- Checks that run on the Control Flow Graph (as opposed to the AST)
-- This is scaffolding for a work in progress.
module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.CFG hiding (cfgAnalysis)
import ShellCheck.CFGAnalysis
import ShellCheck.AnalyzerLib
import ShellCheck.Data
import ShellCheck.Interface
import Control.Monad
import Control.Monad.Reader
import Data.Graph.Inductive.Graph
import qualified Data.Map as M
import qualified Data.Set as S
import Data.List
import Data.Maybe
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
optionalChecks :: [CheckDescription]
optionalChecks = []
-- A check that runs on the entire graph
type ControlFlowCheck = Analysis
-- A check invoked once per node, with its (pre,post) data
type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis
-- A check invoked once per effect, with its node's (pre,post) data
type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis
checker :: AnalysisSpec -> Parameters -> Checker
checker spec params = Checker {
perScript = const $ sequence_ controlFlowChecks,
perToken = const $ return ()
}
controlFlowChecks :: [ControlFlowCheck]
controlFlowChecks = [
runNodeChecks controlFlowNodeChecks
]
controlFlowNodeChecks :: [ControlFlowNodeCheck]
controlFlowNodeChecks = [
runEffectChecks controlFlowEffectChecks
]
controlFlowEffectChecks :: [ControlFlowEffectCheck]
controlFlowEffectChecks = [
]
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
runNodeChecks perNode = do
cfg <- asks cfgAnalysis
mapM_ runOnAll cfg
where
getData datas n@(node, label) = do
(pre, post) <- M.lookup node datas
return (n, (pre, post))
runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis
runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode
runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg)
runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck
runEffectChecks list = checkNode
where
checkNode (node, label) prepost =
case label of
CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -1,5 +1,5 @@
{-
Copyright 2012-2016 Vidar Holen
Copyright 2012-2020 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@ -19,17 +19,20 @@
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Monad
import Control.Monad.RWS
import Data.Char
import Data.Functor.Identity
import Data.List
import Data.Maybe
import qualified Data.Map as Map
@ -58,6 +61,9 @@ checks = [
,checkBraceExpansionVars
,checkMultiDimensionalArrays
,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
,checkNegatedUnaryOps
]
testChecker (ForShell _ t) =
@ -71,22 +77,24 @@ verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "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
f t@(TA_Expansion id _) = potentially $ do
str <- getLiteralString t
first <- str !!! 0
guard $ isDigit first && '.' `elem` str
f t@(TA_Expansion id _) = sequence_ $ do
first:rest <- getLiteralString t
guard $ isDigit first && '.' `elem` rest
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file"
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_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
@ -102,6 +110,7 @@ prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
prop_checkBashisms20 = verify checkBashisms "read -ra foo"
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
prop_checkBashisms21b = verify checkBashisms "test -a foo"
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
@ -133,6 +142,8 @@ prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
@ -177,110 +188,153 @@ prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m)
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
checkBashisms = ForShell [Sh, Dash] $ \t -> do
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
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
kludge params t
where
-- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism
where
isDash = shellType params == Dash
warnMsg id s =
isBusyboxSh = shellType params == BusyboxSh
isDash = shellType params == Dash || isBusyboxSh
warnMsg id code s =
if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
then err id code $ "In dash, " ++ s ++ " not supported."
else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
asStr = getLiteralString
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
bashism (T_DollarSingleQuoted id _) =
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
bashism (T_Condition id DoubleBracket _) =
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
bashism (TC_Binary id _ op _ _) =
checkTestOp bashismBinaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) =
checkTestOp bashismBinaryTestFlags op id
bashism (TC_Unary id _ op _) =
checkTestOp bashismUnaryTestFlags op id
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) =
checkTestOp bashismUnaryTestFlags op id
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef" ] =
unless isDash $ warnMsg id $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
unless isBusyboxSh $ warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
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 num _)
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) =
warnMsg id "+= is"
warnMsg id 3024 "+= is"
bashism (T_IoFile id _ word) | isNetworked =
warnMsg id "/dev/{tcp,udp} is"
warnMsg id 3025 "/dev/{tcp,udp} is"
where
file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id "^ in place of ! in glob bracket expressions is"
warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is"
warnMsg id 3028 $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do
mapM_ check expansion
unless isBusyboxSh $ mapM_ check simpleExpansions
mapM_ check advancedExpansions
when (isBashVariable var) $
warnMsg id $ var ++ " is"
warnMsg id 3028 $ var ++ " is"
where
str = bracedString t
str = concat $ oversimplify token
var = getBracedReference str
check (regex, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id feature
check (regex, code, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id code feature
bashism t@(T_Pipe id "|&") =
warnMsg id "|& in place of 2>&1 | is"
warnMsg id 3029 "|& in place of 2>&1 | is"
bashism (T_Array id _) =
warnMsg id "arrays are"
warnMsg id 3030 "arrays are"
bashism (T_IoFile id _ t) | isGlob t =
warnMsg id "redirecting to/from globs is"
warnMsg id 3031 "redirecting to/from globs is"
bashism (T_CoProc id _ _) =
warnMsg id "coproc is"
warnMsg id 3032 "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is"
warnMsg id 3034 "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is"
warnMsg id 3035 "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex =
if isDash
if isBusyboxSh
then
unless (argString `matches` busyboxFlagRegex) $
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
else if isDash
then
when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n"
warnMsg (getId arg) 3036 "echo flags besides -n"
else
warnMsg (getId arg) "echo flags are"
warnMsg (getId arg) 3037 "echo flags are"
where
argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$"
busyboxFlagRegex = mkRegex "^-[en]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are"
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) 3038 "exec flags are"
bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'let' is"
| t `isCommand` "let" = warnMsg id 3039 "'let' is"
bashism t@(T_SimpleCommand _ _ (cmd:args))
| t `isCommand` "set" = unless isDash $
checkOptions $ getLiteralArgs args
@ -288,16 +342,17 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
-- Get the literal options from a list of arguments,
-- up until the first non-literal one
getLiteralArgs :: [Token] -> [(Id, String)]
getLiteralArgs (first:rest) = fromMaybe [] $ do
str <- getLiteralString first
return $ (getId first, str) : getLiteralArgs rest
getLiteralArgs [] = []
getLiteralArgs = foldr go []
where
go first rest = case getLiteralString first of
Just str -> (getId first, str) : rest
Nothing -> []
-- Check a flag-option pair (such as -o errexit)
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
| flag' `matches` oFlagRegex = do
when (opt' `notElem` longOptions) $
warnMsg oid $ "set option " <> opt' <> " is"
warnMsg oid 3040 $ "set option " <> opt' <> " is"
checkFlags (flag:rest)
| otherwise = checkFlags (flag:opt:rest)
checkOptions (flag:rest) = checkFlags (flag:rest)
@ -310,10 +365,10 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
unless (flag' `matches` validFlagsRegex) $
forM_ (tail flag') $ \letter ->
when (letter `notElem` optionsSet) $
warnMsg fid $ "set flag " <> ('-':letter:" is")
warnMsg fid 3041 $ "set flag " <> ('-':letter:" is")
checkOptions rest
| beginsWithDoubleDash flag' = do
warnMsg fid $ "set flag " <> flag' <> " is"
warnMsg fid 3042 $ "set flag " <> flag' <> " is"
checkOptions rest
-- Either a word that doesn't start with a dash, or simply '--',
-- so stop checking.
@ -335,44 +390,51 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t
in do
when (name == "local" && not isDash) $
-- This is so commonly accepted that we'll make it a special case
warnMsg id 3043 $ "'local' is"
when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is"
potentially $ do
warnMsg id 3044 $ "'" ++ name ++ "' is"
sequence_ $ do
allowed' <- Map.lookup name allowedFlags
allowed <- allowed'
(word, flag) <- listToMaybe $
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
(word, flag) <- find
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is"
when (name == "source" && not isBusyboxSh) $
warnMsg id 3046 "'source' in place of '.' is"
when (name == "trap") $
let
check token = potentially $ do
check token = sequence_ $ do
str <- getLiteralString token
let upper = map toUpper str
return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token)
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
warnMsg (getId token) 3048
"prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $
warnMsg (getId token)
warnMsg (getId token) 3049
"using lower/mixed case for signal names is"
in
mapM_ check (drop 1 rest)
when (name == "printf") $ potentially $ do
when (name == "printf") $ sequence_ $ do
format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "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
unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset"
] ++ if not isDash then ["local"] else []
]
allowedFlags = Map.fromList [
("cd", Just ["L", "P"]),
("exec", Just []),
@ -380,38 +442,48 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
("jobs", Just ["l", "p"]),
("printf", Just []),
("read", Just $ if isDash then ["r", "p"] else ["r"]),
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
("readonly", Just ["p"]),
("trap", Just []),
("type", Just []),
("type", Just $ if isBusyboxSh then ["p"] else []),
("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]),
("unset", Just ["f", "v"]),
("wait", Just [])
]
bashism t@(T_SourceCommand id src _) =
let name = fromMaybe "" $ getCommandName src
in when (name == "source") $ warnMsg id "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix =
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
bashism t@(T_SourceCommand id src _)
| getCommandName src == Just "source" =
unless isBusyboxSh $
warnMsg id 3051 "'source' in place of '.' is"
bashism (TA_Expansion _ (T_Literal id str : _))
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
where
radix = mkRegex "^[0-9]+#"
bashism _ = return ()
varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
advancedExpansions = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
(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 $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
]
bashVars = [
-- This list deliberately excludes $BASH_VERSION as it's often used
-- for shell identification.
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS",
"_"
"_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE",
"BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS",
"FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "_" ]
@ -425,6 +497,50 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
Assignment (_, _, name, _) -> name == var
_ -> False
checkTestOp table op id = sequence_ $ do
(code, shells, msg) <- Map.lookup op table
guard . not $ shellType params `elem` shells
return $ warnMsg id code (msg op)
buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list
bashismBinaryTestFlags = buildTestFlagMap [
-- ([list of applicable flags],
-- (error code, exempt shells, message builder :: String -> String)),
--
-- Distinct error codes allow the wiki to give more helpful, targeted
-- information.
(["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="],
(3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")),
(["=="],
(3014, [BusyboxSh], \op -> op ++ " in place of = is")),
(["=~"],
(3015, [], \op -> op ++ " regex matching is")),
([], (0,[],const ""))
]
bashismUnaryTestFlags = buildTestFlagMap [
(["-v"],
(3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")),
(["-a"],
(3017, [], \op -> "unary " ++ op ++ " in place of -e is")),
(["-o"],
(3062, [], \op -> "test " ++ op ++ " to check options is")),
(["-R"],
(3063, [], \op -> "test " ++ op ++ " and namerefs in general are")),
(["-N"],
(3064, [], \op -> "test " ++ op ++ " is")),
(["-k"],
(3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-G"],
(3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
(["-O"],
(3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
([], (0,[],const ""))
]
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
@ -456,11 +572,10 @@ checkEchoSed = ForShell [Bash, Ksh] f
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = fromMaybe False $ do
[first,rest] <- matchRegex sedRe s
let delimiters = filter (== head first) rest
isSimpleSed s = isJust $ do
[h:_,rest] <- matchRegex sedRe s
let delimiters = filter (== h) rest
guard $ length delimiters == 2
return True
checkIn id s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
@ -487,11 +602,11 @@ checkBraceExpansionVars = ForShell [Bash] f
T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$"
otherwise -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t
_ -> return "-"
toString t = runIdentity $ getLiteralStringExt literalExt t
isEvaled t = do
cmd <- getClosestCommandM t
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
return $ maybe False (`isUnqualifiedCommand` "eval") cmd
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
@ -506,13 +621,13 @@ checkMultiDimensionalArrays = ForShell [Bash] f
case token of
T_Assignment _ _ name (first:second:_) _ -> about second
T_IndexedElement _ (first:second:_) _ -> about second
T_DollarBraced {} ->
when (isMultiDim token) $ about token
T_DollarBraced _ _ l ->
when (isMultiDim l) $ about token
_ -> return ()
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
@ -541,5 +656,46 @@ checkPS1Assignments = ForShell [Bash] f
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 []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View file

@ -2,33 +2,55 @@ module ShellCheck.Data where
import ShellCheck.Interface
import Data.Version (showVersion)
import Paths_ShellCheck (version)
{-
If you are here because you saw an error about Paths_ShellCheck in this file,
simply comment out the import below and define the version as a constant string.
Instead of:
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version
Use:
-- import Paths_ShellCheck (version)
shellcheckVersion = "kludge"
-}
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version -- VERSIONSTRING
internalVariables = [
-- Generic
"", "_", "rest", "REST",
-- Bash
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
"HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
"OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
"READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
"REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",
"BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",
"COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
"IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
"LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
"auto_resume", "histchars",
-- Other
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
@ -41,15 +63,23 @@ internalVariables = [
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return"
-- Bats
,"stderr", "stderr_lines"
]
specialVariablesWithoutSpaces = [
"$", "-", "?", "!", "#"
specialIntegerVariables = [
"$", "?", "!", "#"
]
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@ -95,10 +125,10 @@ commonCommands = [
nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
"cp", "du", "echo", "export", "fg", "fuser", "getconf",
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
"set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
]
sampleWords = [
@ -114,6 +144,10 @@ binaryTestOps = [
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
]
arithmeticBinaryTestOps = [
"-eq", "-ne", "-lt", "-le", "-gt", "-ge"
]
unaryTestOps = [
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
@ -126,9 +160,18 @@ shellForExecutable name =
"sh" -> return Sh
"bash" -> 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
"ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh
"ksh88" -> return Ksh
"ksh93" -> return Ksh
otherwise -> Nothing
"oksh" -> return Ksh
_ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:"
flagsForMapfile = "d:n:O:s:u:C:c:t"
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]

313
src/ShellCheck/Debug.hs Normal file
View file

@ -0,0 +1,313 @@
{-
This file contains useful functions for debugging and developing ShellCheck.
To invoke them interactively, run:
cabal repl
At the ghci prompt, enter:
:load ShellCheck.Debug
You can now invoke the functions. Here are some examples:
shellcheckString "echo $1"
stringToAst "(( x+1 ))"
stringToCfg "if foo; then bar; else baz; fi"
writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done"
The latter file can be rendered to png with GraphViz:
dot -Tpng /tmp/test.dot > /tmp/test.png
To run all unit tests in a module:
ShellCheck.Parser.runTests
ShellCheck.Analytics.runTests
To run a specific test:
:load ShellCheck.Analytics
prop_checkUuoc3
If you make code changes, reload in seconds at any time with:
:r
===========================================================================
Crash course in printf debugging in Haskell:
import Debug.Trace
greet 0 = return ()
-- Print when a function is invoked
greet n | trace ("calling greet " ++ show n) False = undefined
greet n = do
putStrLn "Enter name"
name <- getLine
-- Print at some point in any monadic function
traceM $ "user entered " ++ name
putStrLn $ "Hello " ++ name
-- Print a value before passing it on
greet $ traceShowId (n - 1)
===========================================================================
If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to
debug all of ShellCheck including I/O, you may see an error like this:
src/ShellCheck/Data.hs:5:1: error:
Could not load module Paths_ShellCheck
it is a hidden module in the package ShellCheck-0.8.0
This can easily be circumvented by running `./setgitversion` or manually
editing src/ShellCheck/Data.hs to replace the auto-deduced version number
with a constant string as indicated.
Afterwards, you can run the ShellCheck tool, as if from the shell, with:
$ ghci shellcheck.hs
ghci> runMain ["-x", "file.sh"]
-}
module ShellCheck.Debug () where
import ShellCheck.Analyzer
import ShellCheck.AST
import ShellCheck.CFG
import ShellCheck.Checker
import ShellCheck.CFGAnalysis as CF
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Prelude
import Control.Monad
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.Writer
import Data.Graph.Inductive.Graph as G
import Data.List
import Data.Maybe
import qualified Data.Map as M
import qualified Data.Set as S
-- Run all of ShellCheck (minus output formatters)
shellcheckString :: String -> CheckResult
shellcheckString scriptString =
runIdentity $ checkScript dummySystemInterface checkSpec
where
checkSpec :: CheckSpec
checkSpec = emptyCheckSpec {
csScript = scriptString
}
dummySystemInterface :: SystemInterface Identity
dummySystemInterface = mockedSystemInterface [
-- A tiny, fake filesystem for sourced files
("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"),
("lib/mylib2.sh", "bar=42")
]
-- Parameters used when generating Control Flow Graphs
cfgParams :: CFGParameters
cfgParams = CFGParameters {
cfLastpipe = False,
cfPipefail = False
}
-- An example script to play with
exampleScript :: String
exampleScript = unlines [
"#!/bin/sh",
"count=0",
"for file in *",
"do",
" (( count++ ))",
"done",
"echo $count"
]
-- Parse the script string into ShellCheck's ParseResult
parseScriptString :: String -> ParseResult
parseScriptString scriptString =
runIdentity $ parseScript dummySystemInterface parseSpec
where
parseSpec :: ParseSpec
parseSpec = newParseSpec {
psFilename = "myscript",
psScript = scriptString
}
-- Parse the script string into an Abstract Syntax Tree
stringToAst :: String -> Token
stringToAst scriptString =
case maybeRoot of
Just root -> root
Nothing -> error $ "Script failed to parse: " ++ show parserWarnings
where
parseResult :: ParseResult
parseResult = parseScriptString scriptString
maybeRoot :: Maybe Token
maybeRoot = prRoot parseResult
parserWarnings :: [PositionedComment]
parserWarnings = prComments parseResult
astToCfgResult :: Token -> CFGResult
astToCfgResult = buildGraph cfgParams
astToDfa :: Token -> CFGAnalysis
astToDfa = analyzeControlFlow cfgParams
astToCfg :: Token -> CFGraph
astToCfg = cfGraph . astToCfgResult
stringToCfg :: String -> CFGraph
stringToCfg = astToCfg . stringToAst
stringToDfa :: String -> CFGAnalysis
stringToDfa = astToDfa . stringToAst
cfgToGraphViz :: CFGraph -> String
cfgToGraphViz = cfgToGraphVizWith show
stringToCfgViz :: String -> String
stringToCfgViz = cfgToGraphViz . stringToCfg
stringToDfaViz :: String -> String
stringToDfaViz = dfaToGraphViz . stringToDfa
-- Dump a Control Flow Graph as GraphViz with extended information
stringToDetailedCfgViz :: String -> String
stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph
where
ast :: Token
ast = stringToAst scriptString
cfgResult :: CFGResult
cfgResult = astToCfgResult ast
graph :: CFGraph
graph = cfGraph cfgResult
idToToken :: M.Map Id Token
idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast
idToNode :: M.Map Id (Node, Node)
idToNode = cfIdToRange cfgResult
nodeToStartIds :: M.Map Node (S.Set Id)
nodeToStartIds =
M.fromListWith S.union $
map (\(id, (start, _)) -> (start, S.singleton id)) $
M.toList idToNode
nodeToEndIds :: M.Map Node (S.Set Id)
nodeToEndIds =
M.fromListWith S.union $
map (\(id, (_, end)) -> (end, S.singleton id)) $
M.toList idToNode
formatId :: Id -> String
formatId id = fromMaybe ("Unknown " ++ show id) $ do
(OuterToken _ token) <- M.lookup id idToToken
firstWord <- words (show token) !!! 0
-- Strip off "Inner_"
(_ : tokenName) <- return $ dropWhile (/= '_') firstWord
return $ tokenName ++ " " ++ show id
formatGroup :: S.Set Id -> String
formatGroup set = intercalate ", " $ map formatId $ S.toList set
nodeLabel (node, label) = unlines [
show node ++ ". " ++ show label,
"Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds),
"End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds)
]
-- Dump a Control Flow Graph with Data Flow Analysis as GraphViz
dfaToGraphViz :: CF.CFGAnalysis -> String
dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis
where
label (node, label) =
let
desc = show node ++ ". " ++ show label
in
fromMaybe ("No DFA available\n\n" ++ desc) $ do
(pre, post) <- M.lookup node $ CF.nodeToData analysis
return $ unlines [
"Precondition: " ++ show pre,
"",
desc,
"",
"Postcondition: " ++ show post
]
-- Dump an Control Flow Graph to GraphViz with a given node formatter
cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String
cfgToGraphVizWith nodeLabel graph = concat [
"digraph {\n",
concatMap dumpNode (labNodes graph),
concatMap dumpLink (labEdges graph),
tagVizEntries graph,
"}\n"
]
where
dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n"
dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n"
edgeStyle CFEFlow = "solid"
edgeStyle CFEExit = "bold"
edgeStyle CFEFalseFlow = "dotted"
quoteViz str = "\"" ++ escapeViz str ++ "\""
escapeViz [] = []
escapeViz (c:rest) =
case c of
'\"' -> '\\' : '\"' : escapeViz rest
'\n' -> '\\' : 'l' : escapeViz rest
'\\' -> '\\' : '\\' : escapeViz rest
_ -> c : escapeViz rest
-- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format
astToGraphViz :: Token -> String
astToGraphViz token = concat [
"digraph {\n",
formatTree token,
"}\n"
]
where
formatTree :: Token -> String
formatTree t = snd $ execRWS (doStackAnalysis push pop t) () []
push :: Token -> RWS () String [Int] ()
push (OuterToken (Id n) inner) = do
stack <- get
put (n : stack)
case stack of
[] -> return ()
(top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n"
tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n"
pop :: Token -> RWS () String [Int] ()
pop _ = modify tail
-- For each entry point, set the rank so that they'll align in the graph
tagVizEntries :: CFGraph -> String
tagVizEntries graph = "{ rank=same " ++ rank ++ " }"
where
entries = mapMaybe find $ labNodes graph
find (node, CFEntryPoint name) = return (node, name)
find _ = Nothing
rank = unwords $ map (\(c, _) -> show c) entries

View file

@ -22,6 +22,8 @@
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
import ShellCheck.Interface
import ShellCheck.Prelude
import Control.Monad
import Control.Monad.State
import Data.Array
import Data.List
@ -35,7 +37,7 @@ class Ranged a where
end :: a -> Position
overlap :: a -> a -> Bool
overlap x y =
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
xEnd > yStart && yEnd > xStart
where
yStart = start y
yEnd = end y
@ -86,6 +88,7 @@ instance Ranged Replacement where
instance Monoid Fix where
mempty = newFix
mappend = (<>)
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
instance Semigroup Fix where
f1 <> f2 =
@ -200,7 +203,7 @@ doReplace start end o r =
let si = fromIntegral (start-1)
ei = fromIntegral (end-1)
(x, xs) = splitAt si o
(y, z) = splitAt (ei - si) xs
z = drop (ei - si) xs
in
x ++ r ++ z
@ -228,7 +231,7 @@ applyReplacement2 rep string = do
let (l1, l2) = tmap posLine originalPos in
when (l1 /= 1 || l2 /= 1) $
error "ShellCheck internal error, please report: bad cross-line fix"
error $ pleaseReport "bad cross-line fix"
let replacer = repString rep
let shift = (length replacer) - (oldEnd - oldStart)
@ -273,10 +276,10 @@ getPrefixSum = f 0
where
f sum _ PSLeaf = sum
f sum target (PSBranch pivot left right cumulative) =
case () of
_ | target < pivot -> f sum target left
_ | target > pivot -> f (sum+cumulative) target right
_ -> sum+cumulative
case target `compare` pivot of
LT -> f sum target left
GT -> f (sum+cumulative) target right
EQ -> sum+cumulative
-- Add a value to the Prefix Sum tree at the given index.
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
@ -285,17 +288,17 @@ addPSValue key value tree = if value == 0 then tree else f tree
where
f PSLeaf = PSBranch key PSLeaf PSLeaf value
f (PSBranch pivot left right sum) =
case () of
_ | key < pivot -> PSBranch pivot (f left) right (sum + value)
_ | key > pivot -> PSBranch pivot left (f right) sum
_ -> PSBranch pivot left right (sum + value)
case key `compare` pivot of
LT -> PSBranch pivot (f left) right (sum + value)
GT -> PSBranch pivot left (f right) sum
EQ -> PSBranch pivot left right (sum + value)
prop_pstreeSumsCorrectly kvs targets =
let
-- Trivial O(n * m) implementation
dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
dumbPrefixSums kvs targets =
let prefixSum target = sum . map snd . filter (\(k,v) -> k <= target) $ kvs
let prefixSum target = sum [v | (k,v) <- kvs, k <= target]
in map prefixSum targets
-- PSTree O(n * log m) implementation
smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]

View file

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

View file

@ -38,19 +38,17 @@ import System.FilePath
import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter
format options = do
didOutput <- newIORef False
foundIssues <- newIORef False
reportedIssues <- newIORef False
shouldColor <- shouldOutputColor (foColorOption options)
let color = if shouldColor then colorize else nocolor
return Formatter {
header = return (),
footer = checkFooter didOutput color,
footer = checkFooter foundIssues reportedIssues color,
onFailure = reportFailure color,
onResult = reportResult didOutput color
onResult = reportResult foundIssues reportedIssues color
}
@ -69,9 +67,10 @@ printErr :: ColorFunc -> String -> IO ()
printErr color = hPutStrLn stderr . color bold . color red
reportFailure color file msg = printErr color $ file ++ ": " ++ msg
checkFooter didOutput color = do
output <- readIORef didOutput
unless output $
checkFooter foundIssues reportedIssues color = do
found <- readIORef foundIssues
output <- readIORef reportedIssues
when (found && not output) $
printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
type ColorFunc = (Int -> String -> String)
@ -79,19 +78,20 @@ data LFStatus = LinefeedMissing | LinefeedOk
data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
reportResult :: (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult didOutput color result sys = do
reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult foundIssues reportedIssues color result sys = do
let comments = crComments result
unless (null comments) $ writeIORef foundIssues True
let suggestedFixes = mapMaybe pcFix comments
let fixmap = buildFixMap suggestedFixes
mapM_ output $ M.toList fixmap
where
output (name, fix) = do
file <- (siReadFile sys) name
file <- siReadFile sys (Just True) name
case file of
Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix
writeIORef didOutput True
writeIORef reportedIssues True
Left msg -> reportFailure color name msg
hasTrailingLinefeed str =
@ -203,10 +203,9 @@ formatDoc color (DiffDoc name lf regions) =
buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile
where
splitFixes = concatMap splitFixByFile fixes
splitFixes = splitFixByFile $ mconcat fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
-- There are currently no multi-file fixes, but let's handle it anyways
splitFixByFile :: Fix -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where

View file

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

View file

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

View file

@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.DeepSeq
import Data.Aeson
import Data.IORef
import Data.Monoid
@ -103,7 +104,7 @@ collectResult ref cr sys = mapM_ f groups
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = modifyIORef ref (\x -> comments ++ x)
f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x)
finish ref = do
list <- readIORef ref

View file

@ -23,12 +23,13 @@ module ShellCheck.Formatter.JSON1 (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.DeepSeq
import Data.Aeson
import Data.IORef
import Data.Monoid
import GHC.Exts
import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.List.NonEmpty as NE
format :: IO Formatter
format = do
@ -113,14 +114,14 @@ outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
groups = NE.groupWith sourceFile comments
f :: NE.NonEmpty PositionedComment -> IO ()
f group = do
let filename = sourceFile (head group)
result <- siReadFile sys filename
let filename = sourceFile (NE.head group)
result <- siReadFile sys (Just True) filename
let contents = either (const "") id result
let comments' = makeNonVirtual comments contents
modifyIORef ref (\x -> comments' ++ x)
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
finish ref = do
list <- readIORef ref

View file

@ -23,6 +23,7 @@ import ShellCheck.Fixer
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.DeepSeq
import Control.Monad
import Data.Array
import Data.Foldable
@ -30,9 +31,9 @@ import Data.Ord
import Data.IORef
import Data.List
import Data.Maybe
import GHC.Exts
import System.IO
import System.Info
import qualified Data.List.NonEmpty as NE
wikiLink = "https://www.shellcheck.net/wiki/"
@ -88,7 +89,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
appendComments errRef comments max = do
previous <- readIORef errRef
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current
where
fst3 (x,_,_) = x
equal x y = fst3 x == fst3 y
@ -116,19 +117,19 @@ outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = groupWith sourceFile comments
let fileGroups = NE.groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups
outputForFile color sys comments = do
let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName
let fileName = sourceFile (NE.head comments)
result <- siReadFile sys (Just True) fileName
let contents = either (const "") id result
let fileLinesList = lines contents
let lineCount = length fileLinesList
let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do
let lineNum = fromIntegral $ lineNo (head commentsForLine)
let groups = NE.groupWith lineNo comments
forM_ groups $ \commentsForLine -> do
let lineNum = fromIntegral $ lineNo (NE.head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount
then ""
else fileLines ! fromIntegral lineNum
@ -136,10 +137,9 @@ outputForFile color sys comments = do
putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
putStrLn ""
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
) groups
showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines
-- Pick out only the lines necessary to show a fix in action
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
@ -175,7 +175,7 @@ showFixedString color comments lineNum fileLines =
cuteIndent :: PositionedComment -> String
cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment
where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow =

View file

@ -1,5 +1,5 @@
{-
Copyright 2012-2019 Vidar Holen
Copyright 2012-2024 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
@ -21,14 +21,14 @@
module ShellCheck.Interface
(
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)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
, AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash)
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
, ExecutionMode(Executed, Sourced)
, ErrorMessage
, Code
@ -39,11 +39,12 @@ module ShellCheck.Interface
, ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult
, newAnalysisSpec
, newFormatterOptions
, newParseResult
, newPosition
, newSystemInterface
, newTokenComment
, mockedSystemInterface
, mockRcFile
@ -73,15 +74,19 @@ import qualified Data.Map as Map
data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String),
-- Given:
-- | Given:
-- What annotations say about including external files (if anything)
-- A resolved filename from siFindSource
-- Read the file or return an error
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
-- | Given:
-- the current script,
-- what annotations say about including external files (if anything)
-- a list of source-path annotations in effect,
-- and a sourced file,
-- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath,
-- Get the configuration file (name, contents) for a filename
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
-- | Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String))
}
@ -95,6 +100,7 @@ data CheckSpec = CheckSpec {
csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity,
csExtendedAnalysis :: Maybe Bool,
csOptionalChecks :: [String]
} deriving (Show, Eq)
@ -119,6 +125,7 @@ emptyCheckSpec = CheckSpec {
csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing,
csMinSeverity = StyleC,
csExtendedAnalysis = Nothing,
csOptionalChecks = []
}
@ -131,6 +138,14 @@ newParseSpec = ParseSpec {
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
data ParseSpec = ParseSpec {
psFilename :: String,
@ -161,6 +176,7 @@ data AnalysisSpec = AnalysisSpec {
asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool,
asOptionalChecks :: [String],
asExtendedAnalysis :: Maybe Bool,
asTokenPositions :: Map.Map Id (Position, Position)
}
@ -171,6 +187,7 @@ newAnalysisSpec token = AnalysisSpec {
asExecutionMode = Executed,
asCheckSourced = False,
asOptionalChecks = [],
asExtendedAnalysis = Nothing,
asTokenPositions = Map.empty
}
@ -208,7 +225,7 @@ newCheckDescription = CheckDescription {
}
-- 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)
type ErrorMessage = String
@ -256,9 +273,6 @@ data Replacement = Replacement {
data InsertionPoint = InsertBefore | InsertAfter
deriving (Show, Eq, Generic, NFData)
instance Ord Replacement where
compare r1 r2 = (repStartPos r1) `compare` (repStartPos r2)
newReplacement = Replacement {
repStartPos = newPosition,
repEndPos = newPosition,
@ -310,19 +324,18 @@ data ColorOption =
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {
mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
siReadFile = rf,
siFindSource = fs,
siGetConfig = const $ return Nothing
}
where
rf file =
case filter ((== file) . fst) files of
[] -> return $ Left "File not included in mock."
[(_, contents)] -> return $ Right contents
fs _ _ file = return file
rf _ file = return $
case find ((== file) . fst) files of
Nothing -> Left "File not included in mock."
Just (_, contents) -> Right contents
fs _ _ _ file = return file
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
}

File diff suppressed because it is too large Load diff

51
src/ShellCheck/Prelude.hs Normal file
View file

@ -0,0 +1,51 @@
{-
Copyright 2022 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
-- Generic basic utility functions
module ShellCheck.Prelude where
import Data.Semigroup
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
-- Get the last element or a default. Like `last` but safe.
lastOrDefault def [] = def
lastOrDefault _ list = last list
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Like mconcat but for Semigroups
sconcat1 :: (Semigroup t) => [t] -> t
sconcat1 [x] = x
sconcat1 (x:xs) = x <> sconcat1 xs
sconcatOrDefault def [] = def
sconcatOrDefault _ list = sconcat1 list
-- For more actionable "impossible" errors
pleaseReport str = "ShellCheck internal error, please report: " ++ str

View file

@ -2,7 +2,7 @@
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-13.26
resolver: lts-18.15
# Local packages, usually specified by relative directory name
packages:

View file

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

View file

@ -11,21 +11,37 @@ command -v cabal ||
cabal update ||
die "can't update"
cabal install --dependencies-only --enable-tests ||
if [ -e /etc/arch-release ]
then
# Arch has an unconventional packaging setup
flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic)
else
flags=()
fi
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
cabal install --dependencies-only "${flags[@]}" ||
cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" ||
die "can't install dependencies"
cabal configure --enable-tests ||
cabal configure --enable-tests "${flags[@]}" ||
die "configure failed"
cabal build ||
die "build failed"
cabal test ||
die "test failed"
cabal haddock ||
die "haddock failed"
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
sc="$(find . -name shellcheck -type f -perm -111)"
[ -x "$sc" ] || die "Can't find executable"
"$sc" - << 'EOF' || die "execution failed"
#!/bin/sh
echo "Hello World"
EOF
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
"$sc" - << 'EOF' && die "negative execution failed"
#!/bin/sh
echo $1
EOF

View file

@ -1,4 +1,5 @@
#!/usr/bin/env bash
# shellcheck disable=SC2257
failed=0
fail() {
@ -8,7 +9,18 @@ fail() {
if git diff | grep -q ""
then
fail "There are uncommited changes"
fail "There are uncommitted changes"
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)
@ -33,41 +45,38 @@ then
fail "You are not on master"
fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then
fail "Expected git log message to be 'Stable version ...'"
fi
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
then
fail "Expected git log message to contain CHANGELOG"
fi
i=1 j=1
cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Make sure Travis build currently passes: https://travis-ci.org/koalaman/shellcheck
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((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 SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds, so that all files are
$((i++)). Make sure the Hackage package builds locally.
Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for Travis to build
$((j++)). Wait for GitHub Actions to build.
$((j++)). Verify release:
a. Check that the new versions are uploaded: https://shellcheck.storage.googleapis.com/index.html
a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md

View file

@ -17,13 +17,20 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue.
Also note that 'dist' will be deleted.
Also note that dist*/ and .stack-work/ will be deleted.
EOF
exit 0
}
echo "Deleting 'dist'..."
rm -rf dist
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
rm -rf dist dist-newstyle .stack-work
execs=$(find . -name shellcheck)
if [ -n "$execs" ]
then
die "Found unexpected executables. Remove and try again: $execs"
fi
log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log"
@ -61,20 +68,18 @@ done << EOF
debian:stable apt-get update && apt-get install -y cabal-install
debian:testing apt-get update && apt-get install -y cabal-install
ubuntu:latest apt-get update && apt-get install -y cabal-install
opensuse/leap:latest zypper install -y cabal-install ghc
# Other Ubuntu versions we want to support
ubuntu:19.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.10 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Known to currently fail
centos:latest yum install -y epel-release && yum install -y cabal-install
fedora:latest dnf install -y cabal-install
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Ubuntu LTS
ubuntu:24.04 apt-get update && apt-get install -y cabal-install
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
# Stack on Ubuntu LTS
ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF
exit "$final"

View file

@ -4,8 +4,12 @@ import Control.Monad
import System.Exit
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.ASTLib
import qualified ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis
import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer
@ -14,17 +18,24 @@ import qualified ShellCheck.Parser
main = do
putStrLn "Running ShellCheck tests..."
results <- sequence [
ShellCheck.Analytics.runTests
,ShellCheck.AnalyzerLib.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.Custom.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.Fixer.runTests
,ShellCheck.Formatter.Diff.runTests
,ShellCheck.Parser.runTests
failures <- filter (not . snd) <$> mapM sequenceA tests
if null failures then exitSuccess else do
putStrLn "Tests failed for the following module(s):"
mapM (putStrLn . ("- ShellCheck." ++) . fst) failures
exitFailure
where
tests =
[ ("Analytics" , ShellCheck.Analytics.runTests)
, ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests)
, ("ASTLib" , ShellCheck.ASTLib.runTests)
, ("CFG" , ShellCheck.CFG.runTests)
, ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests)
, ("Checker" , ShellCheck.Checker.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

@ -3,7 +3,7 @@
# various resolvers. It's run via distrotest.
resolvers=(
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
)
die() { echo "$*" >&2; exit 1; }
@ -15,13 +15,14 @@ die() { echo "$*" >&2; exit 1; }
command -v stack ||
die "stack is missing"
stack setup || die "Failed to setup with default resolver"
stack setup --allow-different-user || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
# Nice to haves, but not necessary
for resolver in "${resolvers[@]}"
do
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter."
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter."
done
echo "Success"