244 Commits

Author SHA1 Message Date
Vidar Holen
c9b8ad3439 Drop attoparsec/text dependencies 2023-10-08 18:16:09 -07:00
Vidar Holen
e59fbfebda Re-add other Portage functionality 2023-10-08 16:09:58 -07:00
Vidar Holen
ce3414eeea Move from Parameters to SystemInterface for Portage variables 2023-08-27 17:53:14 -07:00
Vidar Holen
feebbbb096 Merge branch 'kangie' into ebuild 2023-08-27 15:20:00 -07:00
Vidar Holen
87ef5ae18a Merge branch 'portage' of https://github.com/Kangie/shellcheck into kangie 2023-08-27 15:18:32 -07:00
Vidar Holen
0138a6fafc Example plumbing for Portage variables 2023-08-13 17:49:36 -07: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
hololeap
dfa920c5d2 Switch to attoparsec for gentoo scan
Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-05 17:38:01 -06:00
hololeap
fc9b63fb5e Remove PortageAutoInternalVariables and python
The Gentoo eclass list is now populated using pure Haskell. The old
python generators and generated module are no longer needed.

Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-05 15:31:15 -06:00
hololeap
272ef819b9 Scan for Gentoo eclass variables
Creates a Map of eclass names to eclass variables by scanning the
system for repositories and their respective eclasses. Runs `portageq`
to determine repository names and locations. Emits a warning if an
IOException is caught when attempting to run `portageq`.

This Map is passed via CheckSpec to AnalysisSpec and finally to
Parameters, where it is read by `checkUnusedAssignments` in order to
determine which variables can be safely ignored by this check.

Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-05 15:31:15 -06:00
hololeap
08ae7ef836 New IO interface to scan for Gentoo eclass vars
Uses the `portageq` command to scan for repositories, which in turn are
scanned for eclasses, which are then scanned for eclass variables.

The variables are scanned using a heuristic which looks for

    "# @ECLASS_VARIABLE: "

at the start of each line, which means only properly documented
variables will be found.

Signed-off-by: hololeap <hololeap@users.noreply.github.com>
2023-08-04 17:19:05 -06:00
Matt Jolly
e3d8483e49 Rebase of chromiumos fork
https://chromium.googlesource.com/chromiumos/third_party/shellcheck/
2023-08-04 15:56:48 -06: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
Moritz Röhrich
d6bb8fc0d8 Error on backslash in comment #2132
- Report error in case of a backspace in a comment

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

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

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

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

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

View File

@@ -1,4 +1,4 @@
name: Build Lol name: Build ShellCheck
# Run this workflow every time a new commit pushed to your repository # Run this workflow every time a new commit pushed to your repository
on: push on: push
@@ -15,26 +15,29 @@ jobs:
sudo apt-get install cabal-install sudo apt-get install cabal-install
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
with:
- name: Package Source fetch-depth: 0
run: |
mkdir source
cabal sdist
mv dist/*.tar.gz source/source.tar.gz
- name: Deduce tags - name: Deduce tags
run: | run: |
exec > source/tags mkdir source
echo "latest" echo "latest" > source/tags
if tag=$(git describe --exact-match --tags) if tag=$(git describe --exact-match --tags)
then then
echo "stable" echo "stable" >> source/tags
echo "$tag" echo "$tag" >> source/tags
fi 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 - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: source name: source
path: source/ path: source/
@@ -48,10 +51,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v2 uses: actions/download-artifact@v3
- name: Build source - name: Build source
run: | run: |
@@ -60,7 +63,7 @@ jobs:
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} ) ( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: bin name: bin
path: bin/ path: bin/
@@ -71,10 +74,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v2 uses: actions/download-artifact@v3
- name: Work around GitHub permissions bug - name: Work around GitHub permissions bug
run: chmod +x bin/*/shellcheck* run: chmod +x bin/*/shellcheck*
@@ -89,7 +92,7 @@ jobs:
rm -rf */ README* LICENSE* rm -rf */ README* LICENSE*
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: deploy name: deploy
path: deploy/ path: deploy/
@@ -101,10 +104,10 @@ jobs:
environment: Deploy environment: Deploy
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v2 uses: actions/download-artifact@v3
- name: Upload to GitHub - name: Upload to GitHub
env: env:
@@ -113,6 +116,10 @@ jobs:
export TAGS="$(cat source/tags)" export TAGS="$(cat source/tags)"
./.github_deploy ./.github_deploy
- name: Waiting for GitHub to replicate uploaded releases
run: |
sleep 300
- name: Upload to Docker Hub - name: Upload to Docker Hub
env: env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}

View File

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

View File

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

View File

@@ -1,13 +1,87 @@
## Git ## Git
### Added ### Added
- 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.
### Fixed
- source statements with here docs now work correctly
### Changed
## 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` - `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 - SC2259/SC2260: Warn when redirections override pipes
- SC2261: Warn about multiple competing redirections - SC2261: Warn about multiple competing redirections
- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit - SC2262/SC2263: Warn about aliases declared and used in the same parsing unit
- SC2264: Warn about wrapper functions that blatantly recurse - SC2264: Warn about wrapper functions that blatantly recurse
- SC2265/SC2266: Warn when using & or | with test statements - SC2265/SC2266: Warn when using & or | with test statements
- SC2267: Warn when using xargs -i instead of -I - SC2267: Warn when using xargs -i instead of -I
- Optional avoid-x-comparisons: Style warning SC2268 for `[ x$var = xval ]` - SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]`
### Fixed ### Fixed
- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors - SC1072/SC1073 now respond to disable annotations, though ignoring parse errors
@@ -21,7 +95,7 @@
- POSIX/dash unsupported feature warnings now have individual SC3xxx codes - POSIX/dash unsupported feature warnings now have individual SC3xxx codes
- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files - 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 - SC2154: Variables appearing in -z/-n tests are no longer considered unassigned
- SC2270-SC2285: Improved warnings about misused =, e.g. `${var}=42` - SC2270-SC2285: Improved warnings about misused `=`, e.g. `${var}=42`
## v0.7.1 - 2020-04-04 ## v0.7.1 - 2020-04-04
@@ -164,7 +238,7 @@
- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )` - SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )`
- SC2200/SC2201: Warn about brace expansion in [/[[ - SC2200/SC2201: Warn about brace expansion in [/[[
- SC2198/SC2199: Warn about arrays in [/[[ - SC2198/SC2199: Warn about arrays in [/[[
- SC2196/SC2197: Warn about deprected egrep/fgrep - SC2196/SC2197: Warn about deprecated egrep/fgrep
- SC2195: Warn about unmatchable case branches - SC2195: Warn about unmatchable case branches
- SC2194: Warn about constant 'case' statements - SC2194: Warn about constant 'case' statements
- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables - SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables
@@ -181,7 +255,7 @@
### Fixed ### Fixed
- `-c` no longer suggested when using `grep -o | wc` - `-c` no longer suggested when using `grep -o | wc`
- Comments and whitespace are now allowed before filewide directives - Comments and whitespace are now allowed before filewide directives
- Here doc delimters with esoteric quoting like `foo""` are now handled - Here doc delimiters with esoteric quoting like `foo""` are now handled
- SC2095 about `ssh` in while read loops is now suppressed when using `-n` - SC2095 about `ssh` in while read loops is now suppressed when using `-n`
- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks - `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks
- `grep -F` now suppresses regex related suggestions - `grep -F` now suppresses regex related suggestions

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 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 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 may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read Public License instead of this License. But first, please read
<https://www.gnu.org/philosophy/why-not-lgpl.html>. <https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,4 +1,5 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck) [![Build Status](https://github.com/koalaman/shellcheck/actions/workflows/build.yml/badge.svg)](https://github.com/koalaman/shellcheck/actions/workflows/build.yml)
# ShellCheck - A shell script static analysis tool # ShellCheck - A shell script static analysis tool
@@ -111,10 +112,7 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
* [Code Factor](https://www.codefactor.io/) * [Code Factor](https://www.codefactor.io/)
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck) * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
* [Github](https://github.com/features/actions) (only Linux) * [Github](https://github.com/features/actions) (only Linux)
* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
Services and platforms with third party plugins:
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
Most other services, including [GitLab](https://about.gitlab.com/), let you install Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
@@ -143,7 +141,7 @@ On systems with Stack (installs to `~/.local/bin`):
On Debian based distros: On Debian based distros:
apt-get install shellcheck sudo apt install shellcheck
On Arch Linux based distros: On Arch Linux based distros:
@@ -157,8 +155,8 @@ On Gentoo based distros:
On EPEL based distros: On EPEL based distros:
yum -y install epel-release sudo yum -y install epel-release
yum install ShellCheck sudo yum install ShellCheck
On Fedora based distros: On Fedora based distros:
@@ -235,6 +233,9 @@ Alternatively, you can download pre-compiled binaries for the latest release her
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds). (including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
There are currently no official binaries for Apple Silicon, but third party builds are available via
[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
Distro packages already come with a `man` page. If you are building from source, it can be installed with: Distro packages already come with a `man` page. If you are building from source, it can be installed with:
```console ```console
@@ -242,6 +243,19 @@ pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1 sudo mv shellcheck.1 /usr/share/man/man1
``` ```
### pre-commit
To run ShellCheck via [pre-commit](https://pre-commit.com/), add the hook to your `.pre-commit-config.yaml`:
```
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.7.2
hooks:
- id: shellcheck
# args: ["--severity=warning"] # Optionally only show errors and warnings
```
### Travis CI ### Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
@@ -348,6 +362,7 @@ echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Don\'t try this at home' # Attempting to escape ' in '' echo 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
unset var[i] # Array index treated as glob
``` ```
### Conditionals ### Conditionals
@@ -366,6 +381,7 @@ ShellCheck can recognize many types of incorrect test statements.
[ grep -q foo file ] # Command without $(..) [ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed [[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..)) (( 1 -lt 2 )) # Using test operators in ((..))
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
``` ```
### Frequently misused commands ### Frequently misused commands
@@ -437,6 +453,8 @@ echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count printf '%s: %s\n' foo # Mismatches in printf argument count
eval "${array[@]}" # Lost word boundaries in array eval
for i in "${x[@]}"; do ${x[$i]} # Using array value as key
``` ```
### Robustness ### Robustness
@@ -461,6 +479,7 @@ ShellCheck will warn when using features not supported by the shebang. For examp
echo {1..$n} # Works in ksh, but not bash/dash/sh echo {1..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not dash/sh echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh echo -n 42 # Works in ksh, bash and dash, undefined in sh
expr match str regex # Unportable alias for `expr str : regex`
trap 'exit 42' sigint # Unportable signal spec trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files read foo < /dev/tcp/host/22 # Unportable intercepted files
@@ -481,10 +500,15 @@ rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \ echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors !# bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input sed 's/foo/bar/' file > file # Redirecting to input
var2=$var2 # Variable assigned to itself
[ x$var = xval ] # Antiquated x-comparisons
ls() { ls -l "$@"; } # Infinitely recursive wrapper
alias ls='ls -l'; ls foo # Alias used before it takes effect
for x; do for x; do # Nested loop uses same variable
while getopts "a" f; do case $f in "b") # Unhandled getopts flags while getopts "a" f; do case $f in "b") # Unhandled getopts flags
``` ```

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.7.1 Version: 0.9.0
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -8,7 +8,7 @@ Author: Vidar Holen
Maintainer: vidar@vidarholen.net Maintainer: vidar@vidarholen.net
Homepage: https://www.shellcheck.net/ Homepage: https://www.shellcheck.net/
Build-Type: Simple Build-Type: Simple
Cabal-Version: >= 1.8 Cabal-Version: 1.18
Bug-reports: https://github.com/koalaman/shellcheck/issues Bug-reports: https://github.com/koalaman/shellcheck/issues
Description: Description:
The goals of ShellCheck are: The goals of ShellCheck are:
@@ -22,9 +22,11 @@ Description:
* To point out subtle caveats, corner cases and pitfalls, that may cause an * To point out subtle caveats, corner cases and pitfalls, that may cause an
advanced user's otherwise working script to fail under future circumstances. advanced user's otherwise working script to fail under future circumstances.
Extra-Doc-Files:
README.md
CHANGELOG.md
Extra-Source-Files: Extra-Source-Files:
-- documentation -- documentation
README.md
shellcheck.1.md shellcheck.1.md
-- A script to build the man page using pandoc -- A script to build the man page using pandoc
manpage manpage
@@ -43,19 +45,26 @@ library
build-depends: build-depends:
semigroups semigroups
build-depends: build-depends:
aeson, -- The lower bounds are based on GHC 7.10.3
array, -- The upper bounds are based on GHC 9.6.1
aeson >= 1.4.0 && < 2.2,
array >= 0.5.1 && < 0.6,
base >= 4.8.0.0 && < 5, base >= 4.8.0.0 && < 5,
bytestring, bytestring >= 0.10.6 && < 0.12,
containers >= 0.5, containers >= 0.5.6 && < 0.7,
deepseq >= 1.4.0.0, deepseq >= 1.4.1 && < 1.5,
Diff >= 0.2.0, Diff >= 0.4.0 && < 0.5,
directory >= 1.2.3.0, fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
mtl >= 2.2.1, filepath >= 1.4.0 && < 1.5,
filepath, mtl >= 2.2.2 && < 2.4,
parsec, parsec >= 3.1.14 && < 3.2,
regex-tdfa, QuickCheck >= 2.14.2 && < 2.15,
QuickCheck >= 2.7.4, 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: -- When cabal supports it, move this to setup-depends:
process process
exposed-modules: exposed-modules:
@@ -64,11 +73,15 @@ library
ShellCheck.Analytics ShellCheck.Analytics
ShellCheck.Analyzer ShellCheck.Analyzer
ShellCheck.AnalyzerLib ShellCheck.AnalyzerLib
ShellCheck.CFG
ShellCheck.CFGAnalysis
ShellCheck.Checker ShellCheck.Checker
ShellCheck.Checks.Commands ShellCheck.Checks.Commands
ShellCheck.Checks.ControlFlow
ShellCheck.Checks.Custom ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport ShellCheck.Checks.ShellSupport
ShellCheck.Data ShellCheck.Data
ShellCheck.Debug
ShellCheck.Fixer ShellCheck.Fixer
ShellCheck.Formatter.Format ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle ShellCheck.Formatter.CheckStyle
@@ -80,9 +93,12 @@ library
ShellCheck.Formatter.Quiet ShellCheck.Formatter.Quiet
ShellCheck.Interface ShellCheck.Interface
ShellCheck.Parser ShellCheck.Parser
ShellCheck.PortageVariables
ShellCheck.Prelude
ShellCheck.Regex ShellCheck.Regex
other-modules: other-modules:
Paths_ShellCheck Paths_ShellCheck
default-language: Haskell98
executable shellcheck executable shellcheck
if impl(ghc < 8.0) if impl(ghc < 8.0)
@@ -91,18 +107,21 @@ executable shellcheck
build-depends: build-depends:
aeson, aeson,
array, array,
base >= 4 && < 5, base,
bytestring, bytestring,
containers, containers,
deepseq >= 1.4.0.0, deepseq,
Diff >= 0.2.0, Diff,
directory >= 1.2.3.0, directory,
mtl >= 2.2.1, fgl,
mtl,
filepath, filepath,
parsec >= 3.0, parsec,
QuickCheck >= 2.7.4, QuickCheck,
regex-tdfa, regex-tdfa,
transformers,
ShellCheck ShellCheck
default-language: Haskell98
main-is: shellcheck.hs main-is: shellcheck.hs
test-suite test-shellcheck test-suite test-shellcheck
@@ -110,17 +129,19 @@ test-suite test-shellcheck
build-depends: build-depends:
aeson, aeson,
array, array,
base >= 4 && < 5, base,
bytestring, bytestring,
containers, containers,
deepseq >= 1.4.0.0, deepseq,
Diff >= 0.2.0, Diff,
directory >= 1.2.3.0, directory,
mtl >= 2.2.1, fgl,
filepath, filepath,
mtl,
parsec, parsec,
QuickCheck >= 2.7.4, QuickCheck,
regex-tdfa, regex-tdfa,
transformers,
ShellCheck ShellCheck
default-language: Haskell98
main-is: test/shellcheck.hs main-is: test/shellcheck.hs

View File

@@ -1,5 +1,4 @@
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76 FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
FROM liushuyu/osxcross:latest
ENV TARGET x86_64-apple-darwin18 ENV TARGET x86_64-apple-darwin18
ENV TARGETNAME darwin.x86_64 ENV TARGETNAME darwin.x86_64
@@ -7,22 +6,25 @@ ENV TARGETNAME darwin.x86_64
# Build dependencies # Build dependencies
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt-get install -y ghc automake autoconf llvm curl alex happy
# Build GHC # Build GHC
WORKDIR /ghc WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1 RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" RUN ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal # Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config # It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg" ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies # Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1 RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script # Copy the build script
COPY build /usr/bin COPY build /usr/bin

View File

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

View File

@@ -6,22 +6,28 @@ ENV TARGETNAME linux.aarch64
# Build dependencies # Build dependencies
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
# The rest are from 22.10
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
# Build GHC # Build GHC
WORKDIR /ghc WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1 RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal # Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config # It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg" ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies # Prebuild the dependencies
RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1 RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
# Copy the build script # Copy the build script
COPY build /usr/bin COPY build /usr/bin

View File

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

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
FROM ubuntu:20.04
ENV TARGET powerpc64le-linux-gnu
ENV TARGETNAME linux.ppc64le
# Build dependencies
USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
# Build GHC
WORKDIR /ghc
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" --enable-unregisterised
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
RUN make install
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
# Due to an apparent cabal bug, we specify our options directly to cabal
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
# Prebuild the dependencies
#RUN cabal update && IFS=';' && cabal install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1
# Copy the build script
COPY build /usr/bin
WORKDIR /scratch
ENTRYPOINT ["/usr/bin/build"]

View File

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

View File

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

View File

@@ -1,16 +1,10 @@
FROM ubuntu:20.04 FROM alpine:latest
ENV TARGETNAME linux.x86_64 ENV TARGETNAME linux.x86_64
# Install GHC and cabal # Install GHC and cabal
USER root USER root
ENV DEBIAN_FRONTEND noninteractive RUN apk add ghc cabal g++ libffi-dev curl bash
RUN apt-get update && apt-get install -y ghc curl xz-utils
# So we'd like a later version of Cabal that supports --enable-executable-static,
# but we can't use Ubuntu 20.10 because coreutils has switched to new syscalls that
# the TravisCI kernel doesn't support. Download it manually.
RUN curl "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/bin
# Use ld.bfd instead of ld.gold due to # Use ld.bfd instead of ld.gold due to
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error: # x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:

View File

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

View File

@@ -5,22 +5,22 @@ ENV TARGETNAME windows.x86_64
# We don't need wine32, even though it complains # We don't need wine32, even though it complains
USER root USER root
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y curl busybox wine RUN apt-get update && apt-get install -y curl busybox wine winbind
# Fetch Windows version, will be available under z:\haskell # Fetch Windows version, will be available under z:\haskell
WORKDIR /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 RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
WORKDIR /haskell/bin WORKDIR /haskell/bin
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip - RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
RUN curl -L "https://curl.se/windows/dl-7.75.0/curl-7.75.0-win64-mingw.zip" | busybox unzip - && mv curl-7.75.0-win64-mingw/bin/* . RUN curl -L "https://curl.se/windows/dl-7.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.0-win64-mingw/bin/* .
ENV WINEPATH /haskell/bin ENV WINEPATH /haskell/bin
# It's unknown whether Cabal on Windows suffers from the same issue # It's unknown whether Cabal on Windows suffers from the same issue
# that necessitated this but I don't care enough to find out # that necessitated this but I don't care enough to find out
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections" ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections"
# Precompile some deps to speed up later builds. This list is just copied from `cabal build` # Precompile some deps to speed up later builds
RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install $CABALOPTS --lib Diff-0.4.0 base-compat-0.11.2 base-orphans-0.8.4 dlist-1.0 hashable-1.3.0.0 indexed-traversable-0.1.1 integer-logarithms-1.0.3.1 primitive-0.7.1.0 regex-base-0.94.0.0 splitmix-0.1.0.3 tagged-0.8.6.1 th-abstraction-0.4.2.0 transformers-compat-0.6.6 base-compat-batteries-0.11.2 time-compat-1.9.5 unordered-containers-0.2.13.0 data-fix-0.3.1 vector-0.12.2.0 scientific-0.3.6.2 regex-tdfa-1.3.1.0 random-1.2.0 distributive-0.6.2.1 attoparsec-0.13.2.5 uuid-types-1.0.3 comonad-5.0.8 bifunctors-5.5.10 assoc-1.0.2 these-1.1.1.1 strict-0.4.0.1 aeson-1.5.5.1 RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck
COPY build /usr/bin COPY build /usr/bin
WORKDIR /scratch WORKDIR /scratch

View File

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

294
doc/shellcheck_logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 244 KiB

View File

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

View File

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

11
setgitversion Executable file
View File

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

View File

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

View File

@@ -21,6 +21,7 @@ import qualified ShellCheck.Analyzer
import ShellCheck.Checker import ShellCheck.Checker
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.PortageVariables
import ShellCheck.Regex import ShellCheck.Regex
import qualified ShellCheck.Formatter.CheckStyle import qualified ShellCheck.Formatter.CheckStyle
@@ -34,6 +35,8 @@ import qualified ShellCheck.Formatter.Quiet
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Except import Control.Monad.Except
import Data.Bits import Data.Bits
import Data.Char import Data.Char
@@ -225,7 +228,7 @@ runFormatter sys format options files = do
f :: Status -> FilePath -> IO Status f :: Status -> FilePath -> IO Status
f status file = do f status file = do
newStatus <- process file `catch` handler file newStatus <- process file `catch` handler file
return $ status `mappend` newStatus return $! status `mappend` newStatus
handler :: FilePath -> IOException -> IO Status handler :: FilePath -> IOException -> IO Status
handler file e = reportFailure file (show e) handler file e = reportFailure file (show e)
reportFailure file str = do reportFailure file str = do
@@ -234,7 +237,7 @@ runFormatter sys format options files = do
process :: FilePath -> IO Status process :: FilePath -> IO Status
process filename = do process filename = do
input <- siReadFile sys filename input <- siReadFile sys Nothing filename
either (reportFailure filename) check input either (reportFailure filename) check input
where where
check contents = do check contents = do
@@ -389,27 +392,30 @@ parseOption flag options =
throwError SyntaxFailure throwError SyntaxFailure
return (Prelude.read num :: Integer) return (Prelude.read num :: Integer)
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
ioInterface options files = do ioInterface options files = do
inputs <- mapM normalize files inputs <- mapM normalize files
cache <- newIORef emptyCache cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing) configCache <- newIORef ("", Nothing)
return SystemInterface { portageVars <- newIORef Nothing
return (newSystemInterface :: SystemInterface IO) {
siReadFile = get cache inputs, siReadFile = get cache inputs,
siFindSource = findSourceFile inputs (sourcePaths options), siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache siGetConfig = getConfig configCache,
siGetPortageVariables = getOrLoadPortage portageVars
} }
where where
emptyCache :: Map.Map FilePath String emptyCache :: Map.Map FilePath String
emptyCache = Map.empty emptyCache = Map.empty
get cache inputs file = do get cache inputs rcSuggestsExternal file = do
map <- readIORef cache map <- readIORef cache
case Map.lookup file map of case Map.lookup file map of
Just x -> return $ Right x Just x -> return $ Right x
Nothing -> fetch cache inputs file Nothing -> fetch cache inputs rcSuggestsExternal file
fetch cache inputs file = do fetch cache inputs rcSuggestsExternal file = do
ok <- allowable inputs file ok <- allowable rcSuggestsExternal inputs file
if ok if ok
then (do then (do
(contents, shouldCache) <- inputFile file (contents, shouldCache) <- inputFile file
@@ -417,13 +423,16 @@ ioInterface options files = do
modifyIORef cache $ Map.insert file contents modifyIORef cache $ Map.insert file contents
return $ Right contents return $ Right contents
) `catch` handler ) `catch` handler
else
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).") else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where where
handler :: IOException -> IO (Either ErrorMessage String) handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex handler ex = return . Left $ show ex
allowable inputs x = allowable rcSuggestsExternal inputs x =
if externalSources options if fromMaybe (externalSources options) rcSuggestsExternal
then return True then return True
else do else do
path <- normalize x path <- normalize x
@@ -497,7 +506,7 @@ ioInterface options files = do
b <- p x b <- p x
if b then pure (Just x) else acc if b then pure (Just x) else acc
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original = findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original =
if isAbsolute original if isAbsolute original
then then
let (_, relative) = splitDrive original let (_, relative) = splitDrive original
@@ -506,7 +515,7 @@ ioInterface options files = do
find original original find original original
where where
find filename deflt = do find filename deflt = do
sources <- findM ((allowable inputs) `andM` doesFileExist) $ sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation) (adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
case sources of case sources of
Nothing -> return deflt Nothing -> return deflt
@@ -517,6 +526,21 @@ ioInterface options files = do
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest) ("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str _ -> str
getOrLoadPortage cache = do
x <- readIORef cache
case x of
Just m -> do
return m
Nothing -> do
vars <- readPortageVariables `catch` handler
writeIORef cache $ Just vars
return vars
where
handler :: IOException -> IO (Map.Map String [String])
handler e = do
hPutStrLn stderr $ "Error finding portage repos, eclass definitions will be ignored: " ++ show e
return $ Map.empty
inputFile file = do inputFile file = do
(handle, shouldCache) <- (handle, shouldCache) <-
if file == "-" if file == "-"

View File

@@ -45,6 +45,7 @@ data InnerToken t =
| Inner_TA_Variable String [t] | Inner_TA_Variable String [t]
| Inner_TA_Expansion [t] | Inner_TA_Expansion [t]
| Inner_TA_Sequence [t] | Inner_TA_Sequence [t]
| Inner_TA_Parenthesis t
| Inner_TA_Trinary t t t | Inner_TA_Trinary t t t
| Inner_TA_Unary String t | Inner_TA_Unary String t
| Inner_TC_And ConditionType String t t | Inner_TC_And ConditionType String t t
@@ -141,7 +142,7 @@ data InnerToken t =
| Inner_T_CoProcBody t | Inner_T_CoProcBody t
| Inner_T_Include t | Inner_T_Include t
| Inner_T_SourceCommand t t | Inner_T_SourceCommand t t
| Inner_T_BatsTest t t | Inner_T_BatsTest String t
deriving (Show, Eq, Functor, Foldable, Traversable) deriving (Show, Eq, Functor, Foldable, Traversable)
data Annotation = data Annotation =
@@ -150,6 +151,7 @@ data Annotation =
| SourceOverride String | SourceOverride String
| ShellOverride String | ShellOverride String
| SourcePath String | SourcePath String
| ExternalSources Bool
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
@@ -203,6 +205,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
pattern T_Array id t = OuterToken id (Inner_T_Array t) pattern T_Array id t = OuterToken id (Inner_T_Array t)
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t)
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value) pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1) pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
@@ -255,7 +258,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-} {-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parentesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
instance Eq Token where instance Eq Token where
OuterToken _ a == OuterToken _ b = a == b OuterToken _ a == OuterToken _ b = a == b

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2021 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,9 +17,12 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.ASTLib where module ShellCheck.ASTLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.Prelude
import ShellCheck.Regex
import Control.Monad.Writer import Control.Monad.Writer
import Control.Monad import Control.Monad
@@ -31,7 +34,7 @@ import Data.Maybe
import qualified Data.Map as Map import qualified Data.Map as Map
import Numeric (showHex) import Numeric (showHex)
arguments (T_SimpleCommand _ _ (cmd:args)) = args import Test.QuickCheck
-- Is this a type of loop? -- Is this a type of loop?
isLoop t = case t of isLoop t = case t of
@@ -55,10 +58,28 @@ willSplit x =
T_NormalWord _ l -> any willSplit l T_NormalWord _ l -> any willSplit l
_ -> False _ -> False
isGlob T_Extglob {} = True isGlob t = case t of
isGlob T_Glob {} = True T_Extglob {} -> True
isGlob (T_NormalWord _ l) = any isGlob l T_Glob {} -> True
isGlob _ = False T_NormalWord _ l -> any isGlob l || hasSplitRange l
_ -> False
where
-- foo[x${var}y] gets parsed as foo,[,x,$var,y],
-- so check if there's such an interval
hasSplitRange l =
let afterBracket = dropWhile (not . isHalfOpenRange) l
in any isClosingRange afterBracket
isHalfOpenRange t =
case t of
T_Literal _ "[" -> True
_ -> False
isClosingRange t =
case t of
T_Literal _ str -> ']' `elem` str
_ -> False
-- Is this shell word a constant? -- Is this shell word a constant?
isConstant token = isConstant token =
@@ -116,7 +137,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ] flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args flag (x, '-':args) = map (\v -> (x, [v])) args
flag (x, _) = [ (x, "") ] 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 -- -- Get all flags in a GNU way, up until --
getAllFlags :: Token -> [(Token, String)] getAllFlags :: Token -> [(Token, String)]
@@ -224,6 +245,39 @@ getOpts (gnu, arbitraryLongOpts) string longopts args = process args
listToArgs = map (\x -> ("", (x, x))) listToArgs = map (\x -> ("", (x, x)))
-- Generic getOpts that doesn't rely on a format string, but may also be inaccurate.
-- This provides a best guess interpretation instead of failing when new options are added.
--
-- "--" is treated as end of arguments
-- "--anything[=foo]" is treated as a long option without argument
-- "-any" is treated as -a -n -y, with the next arg as an option to -y unless it starts with -
-- anything else is an argument
getGenericOpts :: [Token] -> [(String, (Token, Token))]
getGenericOpts = process
where
process (token:rest) =
case getLiteralStringDef "\0" token of
"--" -> map (\c -> ("", (c,c))) rest
'-':'-':word -> (takeWhile (`notElem` "\0=") word, (token, token)) : process rest
'-':optString ->
let opts = takeWhile (/= '\0') optString
in
case rest of
next:_ | "-" `isPrefixOf` getLiteralStringDef "\0" next ->
map (\c -> ([c], (token, token))) opts ++ process rest
next:remainder ->
case reverse opts of
last:initial ->
map (\c -> ([c], (token, token))) (reverse initial)
++ [([last], (token, next))]
++ process remainder
[] -> process remainder
[] -> map (\c -> ([c], (token, token))) opts
_ -> ("", (token, token)) : process rest
process [] = []
-- Is this an expansion of multiple items of an array? -- Is this an expansion of multiple items of an array?
isArrayExpansion (T_DollarBraced _ _ l) = isArrayExpansion (T_DollarBraced _ _ l) =
let string = concat $ oversimplify l in let string = concat $ oversimplify l in
@@ -232,14 +286,14 @@ isArrayExpansion (T_DollarBraced _ _ l) =
isArrayExpansion _ = False isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args? -- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
where where
f (T_DollarBraced _ _ l) = f quoted (T_DollarBraced _ _ l) =
let string = concat $ oversimplify l in let string = concat $ oversimplify l in
"!" `isPrefixOf` string not quoted || "!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts f quoted (T_DoubleQuoted _ parts) = any (f True) parts
f (T_NormalWord _ parts) = any f parts f quoted (T_NormalWord _ parts) = any (f quoted) parts
f _ = False f _ _ = False
-- Is it certain that this word will becomes multiple words? -- Is it certain that this word will becomes multiple words?
willBecomeMultipleArgs t = willConcatInAssignment t || f t willBecomeMultipleArgs t = willConcatInAssignment t || f t
@@ -247,7 +301,6 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
f T_Extglob {} = True f T_Extglob {} = True
f T_Glob {} = True f T_Glob {} = True
f T_BraceExpansion {} = True f T_BraceExpansion {} = True
f (T_DoubleQuoted _ parts) = any f parts
f (T_NormalWord _ parts) = any f parts f (T_NormalWord _ parts) = any f parts
f _ = False f _ = False
@@ -315,6 +368,21 @@ getGlobOrLiteralString = getLiteralStringExt f
f (T_Glob _ str) = return str f (T_Glob _ str) = return str
f _ = Nothing 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 -- Maybe get the literal value of a token, using a custom function
-- to map unrecognized Tokens into strings. -- to map unrecognized Tokens into strings.
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
@@ -347,14 +415,15 @@ getLiteralStringExt more = g
'\\' -> '\\' : rest '\\' -> '\\' : rest
'x' -> 'x' ->
case cs of case cs of
(x:y:more) -> (x:y:more) | isHexDigit x && isHexDigit y ->
if isHexDigit x && isHexDigit y chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
then chr (16*(digitToInt x) + (digitToInt y)) : rest (x:more) | isHexDigit x ->
else '\\':c:rest chr (digitToInt x) : decodeEscapes more
more -> '\\' : 'x' : decodeEscapes more
_ | isOctDigit c -> _ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs) let (digits, more) = spanMax isOctDigit 3 (c:cs)
num = parseOct digits num = (parseOct digits) `mod` 256
in (if num < 256 then chr num else '?') : rest in (chr num) : decodeEscapes more
_ -> '\\' : c : rest _ -> '\\' : c : rest
where where
rest = decodeEscapes cs rest = decodeEscapes cs
@@ -362,6 +431,11 @@ getLiteralStringExt more = g
where where
f n "" = n f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest 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 (c:cs) = c : decodeEscapes cs
decodeEscapes [] = [] decodeEscapes [] = []
@@ -470,17 +544,16 @@ getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
return t return t
_ -> fail "" _ -> fail ""
-- If a command substitution is a single command, get its name. -- If a command substitution is a single SimpleCommand, return it.
-- $(date +%s) = Just "date" getSimpleCommandFromExpansion :: Token -> Maybe Token
getCommandNameFromExpansion :: Token -> Maybe String getSimpleCommandFromExpansion t =
getCommandNameFromExpansion t =
case t of case t of
T_DollarExpansion _ [c] -> extract c T_DollarExpansion _ [c] -> extract c
T_Backticked _ [c] -> extract c T_Backticked _ [c] -> extract c
T_DollarBraceCommandExpansion _ [c] -> extract c T_DollarBraceCommandExpansion _ [c] -> extract c
_ -> Nothing _ -> Nothing
where where
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd extract (T_Pipeline _ _ [c]) = getCommand c
extract _ = Nothing extract _ = Nothing
-- Get the basename of a token representing a command -- Get the basename of a token representing a command
@@ -488,6 +561,10 @@ getCommandBasename = fmap basename . getCommandName
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
-- Get the arguments to a command
arguments (T_SimpleCommand _ _ (cmd:args)) = args
arguments t = maybe [] arguments (getCommand t)
isAssignment t = isAssignment t =
case t of case t of
T_Redirecting _ _ w -> isAssignment w T_Redirecting _ _ w -> isAssignment w
@@ -672,3 +749,184 @@ isAnnotationIgnoringCode code t =
where where
hasNum (DisableComment from to) = code >= from && code < to hasNum (DisableComment from to) = code >= from && code < to
hasNum _ = False hasNum _ = False
prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh"
prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash"
prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh"
prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash"
prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash"
prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash"
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
-- Get the shell executable from a string like '/usr/bin/env bash'
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
re = mkRegex "/env +(-S|--split-string=?)? *(.*)"
shellFor s | s `matches` re =
case matchRegex re s of
Just [flag, shell] -> fromEnvArgs (words shell)
_ -> ""
shellFor sb =
case words sb of
[] -> ""
[x] -> basename x
(first:second:args) | basename first == "busybox" ->
case basename second of
"sh" -> "ash" -- busybox sh is ash
x -> x
(first:args) | basename first == "env" ->
fromEnvArgs args
(first:_) -> basename first
fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
-- 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
match <- matchRegex re s
index <- match !!! 0
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 ]
match <- matchRegex re mods
offsets <- match !!! 1
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 t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
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]
_ -> []
commandExpansionShouldBeSplit t = do
cmd <- getSimpleCommandFromExpansion t
name <- getCommandName cmd
case () of
-- Should probably be split
_ | name `elem` ["seq", "pgrep"] -> return True
-- Portage macros that return a single word or nothing
_ | name `elem` ["usev", "use_with", "use_enable"] -> return True
-- Portage macros that are fine as long as the arguments have no spaces
_ | name `elem` ["usex", "meson_use", "meson_feature"] -> do
return . not $ any (' ' `elem`) $ map (getLiteralStringDef " ") $ arguments cmd
_ -> 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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -25,28 +25,31 @@ import ShellCheck.Interface
import Data.List import Data.List
import Data.Monoid import Data.Monoid
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on -- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult analyzeScript :: Monad m => SystemInterface m -> AnalysisSpec -> m AnalysisResult
analyzeScript spec = newAnalysisResult { analyzeScript sys spec = do
params <- makeParameters sys spec
return $ newAnalysisResult {
arComments = arComments =
filterByAnnotation spec params . nub $ filterByAnnotation spec params . nub $
runAnalytics spec runChecker params (checkers spec params)
++ runChecker params (checkers spec params)
} }
where
params = makeParameters spec
checkers spec params = mconcat $ map ($ params) [ checkers spec params = mconcat $ map ($ params) [
ShellCheck.Analytics.checker spec,
ShellCheck.Checks.Commands.checker spec, ShellCheck.Checks.Commands.checker spec,
ShellCheck.Checks.ControlFlow.checker spec,
ShellCheck.Checks.Custom.checker, ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker ShellCheck.Checks.ShellSupport.checker
] ]
optionalChecks = mconcat $ [ optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks, ShellCheck.Analytics.optionalChecks,
ShellCheck.Checks.Commands.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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -23,13 +23,16 @@ module ShellCheck.AnalyzerLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import qualified ShellCheck.CFGAnalysis as CF
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
import ShellCheck.Prelude
import ShellCheck.Regex import ShellCheck.Regex
import Control.Arrow (first) import Control.Arrow (first)
import Control.DeepSeq import Control.DeepSeq
import Control.Monad
import Control.Monad.Identity import Control.Monad.Identity
import Control.Monad.RWS import Control.Monad.RWS
import Control.Monad.State import Control.Monad.State
@@ -79,10 +82,18 @@ composeAnalyzers f g x = f x >> g x
data Parameters = Parameters { data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default. -- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool, hasLastpipe :: Bool,
-- Whether this script has the 'inherit_errexit' option set/default.
hasInheritErrexit :: Bool,
-- Whether this script has 'set -e' anywhere. -- Whether this script has 'set -e' anywhere.
hasSetE :: Bool, hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- Whether this script is an Ebuild file.
isPortage :: Bool,
-- A linear (bad) analysis of data flow -- A linear (bad) analysis of data flow
variableFlow :: [StackData], variableFlow :: [StackData],
-- A map from Id to Token
idMap :: Map.Map Id Token,
-- A map from Id to parent Token -- A map from Id to parent Token
parentMap :: Map.Map Id Token, parentMap :: Map.Map Id Token,
-- The shell type, such as Bash or Ksh -- The shell type, such as Bash or Ksh
@@ -92,9 +103,14 @@ data Parameters = Parameters {
-- The root node of the AST -- The root node of the AST
rootNode :: Token, rootNode :: Token,
-- map from token id to start and end position -- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position) tokenPositions :: Map.Map Id (Position, Position),
-- Result from Control Flow Graph analysis (including data flow analysis)
cfgAnalysis :: CF.CFGAnalysis,
-- A set of additional variables known to be set (TODO: make this more general?)
additionalKnownVariables :: [String]
} deriving (Show) } deriving (Show)
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
data Cache = Cache {} data Cache = Cache {}
@@ -141,8 +157,8 @@ producesComments c s = do
let pr = pScript s let pr = pScript s
prRoot pr prRoot pr
let spec = defaultSpec pr let spec = defaultSpec pr
let params = makeParameters spec let params = runIdentity $ makeParameters (mockedSystemInterface []) spec
return . not . null $ runChecker params c return . not . null $ filterByAnnotation spec params $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note = makeComment severity id code note =
@@ -167,6 +183,8 @@ errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m (
errWithFix = addCommentWithFix ErrorC errWithFix = addCommentWithFix ErrorC
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
warnWithFix = addCommentWithFix WarningC warnWithFix = addCommentWithFix WarningC
infoWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
infoWithFix = addCommentWithFix InfoC
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
styleWithFix = addCommentWithFix StyleC styleWithFix = addCommentWithFix StyleC
@@ -178,28 +196,64 @@ makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix = makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str let comment = makeComment severity id code str
withFix = comment { withFix = comment {
tcFix = Just fix -- If fix is empty, pretend it wasn't there.
tcFix = if null (fixReplacements fix) then Nothing else Just fix
} }
in force withFix in force withFix
makeParameters spec = makeParameters :: Monad m => SystemInterface m -> AnalysisSpec -> m Parameters
let params = Parameters { makeParameters sys spec = do
extraVars <-
if asIsPortage spec
then do
vars <- siGetPortageVariables sys
let classes = getInheritedEclasses root
return $ concatMap (\c -> Map.findWithDefault [] c vars) classes
else
return []
return $ makeParams extraVars
where
shell = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec
makeParams extraVars = params
where
params = Parameters {
rootNode = root, rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, shellType = shell,
hasSetE = containsSetE root, hasSetE = containsSetE root,
hasLastpipe = hasLastpipe =
case shellType params of case shellType params of
Bash -> containsLastpipe root Bash -> isOptionSet "lastpipe" root
Dash -> False Dash -> False
Sh -> False Sh -> False
Ksh -> True, Ksh -> True,
hasInheritErrexit =
case shellType params of
Bash -> isOptionSet "inherit_errexit" root
Dash -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> isOptionSet "pipefail" root
Dash -> True
Sh -> True
Ksh -> isOptionSet "pipefail" root,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
isPortage = asIsPortage spec,
idMap = getTokenMap root,
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec tokenPositions = asTokenPositions spec,
} in params cfgAnalysis = CF.analyzeControlFlow cfParams root,
where root = asScript spec additionalKnownVariables = extraVars
}
cfParams = CF.CFGParameters {
CF.cfLastpipe = hasLastpipe params,
CF.cfPipefail = hasPipefail params,
CF.cfAdditionalInitialVariables = additionalKnownVariables params
}
root = asScript spec
-- Does this script mention 'set -e' anywhere? -- Does this script mention 'set -e' anywhere?
@@ -216,18 +270,30 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False _ -> False
re = mkRegex "[[:space:]]-[^-]*e" re = mkRegex "[[:space:]]-[^-]*e"
-- Does this script mention 'shopt -s lastpipe' anywhere?
-- Also used as a hack. containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root
containsLastpipe 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 isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where where
isShoptLastPipe t = isShoptLastPipe t =
case t of case t of
T_SimpleCommand {} -> T_SimpleCommand {} ->
t `isUnqualifiedCommand` "shopt" && t `isUnqualifiedCommand` "shopt" &&
("lastpipe" `elem` oversimplify t) (shopt `elem` oversimplify t)
_ -> False _ -> False
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
@@ -240,6 +306,8 @@ prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
determineShellTest = determineShellTest' Nothing determineShellTest = determineShellTest' Nothing
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
@@ -253,19 +321,6 @@ determineShell fallbackShell t = fromMaybe Bash $
headOrDefault (fromShebang s) [s | ShellOverride s <- annotations] headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash"
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
shellFor s | "/env " `isInfixOf` s = case matchRegex re s of
Just [flag, shell] -> shell
_ -> ""
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
re = mkRegex "/env +(-S|--split-string=?)? *([^ ]*)"
-- Given a root node, make a map from Id to parent Token. -- Given a root node, make a map from Id to parent Token.
-- This is used to populate parentMap in Parameters -- This is used to populate parentMap in Parameters
getParentTree :: Token -> Map.Map Id Token getParentTree :: Token -> Map.Map Id Token
@@ -298,14 +353,14 @@ isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFree = isQuoteFreeNode False isQuoteFree = isQuoteFreeNode False
isQuoteFreeNode strict tree t = isQuoteFreeNode strict shell tree t =
isQuoteFreeElement t || isQuoteFreeElement t ||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t)) (fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t)
where where
-- Is this node self-quoting in itself? -- Is this node self-quoting in itself?
isQuoteFreeElement t = isQuoteFreeElement t =
case t of case t of
T_Assignment {} -> True T_Assignment {} -> assignmentIsQuoting t
T_FdRedirect {} -> True T_FdRedirect {} -> True
_ -> False _ -> False
@@ -317,7 +372,7 @@ isQuoteFreeNode strict tree t =
TC_Binary _ DoubleBracket _ _ _ -> return True TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True TA_Sequence {} -> return True
T_Arithmetic {} -> return True T_Arithmetic {} -> return True
T_Assignment {} -> return True T_Assignment {} -> return $ assignmentIsQuoting t
T_Redirecting {} -> return False T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True T_DollarDoubleQuoted _ _ -> return True
@@ -329,6 +384,18 @@ isQuoteFreeNode strict tree t =
T_SelectIn {} -> return (not strict) T_SelectIn {} -> return (not strict)
_ -> Nothing _ -> 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 t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
shellParsesParamsAsAssignments = shell /= Sh
-- Is this assignment a parameter to a command like export/typeset/etc?
isAssignmentParamToCommand (T_Assignment id _ _ _ _) =
case Map.lookup id tree of
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
_ -> False
-- Check if a token is a parameter to a certain command by name: -- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t -- Example: isParamTo (parentMap params) "sed" t
isParamTo :: Map.Map Id Token -> String -> Token -> Bool isParamTo :: Map.Map Id Token -> String -> Token -> Bool
@@ -374,12 +441,6 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
getId word == currentId || getId (getCommandTokenOrThis t) == currentId getId word == currentId || getId (getCommandTokenOrThis t) == currentId
go _ _ = False go _ _ = False
-- A list of the element and all its parents up to the root node.
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 -- Version of the above taking the map from the current context
-- Todo: give this the name "getPath" -- Todo: give this the name "getPath"
getPathM t = do getPathM t = do
@@ -477,13 +538,11 @@ getModifiedVariables t =
T_SimpleCommand {} -> T_SimpleCommand {} ->
getModifiedVariableCommand t getModifiedVariableCommand t
TA_Unary _ "++|" v@(TA_Variable _ name _) -> TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->
[(t, v, name, DataString $ SourceFrom [v])] [(t, v, name, DataString SourceInteger)]
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> do TA_Assignment _ op (TA_Variable _ name _) rhs -> do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString $ SourceFrom [rhs]) return (t, t, name, DataString SourceInteger)
T_BatsTest {} -> [ T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal), (t, t, "lines", DataArray SourceExternal),
@@ -493,14 +552,8 @@ getModifiedVariables t =
-- Count [[ -v foo ]] as an "assignment". -- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused. -- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> do TC_Unary id _ "-v" token -> maybeToList $ do
str <- fmap (takeWhile (/= '[')) $ -- Quoted index str <- getVariableForTestDashV token
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> []
guard . not . null $ str
return (t, token, str, DataString SourceChecked) return (t, token, str, DataString SourceChecked)
TC_Unary _ _ "-n" token -> markAsChecked t token TC_Unary _ _ "-n" token -> markAsChecked t token
@@ -533,24 +586,16 @@ getModifiedVariables t =
return (place, t, str, DataString SourceChecked) return (place, t, str, DataString SourceChecked)
_ -> Nothing _ -> Nothing
isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available -- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) = getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of case x of
"declare" -> forDeclare
"typeset" -> forDeclare
"export" -> if "f" `elem` flags "export" -> if "f" `elem` flags
then [] then []
else concatMap getReference rest else concatMap getReference rest
"declare" -> if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
"local" -> if "x" `elem` flags "local" -> if "x" `elem` flags
then concatMap getReference rest then concatMap getReference rest
else [] else []
@@ -559,8 +604,26 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
_ -> [] _ -> []
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token] "alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
-- tc-export makes a list of toolchain variables available, similar to export.
-- Usage tc-export CC CXX
"tc-export" -> concatMap getReference rest
-- tc-export_build_env exports the listed variables plus a bunch of BUILD_XX variables.
-- Usage tc-export_build_env BUILD_CC
"tc-export_build_env" ->
concatMap getReference rest
++ [ (base, base, v) | v <- portageBuildEnvVariables ]
_ -> [] _ -> []
where where
forDeclare =
if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)] getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)] getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = [] getReference _ = []
@@ -600,8 +663,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"export" -> "export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars "declare" -> forDeclare
"typeset" -> declaredVars "typeset" -> forDeclare
"local" -> concatMap getModifierParamString rest "local" -> concatMap getModifierParamString rest
"readonly" -> "readonly" ->
@@ -613,6 +676,7 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
return (base, base, "@", DataString $ SourceFrom params) return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest "printf" -> maybeToList $ getPrintfVariable rest
"wait" -> maybeToList $ getWaitVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest "mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest "readarray" -> maybeToList $ getMapfileArray base rest
@@ -622,6 +686,16 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
"DEFINE_integer" -> maybeToList $ getFlagVariable rest "DEFINE_integer" -> maybeToList $ getFlagVariable rest
"DEFINE_string" -> maybeToList $ getFlagVariable rest "DEFINE_string" -> maybeToList $ getFlagVariable rest
"tc-export" -> concatMap getModifierParamString rest
-- tc-export_build_env exports the listed variables plus a bunch of BUILD_XX variables.
-- Usage tc-export_build_env BUILD_CC
"tc-export_build_env" ->
concatMap getModifierParamString rest
++ [ (base, base, var, DataString $ SourceExternal) |
var <- ["BUILD_" ++ x, x ++ "_FOR_BUILD" ],
x <- portageBuildEnvVariables ]
_ -> [] _ -> []
where where
flags = map snd $ getAllFlags base flags = map snd $ getAllFlags base
@@ -632,6 +706,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]] T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t stripEqualsFrom t = t
forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
declaredVars = concatMap (getModifierParam defaultType) rest declaredVars = concatMap (getModifierParam defaultType) rest
where where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
@@ -670,15 +746,15 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
_ -> return (t:fromMaybe [] (getSetParams rest)) _ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list
where getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
where getFlagAssignedVariable str dataSource maybeFlags = do
(varName, varType) = case elemIndex '[' var of flags <- maybeFlags
Just i -> (take i var, DataArray) (_, (flag, value)) <- find ((== str) . fst) flags
Nothing -> (var, DataString) variableName <- getLiteralStringExt (const $ return "!") value
f (_:rest) = f rest let (baseName, index) = span (/= '[') variableName
f [] = fail "not found" return (base, value, baseName, (if null index then DataString else DataArray) dataSource)
-- mapfile has some curious syntax allowing flags plus 0..n variable names -- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. -- where only the first non-option one is used if any.
@@ -712,24 +788,19 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
getModifiedVariableCommand _ = [] getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do -- Given a NormalWord like foo or foo[$bar], get foo.
match <- matchRegex re s -- Primarily used to get references for [[ -v foo[bar] ]]
index <- match !!! 0 getVariableForTestDashV :: Token -> Maybe String
return $ matchAllStrings variableNameRegex index getVariableForTestDashV t = do
str <- takeWhile ('[' /=) <$> getLiteralStringExt toStr t
guard $ isVariableName str
return str
where where
re = mkRegex "(\\[.*\\])" -- foo[bar] gets parsed with [bar] as a glob, so undo that
toStr (T_Glob _ s) = return s
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"] -- Turn foo[$x] into foo[\0] so that we can get the constant array name
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"] -- in a non-constant expression (while filtering out foo$x[$y])
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"] toStr _ = return "\0"
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 "^(\\[.+\\])? *:([^-=?+].*)"
getReferencedVariables parents t = getReferencedVariables parents t =
case t of case t of
@@ -748,7 +819,7 @@ getReferencedVariables parents t =
TC_Unary id _ "-v" token -> getIfReference t token TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs -> TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op if isDereferencingBinaryOp op
then concatMap (getIfReference t) [lhs, rhs] then concatMap (getIfReference t) [lhs, rhs]
else [] else []
@@ -777,17 +848,16 @@ getReferencedVariables parents t =
T_Glob _ s -> return s -- Also when parsed as globs T_Glob _ s -> return s -- Also when parsed as globs
_ -> [] _ -> []
getIfReference context token = do getIfReference context token = maybeToList $ do
str@(h:_) <- getLiteralStringExt literalizer token str <- getVariableForTestDashV token
when (isDigit h) $ fail "is a number"
return (context, token, getBracedReference str) return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False _ -> False
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v] dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
@@ -810,16 +880,6 @@ isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
isConfusedGlobRegex _ = False 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 = getVariablesFromLiteralToken token =
getVariablesFromLiteral (getLiteralStringDef " " token) getVariablesFromLiteral (getLiteralStringDef " " token)
@@ -832,67 +892,6 @@ getVariablesFromLiteral string =
where where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" 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_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:_) | c `elem` "*@#?-$!" = 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
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
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]
-- Useful generic functions.
-- 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 -- Run a command if the shell is in the given list
whenShell l c = do whenShell l c = do
@@ -945,5 +944,30 @@ isBashLike params =
Dash -> False Dash -> False
Sh -> False Sh -> False
isTrueAssignmentSource c =
case c of
DataString SourceChecked -> False
DataString SourceDeclaration -> False
DataArray SourceChecked -> False
DataArray SourceDeclaration -> False
_ -> True
modifiesVariable params token name =
or $ map check flow
where
flow = getVariableFlow params token
check t =
case t of
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
_ -> False
-- Ebuild files inherit eclasses using 'inherit myclass1 myclass2'
getInheritedEclasses :: Token -> [String]
getInheritedEclasses root = execWriter $ doAnalysis findInheritedEclasses root
where
findInheritedEclasses cmd
| cmd `isCommand` "inherit" = tell $ catMaybes $ getLiteralString <$> (arguments cmd)
findInheritedEclasses _ = return ()
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

1312
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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -20,10 +20,12 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Analyzer
import ShellCheck.ASTLib
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
import ShellCheck.Analyzer
import Data.Char
import Data.Either import Data.Either
import Data.Functor import Data.Functor
import Data.List import Data.List
@@ -53,6 +55,8 @@ shellFromFilename filename = listToMaybe candidates
shellExtensions = [(".ksh", Ksh) shellExtensions = [(".ksh", Ksh)
,(".bash", Bash) ,(".bash", Bash)
,(".bats", Bash) ,(".bats", Bash)
,(".ebuild", Bash)
,(".eclass", Bash)
,(".dash", Dash)] ,(".dash", Dash)]
-- The `.sh` is too generic to determine the shell: -- The `.sh` is too generic to determine the shell:
-- We fallback to Bash in this case and emit SC2148 if there is no shebang -- We fallback to Bash in this case and emit SC2148 if there is no shebang
@@ -83,18 +87,24 @@ checkScript sys spec = do
asShellType = csShellTypeOverride spec, asShellType = csShellTypeOverride spec,
asFallbackShell = shellFromFilename $ csFilename spec, asFallbackShell = shellFromFilename $ csFilename spec,
asCheckSourced = csCheckSourced spec, asCheckSourced = csCheckSourced spec,
asIsPortage = isPortage $ csFilename spec,
asExecutionMode = Executed, asExecutionMode = Executed,
asTokenPositions = tokenPositions, asTokenPositions = tokenPositions,
asOptionalChecks = csOptionalChecks spec asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
} where as = newAnalysisSpec root } where as = newAnalysisSpec root
let analysisMessages = let getAnalysisMessages =
maybe [] case prRoot result of
(arComments . analyzeScript . analysisSpec) Just root -> arComments <$> (analyzeScript sys $ analysisSpec root)
$ prRoot result Nothing -> return []
let translator = tokenToPosition tokenPositions let translator = tokenToPosition tokenPositions
analysisMessages <- getAnalysisMessages
return . nub . sortMessages . filter shouldInclude $ return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages) (parseMessages ++ map translator analysisMessages)
isPortage filename =
let f = map toLower filename in
".ebuild" `isSuffixOf` f || ".eclass" `isSuffixOf` f
shouldInclude pc = shouldInclude pc =
severity <= csMinSeverity spec && severity <= csMinSeverity spec &&
case csIncludedWarnings spec of case csIncludedWarnings spec of
@@ -156,6 +166,11 @@ checkWithIncludesAndSourcePath includes mapper = getErrors
siFindSource = mapper siFindSource = mapper
} }
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
(mockRcFile rc $ mockedSystemInterface includes) {
siFindSource = mapper
}
prop_findsParseIssue = check "echo \"$12\"" == [1037] prop_findsParseIssue = check "echo \"$12\"" == [1037]
prop_commentDisablesParseIssue1 = prop_commentDisablesParseIssue1 =
@@ -238,6 +253,9 @@ prop_canStripPrefixAndSource2 =
prop_canSourceDynamicWhenRedirected = prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
prop_canRedirectWithSpaces =
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
prop_recursiveAnalysis = prop_recursiveAnalysis =
[2086] == checkRecursive [("lib", "echo $1")] "source lib" [2086] == checkRecursive [("lib", "echo $1")] "source lib"
@@ -301,6 +319,13 @@ prop_canDisableShebangWarning = null $ result
csScript = "#shellcheck disable=SC2148\nfoo" csScript = "#shellcheck disable=SC2148\nfoo"
} }
prop_canDisableAllWarnings = result == [2086]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`"
}
prop_canDisableParseErrors = null $ result prop_canDisableParseErrors = null $ result
where where
result = checkWithSpec [] emptyCheckSpec { result = checkWithSpec [] emptyCheckSpec {
@@ -384,7 +409,7 @@ prop_canEnableOptionalsWithRc = result == [2244]
prop_sourcePathRedirectsName = result == [2086] prop_sourcePathRedirectsName = result == [2086]
where where
f "dir/myscript" _ "lib" = return "foo/lib" f "dir/myscript" _ _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib", csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript", csFilename = "dir/myscript",
@@ -393,22 +418,113 @@ prop_sourcePathRedirectsName = result == [2086]
prop_sourcePathAddsAnnotation = result == [2086] prop_sourcePathAddsAnnotation = result == [2086]
where where
f "dir/myscript" ["mypath"] "lib" = return "foo/lib" f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib", csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript", csFilename = "dir/myscript",
csCheckSourced = True 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] prop_sourcePathRedirectsDirective = result == [2086]
where where
f "dir/myscript" _ "lib" = return "foo/lib" f "dir/myscript" _ _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null" f _ _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens", csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript", csFilename = "dir/myscript",
csCheckSourced = True csCheckSourced = True
} }
prop_rcCanAllowExternalSources = result == [2086]
where
f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanDenyExternalSources = result == [2086]
where
f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_rcCanLeaveExternalSourcesUnspecified = result == [2086]
where
f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource mylib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCanDisableExternalSources = result == [2006, 2086]
where
f "dir/myscript" (Just True) _ "withExternal" = return "withExternal"
f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec {
csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources = result == [1144]
where
f "dir/myscript" Nothing _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_fileCannotEnableExternalSources2 = result == [1144]
where
f "dir/myscript" (Just False) _ "foo" = return "foo"
f a b c d = error $ show ("Unexpected", a, b, c, d)
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
csFilename = "dir/myscript",
csCheckSourced = True
}
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"
return [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -27,21 +27,28 @@ module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Co
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib
import ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis as CF
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
import ShellCheck.Prelude
import ShellCheck.Regex import ShellCheck.Regex
import Control.Monad import Control.Monad
import Control.Monad.RWS import Control.Monad.RWS
import Data.Char import Data.Char
import Data.Functor.Identity import Data.Functor.Identity
import qualified Data.Graph.Inductive.Graph as G
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
import Debug.Trace -- STRIP
data CommandName = Exactly String | Basename String data CommandName = Exactly String | Basename String
deriving (Eq, Ord) deriving (Eq, Ord)
@@ -57,7 +64,7 @@ commandChecks :: [CommandCheck]
commandChecks = [ commandChecks = [
checkTr checkTr
,checkFindNameGlob ,checkFindNameGlob
,checkNeedlessExpr ,checkExpr
,checkGrepRe ,checkGrepRe
,checkTrapQuotes ,checkTrapQuotes
,checkReturn ,checkReturn
@@ -95,7 +102,14 @@ commandChecks = [
,checkSourceArgs ,checkSourceArgs
,checkChmodDashr ,checkChmodDashr
,checkXargsDashi ,checkXargsDashi
,checkUnquotedEchoSpaces
,checkEvalArray
] ]
++ map checkArgComparison ("alias" : declaringCommands)
++ map checkMaskedReturns declaringCommands
++ map checkMultipleDeclaring declaringCommands
++ map checkBackreferencingDeclaration declaringCommands
optionalChecks = map fst optionalCommandChecks optionalChecks = map fst optionalCommandChecks
optionalCommandChecks :: [(CheckDescription, CommandCheck)] optionalCommandChecks :: [(CheckDescription, CommandCheck)]
@@ -107,7 +121,7 @@ optionalCommandChecks = [
cdNegative = "command -v javac" cdNegative = "command -v javac"
}, checkWhich) }, checkWhich)
] ]
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
prop_verifyOptionalExamples = all check optionalCommandChecks prop_verifyOptionalExamples = all check optionalCommandChecks
where where
@@ -131,40 +145,52 @@ prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True)
prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" [] prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" []
prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" [] prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" []
prop_checkGenericOptsS1 = checkGetOpts "-f x" ["f"] [] $ return . getGenericOpts
prop_checkGenericOptsS2 = checkGetOpts "-abc x" ["a", "b", "c"] [] $ return . getGenericOpts
prop_checkGenericOptsS3 = checkGetOpts "-abc -x" ["a", "b", "c", "x"] [] $ return . getGenericOpts
prop_checkGenericOptsS4 = checkGetOpts "-x" ["x"] [] $ return . getGenericOpts
-- Long options -- Long options
prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)] prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)] prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" [] prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" []
prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" [] prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" []
prop_checkGenericOptsL1 = checkGetOpts "--foo=bar" ["foo"] [] $ return . getGenericOpts
prop_checkGenericOptsL2 = checkGetOpts "--foo bar" ["foo"] ["bar"] $ return . getGenericOpts
prop_checkGenericOptsL3 = checkGetOpts "-x --foo" ["x", "foo"] [] $ return . getGenericOpts
-- Know when to terminate -- Know when to terminate
prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" [] prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" [] prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" []
prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" [] prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" []
prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" [] prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" []
prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGenericOpts
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck M.empty
where where
addCheck map (CommandCheck name function) = addCheck map (CommandCheck name function) =
Map.insertWith composeAnalyzers name function map M.insertWith composeAnalyzers name function map
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
name <- getLiteralString cmd name <- getLiteralString cmd
return $ return $
if '/' `elem` name if '/' `elem` name
then then
Map.findWithDefault nullCheck (Basename $ basename name) map t M.findWithDefault nullCheck (Basename $ basename name) map t
else if name == "builtin" && not (null rest) then else if name == "builtin" && not (null rest) then
let t' = T_SimpleCommand id cmdPrefix rest let t' = T_SimpleCommand id cmdPrefix rest
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t' in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
else do else do
Map.findWithDefault nullCheck (Exactly name) map t M.findWithDefault nullCheck (Exactly name) map t
Map.findWithDefault nullCheck (Basename name) map t M.findWithDefault nullCheck (Basename name) map t
where where
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
@@ -186,7 +212,7 @@ checker spec params = getChecker $ commandChecks ++ optionals
optionals = optionals =
if "all" `elem` keys if "all" `elem` keys
then map snd optionalCommandChecks then map snd optionalCommandChecks
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys else mapMaybe (\x -> M.lookup x optionalCheckMap) keys
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
@@ -235,19 +261,74 @@ checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acc b acc b
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)" prop_checkExpr = verify checkExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``" prop_checkExpr2 = verify checkExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)" prop_checkExpr3 = verifyNot checkExpr "foo=$(expr foo : regex)"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)" prop_checkExpr4 = verifyNot checkExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where prop_checkExpr5 = verify checkExpr "# shellcheck disable=SC2003\nexpr match foo bar"
f t = prop_checkExpr6 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo : fo*"
prop_checkExpr7 = verify checkExpr "# shellcheck disable=SC2003\nexpr 5 -3"
prop_checkExpr8 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr \"$@\""
prop_checkExpr9 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr 5 $rest"
prop_checkExpr10 = verify checkExpr "# shellcheck disable=SC2003\nexpr length \"$var\""
prop_checkExpr11 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo > bar"
prop_checkExpr12 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 | 2"
prop_checkExpr13 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 * 2"
prop_checkExpr14 = verify checkExpr "# shellcheck disable=SC2003\nexpr \"$x\" >= \"$y\""
checkExpr = CommandCheck (Basename "expr") f where
f t = do
when (all (`notElem` exceptions) (words $ arguments t)) $ when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId $ getCommandTokenOrThis t) 2003 style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]." "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
case arguments t of
[lhs, op, rhs] -> do
checkOp lhs
case getWordParts op of
[T_Glob _ "*"] ->
err (getId op) 2304
"* must be escaped to multiply: \\*. Modern $((x * y)) avoids this issue."
[T_Literal _ ":"] | isGlob rhs ->
warn (getId rhs) 2305
"Quote regex argument to expr to avoid it expanding as a glob."
_ -> return ()
[single] | not (willSplit single) ->
warn (getId single) 2307
"'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
[first, second] |
(fromMaybe "" $ getLiteralString first) /= "length"
&& not (willSplit first || willSplit second) -> do
checkOp first
warn (getId t) 2307
"'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|."
(first:rest) -> do
checkOp first
forM_ rest $ \t ->
-- We already find 95%+ of multiplication and regex earlier, so don't bother classifying this further.
when (isGlob t) $ warn (getId t) 2306 "Escape glob characters in arguments to expr to avoid pathname expansion."
_ -> return ()
-- These operators are hard to replicate in POSIX -- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ] exceptions = [ ":", "<", ">", "<=", ">=",
-- We can offer better suggestions for these
"match", "length", "substr", "index"]
words = mapMaybe getLiteralString words = mapMaybe getLiteralString
checkOp side =
case getLiteralString side of
Just "match" -> msg "'expr match' has unspecified results. Prefer 'expr str : regex'."
Just "length" -> msg "'expr length' has unspecified results. Prefer ${#var}."
Just "substr" -> msg "'expr substr' has unspecified results. Prefer 'cut' or ${var#???}."
Just "index" -> msg "'expr index' has unspecified results. Prefer x=${var%%[chars]*}; $((${#x}+1))."
_ -> return ()
where
msg = info (getId side) 2308
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3" prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3" prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
@@ -400,9 +481,16 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'" prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'"
prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'"
prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'"
prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'"
prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'"
prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'"
prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where where
hasEscapes = mkRegex "\\\\[rnt]" hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})"
f cmd = f cmd =
whenShell [Sh, Bash, Ksh] $ whenShell [Sh, Bash, Ksh] $
unless (cmd `hasFlag` "e") $ unless (cmd `hasFlag` "e") $
@@ -602,6 +690,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
let formats = getPrintfFormats string let formats = getPrintfFormats string
let formatCount = length formats let formatCount = length formats
let argCount = length more let argCount = length more
let pluraliseIfMany word n = if n > 1 then word ++ "s" else word
return $ if return $ if
| argCount == 0 && formatCount == 0 -> | argCount == 0 && formatCount == 0 ->
@@ -617,7 +706,8 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
return () -- Great: a suitable number of arguments return () -- Great: a suitable number of arguments
| otherwise -> | otherwise ->
warn (getId format) 2183 $ warn (getId format) 2183 $
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments." "This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++
", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "."
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $ unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
info (getId format) 2059 info (getId format) 2059
@@ -663,7 +753,7 @@ getPrintfFormats = getFormats
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ / -- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
-- V V V V V -- V V V V V
-- flags field width precision format character rest -- flags field width precision format character rest
-- field width and precision can be specified with a '*' instead of a digit, -- field width and precision can be specified with an '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used -- in which case printf will accept one more argument for each '*' used
@@ -718,6 +808,7 @@ prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var" prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1" prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}" prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
prop_checkReadExpansions9 = verify checkReadExpansions "read arr[val]"
checkReadExpansions = CommandCheck (Exactly "read") check checkReadExpansions = CommandCheck (Exactly "read") check
where where
options = getGnuOpts flagsForRead options = getGnuOpts flagsForRead
@@ -725,13 +816,26 @@ checkReadExpansions = CommandCheck (Exactly "read") check
opts <- options $ arguments cmd opts <- options $ arguments cmd
return [y | (x,(_, y)) <- opts, null x || x == "a"] return [y | (x,(_, y)) <- opts, null x || x == "a"]
check cmd = mapM_ warning $ getVars cmd check cmd = do
warning t = sequence_ $ do mapM_ dollarWarning $ getVars cmd
mapM_ arrayWarning $ arguments cmd
dollarWarning t = sequence_ $ do
name <- getSingleUnmodifiedBracedString t name <- getSingleUnmodifiedBracedString t
guard $ isVariableName name -- e.g. not $1 guard $ isVariableName name -- e.g. not $1
return . warn (getId t) 2229 $ return . warn (getId t) 2229 $
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
arrayWarning word =
when (any isUnquotedBracket $ getWordParts word) $
warn (getId word) 2313 $
"Quote array indices to avoid them expanding as globs."
isUnquotedBracket t =
case t of
T_Glob _ ('[':_) -> True
_ -> False
-- Return the single variable expansion that makes up this word, if any. -- Return the single variable expansion that makes up this word, if any.
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing -- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
getSingleUnmodifiedBracedString :: Token -> Maybe String getSingleUnmodifiedBracedString :: Token -> Maybe String
@@ -771,6 +875,9 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]" prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo" prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
prop_checkUnsetGlobs3 = verify checkUnsetGlobs "unset foo[$i]"
prop_checkUnsetGlobs4 = verify checkUnsetGlobs "unset foo[x${i}y]"
prop_checkUnsetGlobs5 = verifyNot checkUnsetGlobs "unset foo]["
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments) checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where where
check arg = check arg =
@@ -852,6 +959,22 @@ checkLocalScope = CommandCheck (Exactly "local") $ \t ->
unless (any isFunctionLike path) $ unless (any isFunctionLike path) $
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }"
prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }"
prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5"
prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5"
prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }"
prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5"
prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5"
checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments)
where
check t = sequence_ $ do
lit <- getUnquotedLiteral t
guard $ lit `elem` declaringCommands
return $ err (getId $ getCommandTokenOrThis t) 2316 $
"This applies " ++ cmd ++ " to the variable named " ++ lit ++
", which is probably not what you want. Use a separate command or the appropriate `declare` options instead."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)" prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)" prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $ checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
@@ -870,34 +993,48 @@ prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done" prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done" prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done" prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
prop_checkWhileGetoptsCase6 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $y in a) foo;; esac; done"
prop_checkWhileGetoptsCase7 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case x$x in xa) foo;; xb) foo;; esac; done"
prop_checkWhileGetoptsCase8 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do x=a; case $x in a) foo;; esac; done"
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
where where
f :: Token -> Analysis f :: Token -> Analysis
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do f t@(T_SimpleCommand _ _ (cmd:arg1:name:_)) = do
path <- getPathM t path <- getPathM t
params <- ask
sequence_ $ do sequence_ $ do
options <- getLiteralString arg1 options <- getLiteralString arg1
getoptsVar <- getLiteralString name
(T_WhileExpression _ _ body) <- findFirst whileLoop path (T_WhileExpression _ _ body) <- findFirst whileLoop path
caseCmd <- mapMaybe findCase body !!! 0 caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0
-- Make sure getopts name and case variable matches
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
[T_Literal _ caseVar] <- return $ getWordParts bracedWord
guard $ caseVar == getoptsVar
-- Make sure the variable isn't modified
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
f _ = return () f _ = return ()
check :: Id -> [String] -> Token -> Analysis check :: Id -> [String] -> Token -> Analysis
check optId opts (T_CaseExpression id _ list) = do check optId opts (T_CaseExpression id _ list) = do
unless (Nothing `Map.member` handledMap) $ do unless (Nothing `M.member` handledMap) $ do
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $ unless (any (`M.member` handledMap) [Just "*",Just "?"]) $
warn id 2220 "Invalid flags are not handled. Add a *) case." warn id 2220 "Invalid flags are not handled. Add a *) case."
mapM_ warnRedundant $ Map.toList notRequested mapM_ warnRedundant $ M.toList notRequested
where where
handledMap = Map.fromList (concatMap getHandledStrings list) handledMap = M.fromList (concatMap getHandledStrings list)
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts requestedMap = M.fromList $ map (\x -> (Just x, ())) opts
notHandled = Map.difference requestedMap handledMap notHandled = M.difference requestedMap handledMap
notRequested = Map.difference handledMap requestedMap notRequested = M.difference handledMap requestedMap
warnUnhandled optId caseId str = warnUnhandled optId caseId str =
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'." warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
@@ -1056,7 +1193,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
prop_checkWhich = verify checkWhich "which '.+'" prop_checkWhich = verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "which") $ checkWhich = CommandCheck (Basename "which") $
\t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead." \t -> info (getId $ getCommandTokenOrThis t) 2230 "'which' is non-standard. Use builtin 'command -v' instead."
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input" prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
@@ -1143,5 +1280,192 @@ checkXargsDashi = CommandCheck (Basename "xargs") f
return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}" return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:" parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
prop_checkArgComparison1 = verify (checkArgComparison "declare") "declare a = b"
prop_checkArgComparison2 = verify (checkArgComparison "declare") "declare a =b"
prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b"
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0"
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
where
wordsWithEqual t = mapM_ check $ arguments t
check arg = do
sequence_ $ do
str <- getLeadingUnquotedString arg
case str of
'=':_ ->
return $ err (headId arg) 2290 $
"Remove spaces around = to assign."
'+':'=':_ ->
return $ err (headId arg) 2290 $
"Remove spaces around += to append."
_ -> Nothing
-- 'let' is parsed as a sequence of arithmetic expansions,
-- so we want the additional warning for "x="
when (cmd == "let") $ sequence_ $ do
token <- getTrailingUnquotedLiteral arg
str <- getLiteralString token
guard $ "=" `isSuffixOf` str
return $ err (getId token) 2290 $
"Remove spaces around = to assign."
headId t =
case t of
T_NormalWord _ (x:_) -> getId x
_ -> getId t
prop_checkMaskedReturns1 = verify (checkMaskedReturns "local") "f() { local a=$(false); }"
prop_checkMaskedReturns2 = verify (checkMaskedReturns "declare") "declare a=$(false)"
prop_checkMaskedReturns3 = verify (checkMaskedReturns "declare") "declare a=\"`false`\""
prop_checkMaskedReturns4 = verify (checkMaskedReturns "readonly") "readonly a=$(false)"
prop_checkMaskedReturns5 = verify (checkMaskedReturns "readonly") "readonly a=\"`false`\""
prop_checkMaskedReturns6 = verifyNot (checkMaskedReturns "declare") "declare a; a=$(false)"
prop_checkMaskedReturns7 = verifyNot (checkMaskedReturns "local") "f() { local -r a=$(false); }"
prop_checkMaskedReturns8 = verifyNot (checkMaskedReturns "readonly") "a=$(false); readonly a"
prop_checkMaskedReturns9 = verify (checkMaskedReturns "typeset") "#!/bin/ksh\n f() { typeset -r x=$(false); }"
prop_checkMaskedReturns10 = verifyNot (checkMaskedReturns "typeset") "#!/bin/ksh\n function f { typeset -r x=$(false); }"
prop_checkMaskedReturns11 = verifyNot (checkMaskedReturns "typeset") "#!/bin/bash\n f() { typeset -r x=$(false); }"
prop_checkMaskedReturns12 = verify (checkMaskedReturns "typeset") "typeset -r x=$(false);"
prop_checkMaskedReturns13 = verify (checkMaskedReturns "typeset") "f() { typeset -g x=$(false); }"
prop_checkMaskedReturns14 = verify (checkMaskedReturns "declare") "declare x=${ false; }"
prop_checkMaskedReturns15 = verify (checkMaskedReturns "declare") "f() { declare x=$(false); }"
checkMaskedReturns str = CommandCheck (Exactly str) checkCmd
where
checkCmd t = do
path <- getPathM t
shell <- asks shellType
sequence_ $ do
name <- getCommandName t
let flags = map snd (getAllFlags t)
let hasDashR = "r" `elem` flags
let hasDashG = "g" `elem` flags
let isInScopedFunction = any (isScopedFunction shell) path
let isLocal = not hasDashG && isLocalInFunction name && isInScopedFunction
let isReadOnly = name == "readonly" || hasDashR
-- Don't warn about local variables that are declared readonly,
-- because the workaround `local x; x=$(false); local -r x;` is annoying
guard . not $ isLocal && isReadOnly
return $ mapM_ checkArgs $ arguments t
checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
warn id 2155 "Declare and assign separately to avoid masking return values."
checkArgs _ = return ()
isLocalInFunction = (`elem` ["local", "declare", "typeset"])
isScopedFunction shell t =
case t of
T_BatsTest {} -> True
-- In ksh, only functions declared with 'function' have their own scope
T_Function _ (FunctionKeyword hasFunction) _ _ _ -> shell /= Ksh || hasFunction
_ -> False
hasReturn t = case t of
T_Backticked {} -> True
T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True
_ -> False
prop_checkUnquotedEchoSpaces1 = verify checkUnquotedEchoSpaces "echo foo bar"
prop_checkUnquotedEchoSpaces2 = verifyNot checkUnquotedEchoSpaces "echo foo"
prop_checkUnquotedEchoSpaces3 = verifyNot checkUnquotedEchoSpaces "echo foo bar"
prop_checkUnquotedEchoSpaces4 = verifyNot checkUnquotedEchoSpaces "echo 'foo bar'"
prop_checkUnquotedEchoSpaces5 = verifyNot checkUnquotedEchoSpaces "echo a > myfile.txt b"
prop_checkUnquotedEchoSpaces6 = verifyNot checkUnquotedEchoSpaces " echo foo\\\n bar"
checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
where
check t = do
let args = arguments t
m <- asks tokenPositions
redir <- getClosestCommandM t
sequence_ $ do
let positions = mapMaybe (\c -> M.lookup (getId c) m) args
let pairs = zip positions (drop 1 positions)
(T_Redirecting _ redirTokens _) <- redir
let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens
guard $ any (hasSpacesBetween redirPositions) pairs
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
hasSpacesBetween redirs ((a,b), (c,d)) =
posLine a == posLine d
&& ((posColumn c) - (posColumn b)) >= 4
&& not (any (\x -> b < x && x < c) redirs)
prop_checkEvalArray1 = verify checkEvalArray "eval $@"
prop_checkEvalArray2 = verify checkEvalArray "eval \"${args[@]}\""
prop_checkEvalArray3 = verify checkEvalArray "eval \"${args[@]@Q}\""
prop_checkEvalArray4 = verifyNot checkEvalArray "eval \"${args[*]@Q}\""
prop_checkEvalArray5 = verifyNot checkEvalArray "eval \"$*\""
checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordParts . arguments)
where
check t =
when (isArrayExpansion t) $
if isEscaped t
then style (getId t) 2293 "When eval'ing @Q-quoted words, use * rather than @ as the index."
else warn (getId t) 2294 "eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string)."
isEscaped q =
case q of
-- Match ${arr[@]@Q} and ${@@Q} and such
T_DollarBraced _ _ l -> 'Q' `elem` getBracedModifier (concat $ oversimplify l)
_ -> False
prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x"
prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))"
prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)"
prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z"
prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
where
check t = foldM_ perArg M.empty $ arguments t
perArg leftArgs t =
case t of
T_Assignment id _ name idx t -> do
warnIfBackreferencing leftArgs $ t:idx
return $ M.insert name id leftArgs
t -> do
warnIfBackreferencing leftArgs [t]
return leftArgs
warnIfBackreferencing backrefs l = do
references <- findReferences l
let reused = M.intersection backrefs references
mapM msg $ M.toList reused
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
findReferences list = do
cfga <- asks cfgAnalysis
let graph = CF.graph cfga
let nodesMap = CF.tokenToNodes cfga
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list
let labels = mapMaybe (G.lab graph) $ S.toList nodes
let references = M.fromList $ concatMap refFromLabel labels
return references
refFromLabel lab =
case lab of
CFApplyEffects effects -> mapMaybe refFromEffect effects
_ -> []
refFromEffect e =
case e of
IdTagged id (CFReadVariable name) -> return (name, id)
_ -> Nothing
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

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
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. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -25,6 +25,7 @@ import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Prelude
import ShellCheck.Regex import ShellCheck.Regex
import Control.Monad import Control.Monad
@@ -59,6 +60,8 @@ checks = [
,checkBraceExpansionVars ,checkBraceExpansionVars
,checkMultiDimensionalArrays ,checkMultiDimensionalArrays
,checkPS1Assignments ,checkPS1Assignments
,checkMultipleBangs
,checkBangAfterPipe
] ]
testChecker (ForShell _ t) = testChecker (ForShell _ t) =
@@ -134,6 +137,8 @@ prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file" prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1" prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2" 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_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar" prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}" prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
@@ -180,7 +185,12 @@ prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_" prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}" prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}" prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
prop_checkBashisms99 = verifyNot checkBashisms "#!/bin/dash\necho [^f]oo" 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 ''"
checkBashisms = ForShell [Sh, Dash] $ \t -> do checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
@@ -224,7 +234,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
warnMsg id 3018 $ filter (/= '|') op ++ " is" warnMsg id 3018 $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& 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 ('{':_) _) = warnMsg id 3022 "named file descriptors are"
bashism (T_FdRedirect id num _) bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are" | all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
@@ -235,7 +246,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
where where
file = onlyLiteralString word file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"] isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
bashism (T_Glob id str) | not isDash && "[^" `isInfixOf` str = bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id 3026 "^ in place of ! in glob bracket expressions is" warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Variable id str _) | isBashVariable str = bashism t@(TA_Variable id str _) | isBashVariable str =
@@ -375,6 +386,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
let literal = onlyLiteralString format let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) 3050 "printf %q is" return $ warnMsg (getId format) 3050 "printf %q is"
when (name == "read" && all isFlag rest) $
warnMsg (getId cmd) 3061 "read without a variable is"
where where
unsupportedCommands = [ unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
@@ -554,5 +568,29 @@ checkPS1Assignments = ForShell [Bash] f
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
checkMultipleBangs = ForShell [Dash, 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, 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 ()
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -2,33 +2,54 @@ module ShellCheck.Data where
import ShellCheck.Interface import ShellCheck.Interface
import Data.Version (showVersion) 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 shellcheckVersion = showVersion version
Use:
-- import Paths_ShellCheck (version)
shellcheckVersion = "kludge"
-}
import Paths_ShellCheck (version)
shellcheckVersion = showVersion version -- VERSIONSTRING
internalVariables = [ internalVariables = [
-- Generic -- Generic
"", "_", "rest", "REST", "", "_", "rest", "REST",
-- Bash -- Bash
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC", "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING", "BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS", "BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS", "COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH", "READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE", "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", "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS", "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL", "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT", "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
"auto_resume", "histchars",
-- Other -- Other
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY", "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
@@ -41,15 +62,104 @@ internalVariables = [
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return" "flags_error", "flags_return"
] ++ portageManualInternalVariables
portageManualInternalVariables = [
-- toolchain settings
"CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS", "FFLAGS", "FCFLAGS",
"CBUILD", "CHOST", "MAKEOPTS",
-- TODO: Delete these if we can handle `tc-export CC` implicit export.
"CC", "CPP", "CXX",
-- portage internals
"EBUILD_PHASE", "EBUILD_SH_ARGS", "EMERGE_FROM", "FILESDIR",
"MERGE_TYPE", "PM_EBUILD_HOOK_DIR", "PORTAGE_ACTUAL_DISTDIR",
"PORTAGE_ARCHLIST", "PORTAGE_BASHRC", "PORTAGE_BINPKG_FILE",
"PORTAGE_BINPKG_TAR_OPTS", "PORTAGE_BINPKG_TMPFILE", "PORTAGE_BIN_PATH",
"PORTAGE_BUILDDIR", "PORTAGE_BUILD_GROUP", "PORTAGE_BUILD_USER",
"PORTAGE_BUNZIP2_COMMAND", "PORTAGE_BZIP2_COMMAND", "PORTAGE_COLORMAP",
"PORTAGE_CONFIGROOT", "PORTAGE_DEBUG", "PORTAGE_DEPCACHEDIR",
"PORTAGE_EBUILD_EXIT_FILE", "PORTAGE_ECLASS_LOCATIONS", "PORTAGE_GID",
"PORTAGE_GRPNAME", "PORTAGE_INST_GID", "PORTAGE_INST_UID",
"PORTAGE_INTERNAL_CALLER", "PORTAGE_IPC_DAEMON", "PORTAGE_IUSE",
"PORTAGE_LOG_FILE", "PORTAGE_MUTABLE_FILTERED_VARS",
"PORTAGE_OVERRIDE_EPREFIX", "PORTAGE_PYM_PATH", "PORTAGE_PYTHON",
"PORTAGE_PYTHONPATH", "PORTAGE_READONLY_METADATA", "PORTAGE_READONLY_VARS",
"PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT",
"PORTAGE_SAVED_READONLY_VARS", "PORTAGE_SIGPIPE_STATUS", "PORTAGE_TMPDIR",
"PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME", "PORTAGE_VERBOSE",
"PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE", "REPLACING_VERSIONS",
"REPLACED_BY_VERSION", "__PORTAGE_HELPER", "__PORTAGE_TEST_HARDLINK_LOCKS",
-- generic ebuilds
"A", "ARCH", "BDEPEND", "BOARD_USE", "BROOT", "CATEGORY", "D",
"DEFINED_PHASES", "DEPEND", "DESCRIPTION", "DISTDIR", "DOCS", "EAPI",
"ECLASS", "ED", "EPREFIX", "EROOT", "ESYSROOT", "EXTRA_ECONF",
"EXTRA_EINSTALL", "EXTRA_MAKE", "FEATURES", "FILESDIR", "HOME", "HOMEPAGE",
"HTML_DOCS", "INHERITED", "IUSE", "KEYWORDS", "LICENSE", "P", "PATCHES",
"PDEPEND", "PF", "PKG_INSTALL_MASK", "PKGUSE", "PN", "PR", "PROPERTIES",
"PROVIDES_EXCLUDE", "PV", "PVR", "QA_AM_MAINTAINER_MODE",
"QA_CONFIGURE_OPTIONS", "QA_DESKTOP_FILE", "QA_DT_NEEDED", "QA_EXECSTACK",
"QA_FLAGS_IGNORED", "QA_MULTILIB_PATHS", "QA_PREBUILT", "QA_PRESTRIPPED",
"QA_SONAME", "QA_SONAME_NO_SYMLINK", "QA_TEXTRELS", "QA_WX_LOAD", "RDEPEND",
"REPOSITORY", "REQUIRED_USE", "REQUIRES_EXCLUDE", "RESTRICT", "ROOT", "S",
"SLOT", "SRC_TEST", "SRC_URI", "STRIP_MASK", "SUBSLOT", "SYSROOT", "T",
"WORKDIR",
-- autotest.eclass declared incorrectly
"AUTOTEST_CLIENT_TESTS", "AUTOTEST_CLIENT_SITE_TESTS",
"AUTOTEST_SERVER_TESTS", "AUTOTEST_SERVER_SITE_TESTS", "AUTOTEST_CONFIG",
"AUTOTEST_DEPS", "AUTOTEST_PROFILERS", "AUTOTEST_CONFIG_LIST",
"AUTOTEST_DEPS_LIST", "AUTOTEST_PROFILERS_LIST",
-- cros-board.eclass declared incorrectly
"CROS_BOARDS",
-- Undeclared cros-kernel2 vars
"AFDO_PROFILE_VERSION",
-- haskell-cabal.eclass declared incorrectly
"CABAL_FEATURES",
-- Undeclared haskell-cabal.eclass vars
"CABAL_CORE_LIB_GHC_PV",
-- Undeclared readme.gentoo.eclass vars
"DOC_CONTENTS",
-- Backwards compatibility perl-module.eclass vars
"MODULE_AUTHOR", "MODULE_VERSION",
-- Undeclared perl-module.eclass vars
"mydoc",
-- python-utils-r1.eclass declared incorrectly
"RESTRICT_PYTHON_ABIS", "PYTHON_MODNAME",
-- ABI variables
"ABI", "DEFAULT_ABI",
-- AFDO variables
"AFDO_LOCATION",
-- Linguas
"LINGUAS"
] ]
specialVariablesWithoutSpaces = [
"$", "-", "?", "!", "#" specialIntegerVariables = [
"$", "?", "!", "#"
] ]
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO", "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID", "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES" "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
"HISTSIZE", "LINES"
-- shflags -- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
@@ -64,7 +174,9 @@ unbracedVariables = specialVariables ++ [
arrayVariables = [ arrayVariables = [
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO", "BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC", "BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY" "DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY",
-- For Portage
"PATCHES"
] ]
commonCommands = [ commonCommands = [
@@ -95,10 +207,10 @@ commonCommands = [
nonReadingCommands = [ nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown", "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", "getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir", "locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname" "set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
] ]
sampleWords = [ sampleWords = [
@@ -124,6 +236,11 @@ unaryTestOps = [
"-o", "-v", "-R" "-o", "-v", "-R"
] ]
-- Variables inspected by Portage tc-export_build_env
portageBuildEnvVariables = [
"CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS"
]
shellForExecutable :: String -> Maybe Shell shellForExecutable :: String -> Maybe Shell
shellForExecutable name = shellForExecutable name =
case name of case name of
@@ -138,3 +255,6 @@ shellForExecutable name =
_ -> Nothing _ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:" flagsForRead = "sreu:n:N:i:p:a:t:"
flagsForMapfile = "d:n:O:s:u:C:c:t"
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]

314
src/ShellCheck/Debug.hs Normal file
View File

@@ -0,0 +1,314 @@
{-
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,
cfAdditionalInitialVariables = []
}
-- 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 module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Prelude
import Control.Monad
import Control.Monad.State import Control.Monad.State
import Data.Array import Data.Array
import Data.List import Data.List
@@ -35,7 +37,7 @@ class Ranged a where
end :: a -> Position end :: a -> Position
overlap :: a -> a -> Bool overlap :: a -> a -> Bool
overlap x y = overlap x y =
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart) xEnd > yStart && yEnd > xStart
where where
yStart = start y yStart = start y
yEnd = end y yEnd = end y
@@ -86,6 +88,7 @@ instance Ranged Replacement where
instance Monoid Fix where instance Monoid Fix where
mempty = newFix mempty = newFix
mappend = (<>) mappend = (<>)
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
instance Semigroup Fix where instance Semigroup Fix where
f1 <> f2 = f1 <> f2 =
@@ -228,7 +231,7 @@ applyReplacement2 rep string = do
let (l1, l2) = tmap posLine originalPos in let (l1, l2) = tmap posLine originalPos in
when (l1 /= 1 || l2 /= 1) $ 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 replacer = repString rep
let shift = (length replacer) - (oldEnd - oldStart) let shift = (length replacer) - (oldEnd - oldStart)

View File

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

View File

@@ -38,9 +38,6 @@ import System.FilePath
import Test.QuickCheck import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter format :: FormatterOptions -> IO Formatter
format options = do format options = do
foundIssues <- newIORef False foundIssues <- newIORef False
@@ -90,7 +87,7 @@ reportResult foundIssues reportedIssues color result sys = do
mapM_ output $ M.toList fixmap mapM_ output $ M.toList fixmap
where where
output (name, fix) = do output (name, fix) = do
file <- (siReadFile sys) name file <- siReadFile sys (Just True) name
case file of case file of
Right contents -> do Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix putStrLn $ formatDoc color $ makeDiff name contents fix
@@ -206,10 +203,9 @@ formatDoc color (DiffDoc name lf regions) =
buildFixMap :: [Fix] -> M.Map String Fix buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile buildFixMap fixes = perFile
where where
splitFixes = concatMap splitFixByFile fixes splitFixes = splitFixByFile $ mconcat fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes 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 -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix) splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where where

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON1 (format) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.DeepSeq
import Data.Aeson import Data.Aeson
import Data.IORef import Data.IORef
import Data.Monoid import Data.Monoid
@@ -117,10 +118,10 @@ collectResult ref cr sys = mapM_ f groups
f :: [PositionedComment] -> IO () f :: [PositionedComment] -> IO ()
f group = do f group = do
let filename = sourceFile (head group) let filename = sourceFile (head group)
result <- siReadFile sys filename result <- siReadFile sys (Just True) filename
let contents = either (const "") id result let contents = either (const "") id result
let comments' = makeNonVirtual comments contents let comments' = makeNonVirtual comments contents
modifyIORef ref (\x -> comments' ++ x) deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
finish ref = do finish ref = do
list <- readIORef ref list <- readIORef ref

View File

@@ -23,6 +23,7 @@ import ShellCheck.Fixer
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.DeepSeq
import Control.Monad import Control.Monad
import Data.Array import Data.Array
import Data.Foldable import Data.Foldable
@@ -88,7 +89,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
appendComments errRef comments max = do appendComments errRef comments max = do
previous <- readIORef errRef previous <- readIORef errRef
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments 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 where
fst3 (x,_,_) = x fst3 (x,_,_) = x
equal x y = fst3 x == fst3 y equal x y = fst3 x == fst3 y
@@ -121,7 +122,7 @@ outputResult options ref result sys = do
outputForFile color sys comments = do outputForFile color sys comments = do
let fileName = sourceFile (head comments) let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName result <- siReadFile sys (Just True) fileName
let contents = either (const "") id result let contents = either (const "") id result
let fileLinesList = lines contents let fileLinesList = lines contents
let lineCount = length fileLinesList let lineCount = length fileLinesList
@@ -174,7 +175,7 @@ showFixedString color comments lineNum fileLines =
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String
cuteIndent comment = cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++ replicate (fromIntegral $ colNo comment - 1) ' ' ++
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment
where where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^" arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow = makeArrow =

View File

@@ -25,7 +25,7 @@ module ShellCheck.Interface
, CheckResult(crFilename, crComments) , CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot) , ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks) , AnalysisSpec(..)
, AnalysisResult(arComments) , AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount) , FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash) , Shell(Ksh, Sh, Bash, Dash)
@@ -39,11 +39,12 @@ module ShellCheck.Interface
, ColorOption(ColorAuto, ColorAlways, ColorNever) , ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix) , TokenComment(tcId, tcComment, tcFix)
, emptyCheckResult , emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult , newAnalysisResult
, newAnalysisSpec
, newFormatterOptions , newFormatterOptions
, newParseResult
, newPosition , newPosition
, newSystemInterface
, newTokenComment , newTokenComment
, mockedSystemInterface , mockedSystemInterface
, mockRcFile , mockRcFile
@@ -73,16 +74,22 @@ import qualified Data.Map as Map
data SystemInterface m = SystemInterface { data SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error -- | Given:
siReadFile :: String -> m (Either ErrorMessage String), -- What annotations say about including external files (if anything)
-- Given: -- A resolved filename from siFindSource
-- Read the file or return an error
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
-- | Given:
-- the current script, -- the current script,
-- what annotations say about including external files (if anything)
-- a list of source-path annotations in effect, -- a list of source-path annotations in effect,
-- and a sourced file, -- and a sourced file,
-- find the sourced file -- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath, siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
-- Get the configuration file (name, contents) for a filename -- | Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String)) siGetConfig :: String -> m (Maybe (FilePath, String)),
-- | Look up Portage Eclass variables
siGetPortageVariables :: m (Map.Map String [String])
} }
-- ShellCheck input and output -- ShellCheck input and output
@@ -131,6 +138,15 @@ newParseSpec = ParseSpec {
psShellTypeOverride = Nothing psShellTypeOverride = Nothing
} }
newSystemInterface :: Monad m => SystemInterface m
newSystemInterface =
SystemInterface {
siReadFile = \_ _ -> return $ Left "Not implemented",
siFindSource = \_ _ _ name -> return name,
siGetConfig = \_ -> return Nothing,
siGetPortageVariables = return Map.empty
}
-- Parser input and output -- Parser input and output
data ParseSpec = ParseSpec { data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
@@ -160,6 +176,7 @@ data AnalysisSpec = AnalysisSpec {
asFallbackShell :: Maybe Shell, asFallbackShell :: Maybe Shell,
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool, asCheckSourced :: Bool,
asIsPortage :: Bool,
asOptionalChecks :: [String], asOptionalChecks :: [String],
asTokenPositions :: Map.Map Id (Position, Position) asTokenPositions :: Map.Map Id (Position, Position)
} }
@@ -170,6 +187,7 @@ newAnalysisSpec token = AnalysisSpec {
asFallbackShell = Nothing, asFallbackShell = Nothing,
asExecutionMode = Executed, asExecutionMode = Executed,
asCheckSourced = False, asCheckSourced = False,
asIsPortage = False,
asOptionalChecks = [], asOptionalChecks = [],
asTokenPositions = Map.empty asTokenPositions = Map.empty
} }
@@ -307,17 +325,17 @@ data ColorOption =
-- For testing -- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface { mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
siReadFile = rf, siReadFile = rf,
siFindSource = fs, siFindSource = fs,
siGetConfig = const $ return Nothing siGetConfig = const $ return Nothing
} }
where where
rf file = return $ rf _ file = return $
case find ((== file) . fst) files of case find ((== file) . fst) files of
Nothing -> Left "File not included in mock." Nothing -> Left "File not included in mock."
Just (_, contents) -> Right contents Just (_, contents) -> Right contents
fs _ _ file = return file fs _ _ _ file = return file
mockRcFile rcfile mock = mock { mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile) siGetConfig = const . return $ Just (".shellcheckrc", rcfile)

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2022 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -24,9 +24,10 @@
module ShellCheck.Parser (parseScript, runTests) where module ShellCheck.Parser (parseScript, runTests) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib hiding (runTests)
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Prelude
import Control.Applicative ((<*), (*>)) import Control.Applicative ((<*), (*>))
import Control.Monad import Control.Monad
@@ -37,7 +38,6 @@ import Data.Functor
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find) import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
import Data.Maybe import Data.Maybe
import Data.Monoid import Data.Monoid
import Debug.Trace
import GHC.Exts (sortWith) import GHC.Exts (sortWith)
import Prelude hiding (readList) import Prelude hiding (readList)
import System.IO import System.IO
@@ -46,7 +46,7 @@ import Text.Parsec.Error
import Text.Parsec.Pos import Text.Parsec.Pos
import qualified Control.Monad.Reader as Mr import qualified Control.Monad.Reader as Mr
import qualified Control.Monad.State as Ms import qualified Control.Monad.State as Ms
import qualified Data.Map as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (quickCheckAll) import Test.QuickCheck.All (quickCheckAll)
@@ -66,7 +66,7 @@ doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_" variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_" variableChars = upper <|> lower <|> digit <|> oneOf "_"
-- Chars to allow in function names -- Chars to allow in function names
functionChars = variableChars <|> oneOf ":+?-./^@" functionChars = variableChars <|> oneOf ":+?-./^@,"
-- Chars to allow in functions using the 'function' keyword -- Chars to allow in functions using the 'function' keyword
extendedFunctionChars = functionChars <|> oneOf "[]*=!" extendedFunctionChars = functionChars <|> oneOf "[]*=!"
specialVariable = oneOf (concat specialVariables) specialVariable = oneOf (concat specialVariables)
@@ -87,11 +87,23 @@ extglobStart = oneOf extglobStartChars
unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036" unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
unicodeSingleQuotes = "\x2018\x2019" unicodeSingleQuotes = "\x2018\x2019"
prop_spacing = isOk spacing " \\\n # Comment" prop_spacing1 = isOk spacing " \\\n # Comment"
prop_spacing2 = isOk spacing "# We can continue lines with \\"
prop_spacing3 = isWarning spacing " \\\n # --verbose=true \\"
spacing = do spacing = do
x <- many (many1 linewhitespace <|> try (string "\\\n" >> return "")) x <- many (many1 linewhitespace <|> continuation)
optional readComment optional readComment
return $ concat x return $ concat x
where
continuation = do
try (string "\\\n")
-- The line was continued. Warn if this next line is a comment with a trailing \
whitespace <- many linewhitespace
optional $ do
x <- readComment
when ("\\" `isSuffixOf` x) $
parseProblem ErrorC 1143 "This backslash is part of a comment and does not continue the line."
return whitespace
spacing1 = do spacing1 = do
spacing <- spacing spacing <- spacing
@@ -198,7 +210,7 @@ getNextIdSpanningTokenList list =
-- Get the span covered by an id -- Get the span covered by an id
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos) getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
getSpanForId id = getSpanForId id =
Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$> Map.findWithDefault (error $ pleaseReport "no parser span for id") id <$>
getMap getMap
-- Create a new id with the same span as an existing one -- Create a new id with the same span as an existing one
@@ -445,8 +457,8 @@ called s p = do
pos <- getPosition pos <- getPosition
withContext (ContextName pos s) p withContext (ContextName pos s) p
withAnnotations anns = withAnnotations anns p =
withContext (ContextAnnotation anns) if null anns then p else withContext (ContextAnnotation anns) p
readConditionContents single = readConditionContents single =
readCondContents `attempting` lookAhead (do readCondContents `attempting` lookAhead (do
@@ -544,7 +556,7 @@ readConditionContents single =
notFollowedBy2 (try (spacing >> string "]")) notFollowedBy2 (try (spacing >> string "]"))
x <- readNormalWord x <- readNormalWord
pos <- getPosition pos <- getPosition
when (endedWith "]" x && notArrayIndex x) $ do when (notArrayIndex x && endedWith "]" x && not (x `containsLiteral` "[")) $ do
parseProblemAt pos ErrorC 1020 $ parseProblemAt pos ErrorC 1020 $
"You need a space before the " ++ (if single then "]" else "]]") ++ "." "You need a space before the " ++ (if single then "]" else "]]") ++ "."
fail "Missing space before ]" fail "Missing space before ]"
@@ -560,6 +572,7 @@ readConditionContents single =
endedWith _ _ = False endedWith _ _ = False
notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "[" notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["
notArrayIndex _ = True notArrayIndex _ = True
containsLiteral x s = s `isInfixOf` onlyLiteralString x
readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True
@@ -807,11 +820,13 @@ readArithmeticContents =
return $ TA_Expansion id pieces return $ TA_Expansion id pieces
readGroup = do readGroup = do
start <- startSpan
char '(' char '('
s <- readSequence s <- readSequence
char ')' char ')'
id <- endSpan start
spacing spacing
return s return $ TA_Parentesis id s
readArithTerm = readGroup <|> readVariable <|> readExpansion readArithTerm = readGroup <|> readVariable <|> readExpansion
@@ -929,6 +944,9 @@ prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]" prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar" prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo" prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]"
prop_readCondition29 = isOk readCondition "[[ x = [*] ]]"
readCondition = called "test expression" $ do readCondition = called "test expression" $ do
opos <- getPosition opos <- getPosition
start <- startSpan start <- startSpan
@@ -972,12 +990,17 @@ prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n" prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n" prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n" prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\n"
prop_readAnnotation9 = isOk readAnnotation "# shellcheck source='foo bar' source-path=\"baz etc\"\n"
prop_readAnnotation10 = isOk readAnnotation "# shellcheck disable='SC1234,SC2345' enable=\"foo\" shell='bash'\n"
prop_readAnnotation11 = isOk (readAnnotationWithoutPrefix False) "external-sources='true'"
readAnnotation = called "shellcheck directive" $ do readAnnotation = called "shellcheck directive" $ do
try readAnnotationPrefix try readAnnotationPrefix
many1 linewhitespace many1 linewhitespace
readAnnotationWithoutPrefix readAnnotationWithoutPrefix True
readAnnotationWithoutPrefix = do readAnnotationWithoutPrefix sandboxed = do
values <- many1 readKey values <- many1 readKey
optional readAnyComment optional readAnyComment
void linefeed <|> eof <|> do void linefeed <|> eof <|> do
@@ -987,13 +1010,24 @@ readAnnotationWithoutPrefix = do
many linewhitespace many linewhitespace
return $ concat values return $ concat values
where where
plainOrQuoted p = quoted p <|> p
quoted p = do
c <- oneOf "'\""
start <- getPosition
str <- many1 $ noneOf (c:"\n")
char c <|> fail "Missing terminating quote for directive."
subParse start p str
readKey = do readKey = do
keyPos <- getPosition keyPos <- getPosition
key <- many1 (letter <|> char '-') key <- many1 (letter <|> char '-')
char '=' <|> fail "Expected '=' after directive key" char '=' <|> fail "Expected '=' after directive key"
annotations <- case key of annotations <- case key of
"disable" -> readRange `sepBy` char ',' "disable" -> plainOrQuoted $ readElement `sepBy` char ','
where where
readElement = readRange <|> readAll
readAll = do
string "all"
return $ DisableComment 0 1000000
readRange = do readRange = do
from <- readCode from <- readCode
to <- choice [ char '-' *> readCode, return $ from+1 ] to <- choice [ char '-' *> readCode, return $ from+1 ]
@@ -1003,26 +1037,41 @@ readAnnotationWithoutPrefix = do
int <- many1 digit int <- many1 digit
return $ read int return $ read int
"enable" -> readName `sepBy` char ',' "enable" -> plainOrQuoted $ readName `sepBy` char ','
where where
readName = EnableComment <$> many1 (letter <|> char '-') readName = EnableComment <$> many1 (letter <|> char '-')
"source" -> do "source" -> do
filename <- many1 $ noneOf " \n" filename <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
return [SourceOverride filename] return [SourceOverride filename]
"source-path" -> do "source-path" -> do
dirname <- many1 $ noneOf " \n" dirname <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
return [SourcePath dirname] return [SourcePath dirname]
"shell" -> do "shell" -> do
pos <- getPosition pos <- getPosition
shell <- many1 $ noneOf " \n" shell <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
when (isNothing $ shellForExecutable shell) $ when (isNothing $ shellForExecutable shell) $
parseNoteAt pos ErrorC 1103 parseNoteAt pos ErrorC 1103
"This shell type is unknown. Use e.g. sh or bash." "This shell type is unknown. Use e.g. sh or bash."
return [ShellOverride shell] return [ShellOverride shell]
"external-sources" -> do
pos <- getPosition
value <- plainOrQuoted $ many1 letter
case value of
"true" ->
if sandboxed
then do
parseNoteAt pos ErrorC 1144 "external-sources can only be enabled in .shellcheckrc, not in individual files."
return []
else return [ExternalSources True]
"false" -> return [ExternalSources False]
_ -> do
parseNoteAt pos ErrorC 1145 "Unknown external-sources value. Expected true/false."
return []
_ -> do _ -> do
parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored." parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored."
anyChar `reluctantlyTill` whitespace anyChar `reluctantlyTill` whitespace
@@ -1039,6 +1088,7 @@ readComment = do
unexpecting "shellcheck annotation" readAnnotationPrefix unexpecting "shellcheck annotation" readAnnotationPrefix
readAnyComment readAnyComment
prop_readAnyComment = isOk readAnyComment "# Comment"
readAnyComment = do readAnyComment = do
char '#' char '#'
many $ noneOf "\r\n" many $ noneOf "\r\n"
@@ -1359,6 +1409,8 @@ prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"
prop_readGlob6 = isOk readGlob "[\\|]" prop_readGlob6 = isOk readGlob "[\\|]"
prop_readGlob7 = isOk readGlob "[^[]" prop_readGlob7 = isOk readGlob "[^[]"
prop_readGlob8 = isOk readGlob "[*?]" prop_readGlob8 = isOk readGlob "[*?]"
prop_readGlob9 = isOk readGlob "[!]^]"
prop_readGlob10 = isOk readGlob "[]]"
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
where where
readSimple = do readSimple = do
@@ -1366,22 +1418,25 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
c <- oneOf "*?" c <- oneOf "*?"
id <- endSpan start id <- endSpan start
return $ T_Glob id [c] return $ T_Glob id [c]
-- Doesn't handle weird things like [^]a] and [$foo]. fixme?
readClass = try $ do readClass = try $ do
start <- startSpan start <- startSpan
char '[' char '['
s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars) negation <- charToString (oneOf "!^") <|> return ""
leadingBracket <- charToString (oneOf "]") <|> return ""
s <- many (predefined <|> readNormalLiteralPart "]" <|> globchars)
guard $ not (null leadingBracket) || not (null s)
char ']' char ']'
id <- endSpan start id <- endSpan start
return $ T_Glob id $ "[" ++ concat s ++ "]" return $ T_Glob id $ "[" ++ concat (negation:leadingBracket:s) ++ "]"
where where
globchars = fmap return . oneOf $ "!$[" ++ extglobStartChars globchars = charToString $ oneOf $ "![" ++ extglobStartChars
predefined = do predefined = do
try $ string "[:" try $ string "[:"
s <- many1 letter s <- many1 letter
string ":]" string ":]"
return $ "[:" ++ s ++ ":]" return $ "[:" ++ s ++ ":]"
charToString = fmap return
readGlobbyLiteral = do readGlobbyLiteral = do
start <- startSpan start <- startSpan
c <- extglobStart <|> char '[' c <- extglobStart <|> char '['
@@ -1404,6 +1459,8 @@ readNormalEscaped = called "escaped char" $ do
do do
next <- quotable <|> oneOf "?*@!+[]{}.,~#" next <- quotable <|> oneOf "?*@!+[]{}.,~#"
when (next == ' ') $ checkTrailingSpaces pos <|> return () when (next == ' ') $ checkTrailingSpaces pos <|> return ()
-- Check if this line is followed by a commented line with a trailing backslash
when (next == '\n') $ try . lookAhead $ void spacing
return $ if next == '\n' then "" else [next] return $ if next == '\n' then "" else [next]
<|> <|>
do do
@@ -1471,7 +1528,6 @@ readSingleEscaped = do
case x of case x of
'\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'."; '\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'.";
'\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line."
_ -> return () _ -> return ()
return [s] return [s]
@@ -1875,7 +1931,7 @@ readPendingHereDocs = do
-- The end token is just a prefix -- The end token is just a prefix
skipLine skipLine
| hasTrailer -> | hasTrailer ->
error "ShellCheck bug, please report (here doc trailer)." error $ pleaseReport "unexpected heredoc trailer"
-- The following cases assume no trailing text: -- The following cases assume no trailing text:
| dashed == Undashed && (not $ null leadingSpace) -> do | dashed == Undashed && (not $ null leadingSpace) -> do
@@ -1981,12 +2037,14 @@ readHereString = called "here string" $ do
word <- readNormalWord word <- readNormalWord
return $ T_HereString id word return $ T_HereString id word
prop_readNewlineList1 = isOk readScript "&> /dev/null echo foo"
readNewlineList = readNewlineList =
many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak
where where
checkBadBreak = optional $ do checkBadBreak = optional $ do
pos <- getPosition pos <- getPosition
try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or && try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or &&
notFollowedBy2 (string "&>") -- Except &> or &>> which is valid
parseProblemAt pos ErrorC 1133 parseProblemAt pos ErrorC 1133
"Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one." "Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one."
readLineBreak = optional readNewlineList readLineBreak = optional readNewlineList
@@ -2046,6 +2104,7 @@ prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)"
prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi" prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi"
prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )" prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"
prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls" prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls"
prop_readSimpleCommand7b = isOk readSimpleCommand "\\:"
prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol" prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol"
prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */" prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */"
prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */" prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */"
@@ -2053,6 +2112,7 @@ prop_readSimpleCommand11 = isOk readSimpleCommand "/\\* foo"
prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo" prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo"
prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo" prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo"
prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]" prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"
prop_readSimpleCommand15 = isWarning readSimpleCommand "trap 'foo\"bar' INT"
readSimpleCommand = called "simple command" $ do readSimpleCommand = called "simple command" $ do
prefix <- option [] readCmdPrefix prefix <- option [] readCmdPrefix
skipAnnotationAndWarn skipAnnotationAndWarn
@@ -2082,9 +2142,12 @@ readSimpleCommand = called "simple command" $ do
id2 <- getNewIdFor id1 id2 <- getNewIdFor id1
let result = makeSimpleCommand id1 id2 prefix [cmd] suffix let result = makeSimpleCommand id1 id2 prefix [cmd] suffix
if isCommand ["source", "."] cmd case () of
then readSource result _ | isCommand ["source", "."] cmd -> readSource result
else return result _ | isCommand ["trap"] cmd -> do
syntaxCheckTrap result
return result
_ -> return result
where where
isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
isCommand _ _ = False isCommand _ _ = False
@@ -2104,6 +2167,17 @@ readSimpleCommand = called "simple command" $ do
parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch." parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."
_ -> return () _ -> return ()
syntaxCheckTrap cmd =
case cmd of
(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:arg:_))) -> checkArg arg (getLiteralString arg)
_ -> return ()
where
checkArg _ Nothing = return ()
checkArg arg (Just ('-':_)) = return ()
checkArg arg (Just str) = do
(start,end) <- getSpanForId (getId arg)
subParse start (tryWithErrors (readCompoundListOrEmpty >> verifyEof) <|> return ()) str
commentWarning id = commentWarning id =
parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh." parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh."
@@ -2153,10 +2227,12 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
if filename == "/dev/null" -- always allow /dev/null if filename == "/dev/null" -- always allow /dev/null
then return (Right "", filename) then return (Right "", filename)
else do else do
allAnnotations <- getCurrentAnnotations True
currentScript <- Mr.asks currentFilename currentScript <- Mr.asks currentFilename
paths <- mapMaybe getSourcePath <$> getCurrentAnnotations True let paths = mapMaybe getSourcePath allAnnotations
resolved <- system $ siFindSource sys currentScript paths filename let externalSources = listToMaybe $ mapMaybe getExternalSources allAnnotations
contents <- system $ siReadFile sys resolved resolved <- system $ siFindSource sys currentScript externalSources paths filename
contents <- system $ siReadFile sys externalSources resolved
return (contents, resolved) return (contents, resolved)
case input of case input of
Left err -> do Left err -> do
@@ -2190,6 +2266,11 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
SourcePath x -> Just x SourcePath x -> Just x
_ -> Nothing _ -> Nothing
getExternalSources t =
case t of
ExternalSources b -> Just b
_ -> Nothing
-- If the word has a single expansion as the directory, try stripping it -- If the word has a single expansion as the directory, try stripping it
-- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file` -- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file`
stripDynamicPrefix word = stripDynamicPrefix word =
@@ -2202,22 +2283,31 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
subRead name script = subRead name script =
withContext (ContextSource name) $ withContext (ContextSource name) $
inSeparateContext $ inSeparateContext $ do
subParse (initialPos name) (readScriptFile True) script oldState <- getState
setState $ oldState { pendingHereDocs = [] }
result <- subParse (initialPos name) (readScriptFile True) script
newState <- getState
setState $ newState { pendingHereDocs = pendingHereDocs oldState }
return result
readSource t = return t readSource t = return t
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu" prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu" prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat" prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat"
prop_readPipeline4 = isOk readPipeline "! ! true"
prop_readPipeline5 = isOk readPipeline "true | ! true"
readPipeline = do readPipeline = do
unexpecting "keyword/token" readKeyword unexpecting "keyword/token" readKeyword
do readBanged readPipeSequence
readBanged parser = do
pos <- getPosition
(T_Bang id) <- g_Bang (T_Bang id) <- g_Bang
pipe <- readPipeSequence next <- readBanged parser
return $ T_Banged id pipe return $ T_Banged id next
<|> <|> parser
readPipeSequence
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1" prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo" prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
@@ -2233,7 +2323,7 @@ readAndOr = do
parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches." parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."
andOr <- withAnnotations annotations $ andOr <- withAnnotations annotations $
chainr1 readPipeline $ do chainl1 readPipeline $ do
op <- g_AND_IF <|> g_OR_IF op <- g_AND_IF <|> g_OR_IF
readLineBreak readLineBreak
return $ case op of T_AND_IF id -> T_AndIf id return $ case op of T_AND_IF id -> T_AndIf id
@@ -2273,7 +2363,7 @@ readTerm = do
readPipeSequence = do readPipeSequence = do
start <- startSpan start <- startSpan
(cmds, pipes) <- sepBy1WithSeparators readCommand (cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand)
(readPipe `thenSkip` (spacing >> readLineBreak)) (readPipe `thenSkip` (spacing >> readLineBreak))
id <- endSpan start id <- endSpan start
spacing spacing
@@ -2303,10 +2393,14 @@ readCommand = choice [
] ]
readCmdName = do readCmdName = do
-- If the command name is `!` then
optional . lookAhead . try $ do
char '!'
whitespace
-- Ignore alias suppression -- Ignore alias suppression
optional . try $ do optional . try $ do
char '\\' char '\\'
lookAhead $ variableChars lookAhead $ variableChars <|> oneOf ":."
readCmdWord readCmdWord
readCmdWord = do readCmdWord = do
@@ -2434,16 +2528,29 @@ readBraceGroup = called "brace group" $ do
spacing spacing
return $ T_BraceGroup id list return $ T_BraceGroup id list
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}" prop_readBatsTest1 = isOk readBatsTest "@test 'can parse' {\n true\n}"
prop_readBatsTest2 = isOk readBatsTest "@test random text !(@*$Y&! {\n true\n}"
prop_readBatsTest3 = isOk readBatsTest "@test foo { bar { baz {\n true\n}"
prop_readBatsTest4 = isNotOk readBatsTest "@test foo \n{\n true\n}"
readBatsTest = called "bats @test" $ do readBatsTest = called "bats @test" $ do
start <- startSpan start <- startSpan
try $ string "@test " try $ string "@test "
spacing spacing
name <- readNormalWord name <- readBatsName
spacing spacing
test <- readBraceGroup test <- readBraceGroup
id <- endSpan start id <- endSpan start
return $ T_BatsTest id name test return $ T_BatsTest id name test
where
readBatsName = do
line <- try . lookAhead $ many1 $ noneOf "\n"
let name = reverse $ f $ reverse line
string name
-- We want everything before the last " {" in a string, so we find everything after "{ " in its reverse
f ('{':' ':rest) = dropWhile isSpace rest
f (a:rest) = f rest
f [] = ""
prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done" prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"
readWhileClause = called "while loop" $ do readWhileClause = called "while loop" $ do
@@ -2472,7 +2579,7 @@ readDoGroup kwId = do
parseProblem ErrorC 1058 "Expected 'do'." parseProblem ErrorC 1058 "Expected 'do'."
return "Expected 'do'" return "Expected 'do'"
acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'." acceptButWarn g_Semi ErrorC 1059 "Semicolon is not allowed directly after 'do'. You can just delete it."
allspacing allspacing
optional (do optional (do
@@ -2792,7 +2899,7 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
startPos <- getPosition startPos <- getPosition
expression <- readStringForParser readCmdWord expression <- readStringForParser readCmdWord
let (unQuoted, newPos) = kludgeAwayQuotes expression startPos let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
subParse newPos readArithmeticContents unQuoted subParse newPos (readArithmeticContents <* eof) unQuoted
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos) kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
kludgeAwayQuotes s p = kludgeAwayQuotes s p =
@@ -3179,7 +3286,7 @@ prop_readConfigKVs4 = isOk readConfigKVs "\n\n\n\n\t \n"
prop_readConfigKVs5 = isOk readConfigKVs "# shellcheck accepts annotation-like comments in rc files\ndisable=1234" prop_readConfigKVs5 = isOk readConfigKVs "# shellcheck accepts annotation-like comments in rc files\ndisable=1234"
readConfigKVs = do readConfigKVs = do
anySpacingOrComment anySpacingOrComment
annotations <- many (readAnnotationWithoutPrefix <* anySpacingOrComment) annotations <- many (readAnnotationWithoutPrefix False <* anySpacingOrComment)
eof eof
return $ concat annotations return $ concat annotations
anySpacingOrComment = anySpacingOrComment =
@@ -3191,23 +3298,30 @@ prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=(" prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n" prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n" prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"
prop_readScript7 = isOk readScript "#!/bin/zsh\n# shellcheck disable=SC1071\nfor f (a b); echo $f\n"
readScriptFile sourced = do readScriptFile sourced = do
start <- startSpan start <- startSpan
pos <- getPosition pos <- getPosition
optional $ do
readUtf8Bom
parseProblem ErrorC 1082
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
shebang <- readShebang <|> readEmptyLiteral
let (T_Literal _ shebangString) = shebang
allspacing
annotationStart <- startSpan
fileAnnotations <- readAnnotations
rcAnnotations <- if sourced rcAnnotations <- if sourced
then return [] then return []
else do else do
filename <- Mr.asks currentFilename filename <- Mr.asks currentFilename
readConfigFile filename readConfigFile filename
-- Put the rc annotations on the stack so that one can ignore e.g. SC1084 in .shellcheckrc
withAnnotations rcAnnotations $ do
hasBom <- wasIncluded readUtf8Bom
shebang <- readShebang <|> readEmptyLiteral
let (T_Literal _ shebangString) = shebang
allspacing
annotationStart <- startSpan
fileAnnotations <- readAnnotations
-- Similarly put the filewide annotations on the stack to allow earlier suppression
withAnnotations fileAnnotations $ do
when (hasBom) $
parseProblemAt pos ErrorC 1082
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
let annotations = fileAnnotations ++ rcAnnotations let annotations = fileAnnotations ++ rcAnnotations
annotationId <- endSpan annotationStart annotationId <- endSpan annotationStart
let shellAnnotationSpecified = let shellAnnotationSpecified =
@@ -3216,11 +3330,12 @@ readScriptFile sourced = do
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
unless ignoreShebang $ unless ignoreShebang $
verifyShebang pos (getShell shebangString) verifyShebang pos (executableFromShebang shebangString)
if ignoreShebang || isValidShell (getShell shebangString) /= Just False if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
then do then do
commands <- withAnnotations annotations readCompoundListOrEmpty commands <- readCompoundListOrEmpty
id <- endSpan start id <- endSpan start
readPendingHereDocs
verifyEof verifyEof
let script = T_Annotation annotationId annotations $ let script = T_Annotation annotationId annotations $
T_Script id shebang commands T_Script id shebang commands
@@ -3231,17 +3346,6 @@ readScriptFile sourced = do
return $ T_Script id shebang [] return $ T_Script id shebang []
where where
basename s = reverse . takeWhile (/= '/') . reverse $ s
skipFlags = dropWhile ("-" `isPrefixOf`)
getShell sb =
case words sb of
[] -> ""
[x] -> basename x
(first:args) ->
if basename first == "env"
then fromMaybe "" $ find (notElem '=') $ skipFlags args
else basename first
verifyShebang pos s = do verifyShebang pos s = do
case isValidShell s of case isValidShell s of
Just True -> return () Just True -> return ()
@@ -3270,6 +3374,7 @@ readScriptFile sourced = do
"awk", "awk",
"csh", "csh",
"expect", "expect",
"fish",
"perl", "perl",
"python", "python",
"ruby", "ruby",
@@ -3332,16 +3437,6 @@ parsesCleanly parser string = runIdentity $ do
return $ Just . null $ parseNotes userState ++ parseProblems systemState return $ Just . null $ parseNotes userState ++ parseProblems systemState
(Left _, _) -> return Nothing (Left _, _) -> return Nothing
-- For printf debugging: print the value of an expression
-- Example: return $ dump $ T_Literal id [c]
dump :: Show a => a -> a
dump x = trace (show x) x
-- Like above, but print a specific expression:
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
dumps :: Show x => x -> a -> a
dumps t = trace (show t)
parseWithNotes parser = do parseWithNotes parser = do
item <- parser item <- parser
state <- getState state <- getState
@@ -3421,9 +3516,9 @@ notesForContext list = zipWith ($) [first, second] $ filter isName list
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text -- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
-- depending on declare -A statements. -- depending on declare -A statements.
reparseIndices root = reparseIndices root = process root
analyze blank blank f root
where where
process = analyze blank blank f
associative = getAssociativeArrays root associative = getAssociativeArrays root
isAssociative s = s `elem` associative isAssociative s = s `elem` associative
f (T_Assignment id mode name indices value) = do f (T_Assignment id mode name indices value) = do
@@ -3448,8 +3543,9 @@ reparseIndices root =
fixAssignmentIndex name word = fixAssignmentIndex name word =
case word of case word of
T_UnparsedIndex id pos src -> T_UnparsedIndex id pos src -> do
parsed name pos src idx <- parsed name pos src
process idx -- Recursively parse for cases like x[y[z=1]]=1
_ -> return word _ -> return word
parsed name pos src = parsed name pos src =

View File

@@ -0,0 +1,138 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module ShellCheck.PortageVariables (
readPortageVariables
) where
import ShellCheck.Regex
import Control.Exception
import Control.Monad
import Data.Maybe
import System.Directory (listDirectory)
import System.Exit (ExitCode(..))
import System.FilePath
import System.IO
import System.Process
import qualified Data.ByteString as B
import qualified Data.Map as M
type RepoName = String
type RepoPath = String
type EclassName = String
type EclassVar = String
-- | This is used for looking up what eclass variables are inherited,
-- keyed by the name of the eclass.
type EclassMap = M.Map EclassName [EclassVar]
data Repository = Repository
{ repositoryName :: RepoName
, repositoryLocation :: RepoPath
, repositoryEclasses :: [Eclass]
} deriving (Show, Eq, Ord)
data Eclass = Eclass
{ eclassName :: EclassName
, eclassVars :: [EclassVar]
} deriving (Show, Eq, Ord)
readPortageVariables :: IO (M.Map String [String])
readPortageVariables = portageVariables <$> scanRepos
-- | Map from eclass names to a list of eclass variables
portageVariables :: [Repository] -> EclassMap
portageVariables = foldMap $ foldMap go . repositoryEclasses
where
go e = M.singleton (eclassName e) (eclassVars e)
-- | Run @portageq@ to gather a list of repo names and paths, then scan each
-- one for eclasses and ultimately eclass metadata.
scanRepos :: IO [Repository]
scanRepos = do
let cmd = "portageq"
let args = ["repos_config", "/"]
out <- runOrDie cmd args
forM (reposParser $ lines out) $ \(n,p) -> Repository n p <$> getEclasses p
-- | Get the name of the repo and its path from blocks outputted by
-- @portageq@. If the path doesn't exist, this will return @Nothing@.
reposParser :: [String] -> [(RepoName, RepoPath)]
reposParser = f ""
where
segmentRegex = mkRegex "^\\[(.*)\\].*"
locationRegex = mkRegex "^[[:space:]]*location[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$"
f name [] = []
f name (line:rest) =
case (matchRegex segmentRegex line, matchRegex locationRegex line) of
(Just [next], _) -> f next rest
(_, Just [location]) -> (name, location) : f name rest
_ -> f name rest
-- | Scan the repo path for @*.eclass@ files in @eclass/@, then run
-- 'eclassParser' on each of them to produce @[Eclass]@.
--
-- If the @eclass/@ directory doesn't exist, the scan is skipped for that
-- repo.
getEclasses :: RepoPath -> IO [Eclass]
getEclasses repoLoc = do
let eclassDir = repoLoc </> "eclass"
files <- handle catcher $ listDirectory eclassDir
let names = filter (\(_, e) -> e == ".eclass") $ map splitExtension files
forM (names :: [(String, String)]) $ \(name, ext) -> do
contents <- withFile (eclassDir </> name <.> ext) ReadMode readFully
return $ Eclass name $ eclassParser (lines contents)
where
catcher :: IOException -> IO [String]
catcher e = do
hPutStrLn stderr $ "Unable to find .eclass files: " ++ show e
return []
-- | Scan a @.eclass@ file for any @@@ECLASS_VARIABLE:@ comments, generating
-- a list of eclass variables.
eclassParser :: [String] -> [String]
eclassParser lines = mapMaybe match lines
where
varRegex = mkRegex "^[[:space:]]*#[[:space:]]*@ECLASS_VARIABLE:[[:space:]]*([^[:space:]]*)[[:space:]]*$"
match str = head <$> matchRegex varRegex str
-- | Run the command and return the full stdout string (stdin is ignored).
--
-- If the command exits with a non-zero exit code, this will throw an
-- error including the captured contents of stdout and stderr.
runOrDie :: FilePath -> [String] -> IO String
runOrDie cmd args = bracket acquire release $ \(_,o,e,p) -> do
ot <- readFully (fromJust o)
et <- readFully (fromJust e)
ec <- waitForProcess p
case ec of
ExitSuccess -> pure ot
ExitFailure i -> fail $ unlines $ map unwords
$ [ [ show cmd ]
++ map show args
++ [ "failed with exit code", show i]
, [ "stdout:" ], [ ot ]
, [ "stderr:" ], [ et ]
]
where
acquire = createProcess (proc cmd args)
{ std_in = NoStream
, std_out = CreatePipe
, std_err = CreatePipe
}
release (i,o,e,p) = do
_ <- waitForProcess p
forM_ [i,o,e] $ mapM_ hClose
readFully :: Handle -> IO String
readFully handle = do
hSetBinaryMode handle True
str <- hGetContents handle
length str `seq` return str

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

View File

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

View File

@@ -22,6 +22,7 @@ fi
cabal install --dependencies-only --enable-tests "${flags[@]}" || cabal install --dependencies-only --enable-tests "${flags[@]}" ||
cabal install --dependencies-only "${flags[@]}" || cabal install --dependencies-only "${flags[@]}" ||
cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" ||
die "can't install dependencies" die "can't install dependencies"
cabal configure --enable-tests "${flags[@]}" || cabal configure --enable-tests "${flags[@]}" ||
die "configure failed" die "configure failed"
@@ -29,6 +30,8 @@ cabal build ||
die "build failed" die "build failed"
cabal test || cabal test ||
die "test failed" die "test failed"
cabal haddock ||
die "haddock failed"
sc="$(find . -name shellcheck -type f -perm -111)" sc="$(find . -name shellcheck -type f -perm -111)"
[ -x "$sc" ] || die "Can't find executable" [ -x "$sc" ] || die "Can't find executable"

View File

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

View File

@@ -25,6 +25,13 @@ exit 0
echo "Deleting 'dist' and 'dist-newstyle'..." echo "Deleting 'dist' and 'dist-newstyle'..."
rm -rf dist dist-newstyle rm -rf dist dist-newstyle
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" log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log" date >> "$log" || die "Can't write to log"
@@ -63,14 +70,17 @@ debian:testing apt-get update && apt-get install -y cabal-install
ubuntu:latest apt-get update && apt-get install -y cabal-install ubuntu:latest apt-get update && apt-get install -y cabal-install
haskell:latest true haskell:latest true
opensuse/leap:latest zypper install -y cabal-install ghc opensuse/leap:latest zypper install -y cabal-install ghc
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
# Other versions we want to support # Ubuntu LTS
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.04 apt-get update && apt-get install -y cabal-install ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:16.04 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build # Stack on Ubuntu LTS
ubuntu:18.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest ubuntu:22.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
EOF EOF
exit "$final" exit "$final"

View File

@@ -4,8 +4,12 @@ import Control.Monad
import System.Exit import System.Exit
import qualified ShellCheck.Analytics import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.ASTLib
import qualified ShellCheck.CFG
import qualified ShellCheck.CFGAnalysis
import qualified ShellCheck.Checker import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ControlFlow
import qualified ShellCheck.Checks.Custom import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer import qualified ShellCheck.Fixer
@@ -17,8 +21,12 @@ main = do
results <- sequence [ results <- sequence [
ShellCheck.Analytics.runTests ShellCheck.Analytics.runTests
,ShellCheck.AnalyzerLib.runTests ,ShellCheck.AnalyzerLib.runTests
,ShellCheck.ASTLib.runTests
,ShellCheck.CFG.runTests
,ShellCheck.CFGAnalysis.runTests
,ShellCheck.Checker.runTests ,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests ,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.ControlFlow.runTests
,ShellCheck.Checks.Custom.runTests ,ShellCheck.Checks.Custom.runTests
,ShellCheck.Checks.ShellSupport.runTests ,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.Fixer.runTests ,ShellCheck.Fixer.runTests

View File

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