234 Commits

Author SHA1 Message Date
Vidar Holen
cb57b4a74f Stable version 0.6.0
This release is dedicated to Factorio. If this is how much fun it is to
build factories and oppress natives, then history makes a lot of sense.
2018-12-02 19:08:06 -08:00
Vidar Holen
e3f0243c0e Add 'striptests' script to Cabal package 2018-12-02 19:08:06 -08:00
Vidar Holen
66b5f13c6f Make wiki links fit in 80 columns 2018-12-02 19:08:06 -08:00
Vidar Holen
a7a404a5a8 Fill in missing bits in CHANGELOG 2018-12-02 14:49:04 -08:00
Vidar Holen
0761f5c923 Merge pull request #1397 from ngzhian/man-en-dash
Disable smart typography extension for markdown input
2018-12-02 13:56:14 -08:00
Vidar Holen
b55149b22d Add man page instructions (fixes #1347) 2018-12-02 12:29:42 -08:00
Ng Zhi An
4097bb5154 Disable smart typography extension for markdown input
Fixes #1392
2018-11-29 23:18:20 -08:00
Vidar Holen
1b207b3d43 Preemptively fix possible '-- |' breakage 2018-11-26 20:43:15 -08:00
Vidar Holen
135b4aa485 Add stack builds to distro test 2018-11-26 20:41:29 -08:00
Vidar Holen
cb76951ad2 Add warnings for 'exit' similar to 'return' (fixes #1388) 2018-11-24 23:05:40 -08:00
Vidar Holen
705e476e4c Merge pull request #1389 from romanzolotarev/patch-1
Add OpenBSD to "Installing" section of README.md
2018-11-15 19:58:30 +11:00
Roman Zolotarev
e705552c97 Update README.html
Add OpenBSD to "Installing" section.
2018-11-15 08:49:26 +00:00
Vidar Holen
198aa4fc3d Merge pull request #1383 from l2dy/master
Fix typo in CHANGELOG
2018-11-08 11:13:55 +08:00
Zero King
f4044fbcc7 Fix typo in CHANGELOG 2018-11-08 03:07:52 +00:00
Vidar Holen
2827b35696 SC2240: Warn about . script args.. in sh/dash (fixes #1373) 2018-11-07 18:04:18 -08:00
Vidar Holen
de95c376ea Merge pull request #1380 from PeterDaveHello/Update-Dockerfile
Update Docker build-only image to Ubuntu 18.04
2018-11-08 08:34:21 +08:00
Peter Dave Hello
5e1b1e010a Update Docker build-only image to Ubuntu 18.04
Ref:
> Ubuntu 17.10 (Artful Aardvark) End of Life reached on July 19 2018
https://fridge.ubuntu.com/2018/07/19/ubuntu-17-10-artful-aardvark-end-of-life-reached-on-july-19-2018/
2018-11-02 13:47:22 +08:00
Vidar Holen
620c9c2023 Also warn about glob matching with [ a != b* ] (fixes #1374) 2018-11-01 04:47:44 -07:00
Vidar Holen
359b1467a2 Work around snap's old cabal + new snapcraft proxy. 2018-10-24 21:33:42 -07:00
Vidar Holen
df0a0d41fa Add SC1133: Warn when a line starts with |/||/&& (fixes #1359) 2018-10-21 17:46:46 -07:00
Vidar Holen
b815242506 Improve regex parsing (fixes #1367) 2018-10-21 15:25:35 -07:00
Vidar Holen
07b5aa2971 Add SC2239: shebang is not absolute path. 2018-10-17 20:38:21 -07:00
Vidar Holen
f7b82658f4 Add $# to list of variables not containing spaces (fixes #1362) 2018-10-17 09:00:52 -07:00
Vidar Holen
e0e46e979a Add wiki links to output, and a -W controlling it. (Fixes #920) 2018-10-10 21:53:43 -07:00
Vidar Holen
79319558a5 Merge pull request #1350 from peti/master
Fix build with ghc 8.6.1
2018-10-04 20:11:40 -07:00
Vidar Holen
8d13add1ed Add automated cross-distro testing via Docker 2018-10-01 17:10:43 -07:00
Peter Simons
8940e60300 ShellCheck.cabal: our Setup.hs works fine with Cabal 2.4.x 2018-09-27 17:32:45 +02:00
Peter Simons
5f1c969546 getParentTree: avoid pattern matching in do notation
Pattern matching in "do" requires a MonadFail context, which we don't have in
pure code. Instead, we'll use "case-of" to bind the part of the state that
we're interested in.
2018-09-27 17:30:41 +02:00
Vidar Holen
dadfdfde97 Don't suggest subshells for cd ..; foo; cd.. 2018-09-21 21:08:41 -07:00
Vidar Holen
3e2cb26119 Add SC2238 about redirections to command names 2018-09-17 17:46:49 -07:00
Vidar Holen
1a6ae4f19e Add plug for shfmt 2018-09-16 10:56:53 -07:00
Vidar Holen
95a376aad1 Minor script cleanup 2018-09-16 10:11:03 -07:00
Vidar Holen
a06d7c1841 Merge pull request #1324 from ngzhian/679
Understand array variable declaration in read (fixes #679)
2018-09-15 12:33:58 -07:00
Vidar Holen
5202072a34 Add employer mandated disclaimer 2018-09-15 11:10:27 -07:00
Vidar Holen
72af1cfd59 Merge pull request #1331 from federicotdn/patch-1
Add link to flymake-shellcheck under Emacs section
2018-09-15 11:09:04 -07:00
Vidar Holen
228af7df54 Merge pull request #1337 from dimo414/master
Expand "rhs"; this abbreviation seems needlessly obfuscating.
2018-09-12 16:01:43 -07:00
Michael Diamond
6db392511b Expand "rhs"; this abbreviation seems needlessly obfuscating. 2018-09-12 14:22:40 -07:00
Ng Zhi An
07f04e13ce Understand array variable declaration in read (fixes #679 fixes #1272)
It used to only treat all trailing variables in read as varaible
declarings, but an array variable can be declared in other positions:

    read -a foo -r

foo is a declared variable, and multiple such variables can be declared.
2018-09-08 09:19:02 -07:00
Federico T
493ecd6f73 Add link to flymake-shellcheck under Emacs section
I've recently created the flymake-shellcheck package for Emacs, which allows using ShellCheck with the built-in Flymake package.
2018-09-07 12:37:16 -03:00
Vidar Holen
f0a2e688c4 Don't warn about LINENO since it's now POSIX. Fixes #644 2018-09-03 12:36:25 -07:00
Vidar Holen
0cee8a993d Suggest reading the wiki page in the issue template 2018-08-28 20:40:57 -07:00
Vidar Holen
3d03b0ab3b Suggest -z/-n instead of ! -n/-z (fixes #1326). 2018-08-28 20:15:54 -07:00
Vidar Holen
488d6dcb41 Improve find leading flag detection (fixes #1312) 2018-08-26 18:18:57 -07:00
Vidar Holen
d02a9bbcce Account for &&/||/{}/() in SC2233&co (fixes #1320). 2018-08-26 17:36:10 -07:00
Vidar Holen
165e408114 Merge branch 'ngzhian-opqaque-interface' 2018-08-18 20:33:14 -07:00
Vidar Holen
932e2b3538 Merge branch 'opqaque-interface' of https://github.com/ngzhian/shellcheck into ngzhian-opqaque-interface 2018-08-18 20:32:27 -07:00
Vidar Holen
76b1482f64 Avoid using error for option parsing failure 2018-08-18 20:06:44 -07:00
Vidar Holen
49250eadae Add --severity to CHANGELOG 2018-08-18 20:06:31 -07:00
Martin Schwenke
3fe11927bb SQUASH: --severity specifies *minimum* severity to be handled
Signed-off-by: Martin Schwenke <martin@meltin.net>
2018-08-18 20:05:56 -07:00
Martin Schwenke
b16da4b242 Add command-line option -S/--severity
Specifies the maximum severity of errors to handle.  For example,
specifying "-S warning" means that errors of severity "info" and
"style" are ignored.

Signed-off-by: Martin Schwenke <martin@meltin.net>
2018-08-18 20:05:56 -07:00
Ng Zhi An
c8e0797350 Make data in Interface more opaque 2018-08-17 22:10:18 -07:00
Vidar Holen
15aaacf715 Add test for parsing bitwise not 2018-08-15 18:30:10 -07:00
Vidar Holen
5ef4229f61 Modernize SC2028 echo escape test 2018-08-07 19:31:28 -07:00
Vidar Holen
afada43978 Merge pull request #1311 from ngzhian/1310
Use regex to match special flags for printf
2018-08-07 18:45:44 -07:00
Ng Zhi An
8be76b13b9 Use regex to match special flags for printf
Fixes #1310
2018-08-05 22:45:24 -07:00
Vidar Holen
581be5878b Suggest 'cat' when piping/redirecting to echo (fixes #1292) 2018-07-28 17:38:53 -07:00
Vidar Holen
0f835a5a2c Don't trigger SC2222 for fallthrough case branches (fixes #1044) 2018-07-28 12:30:06 -07:00
Vidar Holen
4b0a35d4c9 Merge pull request #1302 from pjeby/fix949
Fix #949 (failing on @ in function names)
2018-07-26 21:07:48 -07:00
Vidar Holen
51e0c1be62 Use three instead of two dots in 2006 message 2018-07-26 19:59:42 -07:00
Vidar Holen
d8a32da07f Retire SC1117 (unknown quoted escapes) due to noise 2018-07-26 19:23:53 -07:00
PJ Eby
0d1a34a291 Fix #949 (failing on @ in function names)
'@' was previously mentioned in 5005dc0fa1 as a
character needed to fix #909, but was not included
in the actual change at that time.
2018-07-23 16:18:35 -04:00
Vidar Holen
5005dc0fa1 Allow directive/-s to override shebang blacklist (fixes #974) 2018-07-22 12:43:51 -07:00
Vidar Holen
b8ee7436e5 Add a test for 03ce3b15 2018-07-21 13:51:21 -07:00
Vidar Holen
da8e450386 Realign =s 2018-07-21 13:51:08 -07:00
Vidar Holen
c3ac4c3d87 Merge pull request #1298 from ngzhian/1268
Fix false positive when indexing into array in cond
2018-07-21 13:43:45 -07:00
Ng Zhi An
03ce3b15b6 Fix false positive when indexing into array in cond
Fixes #1268
2018-07-18 22:31:58 -07:00
Vidar Holen
10edba3ab8 Minimize build size with -Os and -split-sections 2018-07-12 09:33:59 -07:00
Vidar Holen
797b424917 Add armv6hf link for Raspberry Pi 2018-07-08 20:47:05 -07:00
Vidar Holen
84e678e9ff TravisCI armv6hf build (aka Raspberry Pi build) 2018-07-08 20:13:33 -07:00
Vidar Holen
3a672968f3 Merge pull request #1282 from kenden/patch-1
Add instructions to install linux binary
2018-07-07 13:32:37 -07:00
Quentin Nerden
8c7efae393 Add instructions to install linux binary 2018-07-05 15:30:16 +02:00
Vidar Holen
f91b5bc270 Merge pull request #1256 from ngzhian/mv-stdin
Do not warn on mv -i (fixes #1251)
2018-06-24 11:47:53 -07:00
Vidar Holen
b01f1128c7 Make SC1012 "printf '\t'" suggestion use single quotes 2018-06-24 11:47:00 -07:00
Vidar Holen
db33294838 Merge pull request #1257 from ngzhian/trailing-comma-exclude
Allow trailing comma in exclude flag
2018-06-24 11:46:26 -07:00
Vidar Holen
75fb4da387 Don't warn about tr '[=e=]' equivalence classes 2018-06-23 16:55:35 -07:00
Vidar Holen
366262af18 Update CHANGELOG to mention end positions 2018-06-23 16:53:44 -07:00
Vidar Holen
6869c2fa18 Merge pull request #1261 from ngzhian/1188
Do not warn find --help (fixes #1188)
2018-06-23 16:07:23 -07:00
Vidar Holen
868a7be33e Improve spans for some warnings 2018-06-17 19:19:18 -07:00
Vidar Holen
7138abff4b Expose (some) span information in TTY output 2018-06-17 17:44:31 -07:00
Vidar Holen
9d3e79b576 Require all Ids to be constructed with a span 2018-06-16 17:33:08 -07:00
Vidar Holen
402e635f86 Warn about & followed by letters, e.g. http://foo/?a=b&c=d 2018-06-16 12:30:19 -07:00
Ng Zhi An
91cbcddd9d Do not warn find --help (fixes #1188) 2018-06-14 22:40:26 -07:00
Ng Zhi An
963b39b002 Allow trailing comma in exclude flag 2018-06-13 22:31:51 -07:00
Ng Zhi An
0cc45447d3 Do not warn on mv -i (fixes #1251) 2018-06-13 21:39:37 -07:00
Vidar Holen
32a53f21b5 Merge pull request #1239 from ngzhian/end_column
End column
2018-06-13 19:29:55 -07:00
Vidar Holen
12b8720bd8 Merge pull request #1255 from ngzhian/popd-n
Check popd flags for -n (fixes #1252)
2018-06-13 19:22:39 -07:00
Ng Zhi An
7adeaccd11 Check popd flags for -n (fixes #1252) 2018-06-12 22:53:41 -07:00
Ng Zhi An
b63483d44c Remove unused import 2018-06-12 22:50:02 -07:00
Ng Zhi An
4111ce8fde Make end pos non-optional 2018-06-12 22:39:06 -07:00
Ng Zhi An
b9a9eb2529 Change getNextId to create a zero width span at new id 2018-06-12 22:17:35 -07:00
Ng Zhi An
e717802de1 Change usage of endPosOfStartId to startSpan and endSpan 2018-06-12 22:11:11 -07:00
Ng Zhi An
1699c9e9ba Add api to begin and end a span of source code 2018-06-12 21:56:53 -07:00
Vidar Holen
bfc32200e2 Correctly consider $'..' a literal (fixes #1242) 2018-06-10 20:23:10 -07:00
Vidar Holen
52e8a42d9d Merge pull request #1253 from sblondon/master
Remove unnecessary dot
2018-06-09 13:03:24 -07:00
sblondon
00360af672 Remove unnecessary dot
The dot after the screenshot is strange so this commit remove it.
2018-06-08 10:57:41 +02:00
Ng Zhi An
8ff35fb4af Add end pos to readSingleQuoted 2018-06-07 23:09:59 -07:00
Ng Zhi An
29e8c0a16e Add end pos to readDollarBraced 2018-06-07 22:25:16 -07:00
Ng Zhi An
3848788c2d Add end pos to readDollarVariable 2018-06-07 21:55:41 -07:00
Ng Zhi An
0c459ae2cb Add function to set end pos of start id 2018-06-07 21:55:41 -07:00
Ng Zhi An
e496b413bd Remove usage of withNextId 2018-06-07 21:55:41 -07:00
Ng Zhi An
48ac654a93 Merge end pos map into start pos map 2018-06-07 21:55:41 -07:00
Russell Harmon
4470fe715c Support emitting a correct end column on SC2086
This does the necessary work to emit end columns on AST analyses. SC2086
is made to emit a correct end column as an illustrative example.

For example:
```
$ shellcheck -s bash -f json /dev/stdin <<< 'echo $1'
[{"file":"/dev/stdin","line":1,"endLine":1,"column":6,"endColumn":8,"level":"info","code":2086,"message":"Double quote to prevent globbing and word splitting."}]
```

This change deprecates the parser's getNextId and getNextIdAt, replacing
it with a new withNextId function. This function has the type signature:

withNextId :: Monad m => ParsecT s UserState (SCBase m) (Id -> b) -> ParsecT s UserState (SCBase m) b

Specifically, it should be used to wrap read* functions and will pass in
a newly generated Id which should be used to represent that node.
Sub-parsers will need their own call to withNextId in order to get a
unique Id.

In doing this, withNextId can now track both the entry and exit position
of every read* parser which uses it, enabling the tracking of end
columns throughout the application.
2018-06-07 21:55:41 -07:00
Vidar Holen
379321d1f3 Show tags in travis output 2018-06-07 20:40:44 -07:00
Vidar Holen
0adea473fd Update CHANGELOG after release 2018-06-07 20:19:00 -07:00
Vidar Holen
a3be776f80 Stable version 0.5.0
This release is dedicated to Valve for keeping PC gaming awesome.
Also, for that time they proved the need for a tool like ShellCheck.
2018-05-31 20:01:42 -07:00
Vidar Holen
b5e5d249c4 Merge pull request #1237 from ngzhian/1234
Check if builtin cd is called
2018-05-27 18:34:18 -07:00
Vidar Holen
efffc6150b Warn about string operations on $@/$* (fixes #1236) 2018-05-27 12:53:01 -07:00
Ng Zhi An
cb4a0c0250 Check if builtin cd is called
Fixes #1234
2018-05-27 11:42:37 -07:00
Vidar Holen
135cf5932f Parse here docs as per spec (fixes #1050) 2018-05-26 21:01:18 -07:00
Vidar Holen
467dfe07b6 Add a unit test and separate ids for 884eff0c 2018-05-23 19:51:36 -07:00
Vidar Holen
d140388bea Merge pull request #1232 from ngzhian/source-x
Add T_SourceCommand to wrap source commands and sourced code
2018-05-23 19:41:55 -07:00
Ng Zhi An
884eff0c36 Add T_SourceCommand to wrap source commands and sourced code
Fixes #1181
2018-05-22 22:43:26 -07:00
Vidar Holen
1d8047cce1 Warn about unnecessary subshells in tests 2018-05-22 22:35:37 -07:00
Vidar Holen
77546fba2f Merge pull request #1223 from ngzhian/1124
Handle offset references of the form [foo]:bar (fixes #1124)
2018-05-20 17:26:08 -07:00
Vidar Holen
4a5ee06ce4 Merge pull request #1206 from ngzhian/aeson
Change to aeson (fixes #1085)
2018-05-20 17:19:20 -07:00
Ng Zhi An
4dceecb1ed Handle offset references of the form [foo]:bar (fixes #1124) 2018-05-13 22:48:02 -07:00
Vidar Holen
5b226e733b Merge pull request #1203 from ngzhian/1199
Whitelist rename for SC2016 (fixes #1199)
2018-05-13 16:56:09 -07:00
Vidar Holen
46c10c1571 Merge branch 'master' into 1199 2018-05-13 16:55:53 -07:00
Vidar Holen
1de8ba0210 Merge pull request #1208 from ngzhian/1186
Add unset to list of commands exempt from 2016
2018-05-13 16:47:29 -07:00
Vidar Holen
407f6a63b9 Merge branch 'master' into 1186 2018-05-13 16:47:18 -07:00
Vidar Holen
7ee7448a70 Merge pull request #1221 from ngzhian/1192
Assignments are okay in SC2094 (fixes #1192)
2018-05-13 16:44:11 -07:00
Vidar Holen
48ebd41e22 Merge pull request #1222 from ngzhian/196
Suppress SC2016 for git filter-branch (fixes #196)
2018-05-13 15:45:20 -07:00
Ng Zhi An
0c88fbc76d Suppress SC2016 for git filter-branch (fixes #196) 2018-05-13 15:18:55 -07:00
Ng Zhi An
b3362f1dc3 Assignments are okay in SC2094 (fixes #1192) 2018-05-13 15:17:32 -07:00
Ng Zhi An
2c6bc43614 Add Data.Monoid 2018-05-13 11:44:21 -07:00
Vidar Holen
235bf6605f Merge pull request #1205 from ngzhian/remove-unused
Remove unused code
2018-05-12 19:15:42 -07:00
Vidar Holen
7029a713c7 Merge pull request #1207 from ngzhian/1184
When given a %* format string, expect one more argument
2018-05-12 18:50:23 -07:00
Vidar Holen
cf608dc2f6 Parse FD move operations like 2>&1- correctly. Fixes #1180. 2018-05-12 18:30:35 -07:00
Vidar Holen
aa3b3fdc56 Make .ghci look in ./src 2018-05-12 17:47:21 -07:00
Vidar Holen
bca2ad4e18 Don't think declare -x -F var is used (fixes #1209). 2018-05-12 17:34:23 -07:00
Vidar Holen
719e1854e5 Clarify 'export' suggestion in SC2034 (unused vars). 2018-05-11 21:39:54 -07:00
Ng Zhi An
20ad7dc8de Add unset to list of commands exempt from 2016
Fixes #1186
2018-05-06 16:45:57 -07:00
Ng Zhi An
f84859ab90 When given a %* format string, expect one more argument
Fixes #1184
2018-05-06 16:39:51 -07:00
Ng Zhi An
08235a1cb2 Change to aeson (fixes #1085)
Adds bytestring as a dependency for putStrLn encoded values.
2018-05-06 16:07:53 -07:00
Ng Zhi An
728922d2b8 Remove unused code 2018-05-06 15:24:34 -07:00
Ng Zhi An
a953dd3454 Whitelist rename for SC2016 (fixes #1199) 2018-05-06 10:28:17 -07:00
Vidar Holen
ef6a5b97b9 Refactor sudo checks into CommandChecks 2018-04-30 22:59:23 -07:00
Vidar Holen
8873a1732b Merge pull request #1195 from sdknudsen/master
Warn about invalid arguments to sudo
2018-04-30 21:50:28 -07:00
Stefan Knudsen
5adfce72e1 Warn about invalid arguments to sudo 2018-04-29 01:16:31 -04:00
Vidar Holen
12b3fdf661 Update gallery of bad code 2018-04-28 12:58:44 -07:00
Vidar Holen
bb4ce86fab Account for array index in SC2154 ${var:?} (fixes #1166) 2018-04-28 12:09:54 -07:00
Vidar Holen
7ec2fa2d3e Remove 'nice' from list of non-reading commands (fixes #1169) 2018-04-28 11:39:42 -07:00
Vidar Holen
5481ccd7f7 Warn about elseif or elsif as command names (fixes #1177) 2018-04-27 22:23:37 -07:00
Vidar Holen
a1d8947297 Fix broken test stripping 2018-04-22 14:57:43 -07:00
Vidar Holen
683a30abde Merge pull request #1174 from rasa/patch-1
Added scoop install
2018-04-22 14:13:35 -07:00
Vidar Holen
573936f353 Merge pull request #1185 from 0mp/master
Add FreeBSD installation instructions
2018-04-22 14:10:59 -07:00
Vidar Holen
ce7658ed86 Merge pull request #1168 from vmchale/master
Bump to ghc 8.4.1
2018-04-22 14:10:25 -07:00
Mateusz Piotrowski
0136d9ccce Add FreeBSD installation instructions 2018-04-19 13:25:25 +02:00
Ross Smith II
a4c6cea5e6 Added scoop install
See https://github.com/lukesampson/scoop/blob/master/bucket/shellcheck.json
2018-04-07 16:22:29 -07:00
Vidar Holen
32af2783f0 Allow stripping unit tests 2018-04-02 21:14:23 -07:00
Vanessa McHale
08aab3c161 hlints 2018-04-02 11:48:21 -05:00
Vanessa McHale
cf39adff75 bump to latest ghc 2018-04-02 11:44:18 -05:00
Vidar Holen
da4072a118 Blacklist base 4.6.0.1 to disable GHC 7.6.3 (#1131) 2018-03-29 21:41:00 -07:00
Vidar Holen
08d2eef411 Whitelist docker for SC2016 about '$var'. Fixes #1161 2018-03-29 19:27:34 -07:00
Vidar Holen
de257a6cf3 Merge pull request #1164 from Lin-Buo-Ren/improve-snap-package
Improve snap packaging
2018-03-29 11:49:26 -07:00
林博仁(Buo-Ren Lin)
68c24925bc Add info on connecting to removable-media interface in snap description
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 23:47:59 +08:00
林博仁(Buo-Ren Lin)
366dc5d3f8 Add snap install instructions to README
Currently shellcheck is only provided by the edge channel, should remove the --channel argument after it is in candidate/stable.

Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:24:31 +08:00
林博仁(Buo-Ren Lin)
1ed743e410 Add snapcraft generated files to the Git tracking ignore rules
This patches uses the following gitignore syntax so that only entries in the root folder is ignored, it is suggested to apply it to existing rules as well.

```
A leading slash matches the beginning of the pathname. For
example, "/*.c" matches "cat-file.c" but not
"mozilla-sha1/sha1.c".
```

Refer-to: gitignore(5) manpage
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:05:52 +08:00
林博仁(Buo-Ren Lin)
9a2aad16ad Add removable-media plug so that scripts in removable media can be checked
Otherwise it will be blocked by Apparmor with the following message:

```
$ shellcheck script
audit: type=1400 audit(TIMESTAMP): apparmor="DENIED" operation="open" profile="snap.shellcheck.shellcheck" name=2F6D656469612F4C696E2D42756F2D52656E2F57696E646F7773205553422F717569636B72756E pid=10175 comm="shellcheck" requested_mask="r" denied_mask="r" fsuid=FSUID ouid=OUID
script: script: openBinaryFile: permission denied (Permission denied)
```

NOTE:

* This plug is not Auto-connect plug, it has to be manually connected by user with `snap connect shellcheck:removable-media :removable-media`
* Currently files under /mnt is not checkable as snapd doesn't provide an interface for it for now.

Refer-to: Interfaces reference - Snaps are universal Linux packages <https://docs.snapcraft.io/reference/interfaces>
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:00:23 +08:00
Vidar Holen
177cb10daa Enable strict mode with home access 2018-03-28 18:26:16 -07:00
Vidar Holen
4aca1ff128 Warn when printf arg count is not a multiple of format count 2018-03-28 08:57:38 -07:00
Vidar Holen
ffed7caff4 quickrun script now works with new source layout 2018-03-28 08:57:38 -07:00
Vidar Holen
ef28200199 Merge pull request #1159 from geirha/patch-1
Consider type a valid command in sh
2018-03-27 11:38:07 -07:00
Geir Hauge
55216792c9 Consider type a valid command in sh
[type](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/type.html) *is* defined by POSIX, and even the bourne shell has the `type` command.
2018-03-25 12:11:57 +02:00
Vidar Holen
51e115cf47 Attempt to fix snap build 2018-03-24 17:43:32 -07:00
Vidar Holen
764b242f1b Suggest quoting expansions in for loop globs 2018-03-24 17:43:20 -07:00
Vidar Holen
c3b606c68a Encourage users to use stable rather than latest 2018-03-21 17:55:21 -07:00
Vidar Holen
9f53109dfa Fix tagging of 'stable' 2018-03-21 17:54:36 -07:00
Vidar Holen
795a881219 Fix docker image workdir and add test 2018-03-21 09:39:06 -07:00
Vidar Holen
6dd5350e3b Merge pull request #1143 from pratikmallya/simplify_dockerbuild
Simplify Dockerfile
2018-03-20 22:31:36 -07:00
Vidar Holen
a5b359591c Merge branch 'master' into simplify_dockerbuild 2018-03-20 22:31:21 -07:00
Vidar Holen
966194e387 Merge branch 'pratikmallya-simplify_dockerbuild' 2018-03-20 22:10:24 -07:00
Vidar Holen
71bcc80c2f Allow more Docker build caching 2018-03-20 22:09:13 -07:00
Vidar Holen
48616225b3 Merge pull request #1147 from tdmalone/patch-1
README - Travis script for updating shellcheck ver
2018-03-20 19:28:39 -07:00
Tim Malone
99276cb9f5 README - Travis script for updating shellcheck ver
Shellcheck in default Travis builds is currently a version behind
2018-03-19 11:15:28 +11:00
Pratik Mallya
f769d4e92c Add TravisCI Build Status file 2018-03-15 23:13:54 -05:00
Pratik Mallya
71df01c00f Simplify Dockerfile
Use multi stage Dockerfile to greatly simplify build.
2018-03-15 23:12:28 -05:00
Vidar Holen
5364701914 Merge branch 'simplify_dockerbuild' of https://github.com/pratikmallya/shellcheck into pratikmallya-simplify_dockerbuild 2018-03-15 09:53:50 -07:00
Vidar Holen
fb97aca5a6 Merge pull request #1140 from phadej/src
Move library into src/
2018-03-15 16:52:26 +00:00
Pratik Mallya
6b81a9924c Simplify Dockerfile
Use multi stage Dockerfile to greatly simplify build.
2018-03-14 21:43:34 -05:00
Oleg Grenrus
cd7c077ecc Move library into src/ 2018-03-08 19:57:40 +02:00
Oleg Grenrus
b33607b048 Add custom-setup stanza and containers lowerbound
custom-setup:

- http://cabal.readthedocs.io/en/latest/developing-packages.html#custom-setup-scripts
- https://www.well-typed.com/blog/2015/07/cabal-setup-deps/

Bounds:
- containers-0.5 is required if Data.Map.Strict
- parsec-3.0 for Text.Parsec
- json-0.3.6 for makeObj
2018-03-08 19:35:50 +02:00
Vidar Holen
969230f171 MacPorts version appears unmaintained. Remove from docs. 2018-03-08 08:48:03 -08:00
Vidar Holen
a98d69f4ff Update CHANGELOG with $var[ and here doc expansion fixes 2018-03-04 15:42:52 -08:00
Vidar Holen
f71c142a44 Don't ignore parse failures in here documents. Fixes #1135. 2018-03-04 15:24:04 -08:00
Vidar Holen
9dfcf54f10 Functionality for emitting parse errors but still continue 2018-03-04 14:42:47 -08:00
Vidar Holen
c8cd9dd09c Add a debugParseScript for development 2018-03-03 15:36:50 -08:00
Vidar Holen
8b8aeb4409 Rephrase SC2069 (cmd 2>&1 > file) and make it a warning. Fixes #633 2018-03-03 13:33:24 -08:00
Vidar Holen
ee354ffce8 POSIX warning for export -[^p]. Fixes #1130. 2018-02-27 20:38:02 -08:00
Vidar Holen
9fc3ddf849 Fix SC1087 to trigger on any $var[, not just $var[@] 2018-02-25 18:25:47 -08:00
Vidar Holen
ecb9d07f52 Update changelog with associative arrays in (()) fix 2018-02-25 18:15:55 -08:00
Vidar Holen
d16bf41c3d Better support arrays in arithmetic contexts. Fixes #1074 2018-02-25 18:08:38 -08:00
Vidar Holen
8d5e3a80ae Merge pull request #1123 from jonhiggs/check-for-which-usage
Check for calls to `which`.
2018-02-25 17:08:37 -08:00
Vidar Holen
34e0fa53c8 Merge pull request #1125 from jonhiggs/nextnumber-macos-support
MacOS support for nextnumber
2018-02-25 17:08:15 -08:00
Jon Higgs
7fb27310e1 Rely upon /usr/bin/env to find bash
This allows you to use the homebrew install Bash 4 on MacOS systems. It
should compatible with most if not all modern Linux distros.
2018-02-26 11:24:09 +11:00
Jon Higgs
00d3c09ddb Raise error unless interpreter supports globstar 2018-02-26 11:23:42 +11:00
Jon Higgs
e8fc09414a Check for calls to which.
Favour the builtin `command -v` instead.
2018-02-26 10:00:25 +11:00
Vidar Holen
b7a8b090d2 SC2229: Warn about 'read $var' 2018-02-25 13:47:58 -08:00
Vidar Holen
72044a79c6 Update changelog 2018-02-25 13:47:58 -08:00
Vidar Holen
6511dc0246 Add missing import 2018-02-25 13:29:06 -08:00
Vidar Holen
740441f2c4 Merge pull request #1121 from PeterDaveHello/update-travis-ci-doc
[Docs] Update Travis CI part in README.md
2018-02-25 11:37:00 -08:00
Peter Dave Hello
b311563421 Update Travis CI part in README.md 2018-02-20 04:52:12 +08:00
Vidar Holen
6d257bfa17 Warn about 'while!' and 'while:' 2018-02-17 21:58:29 -08:00
Vidar Holen
d8717c7046 s/parser error/syntax error/g 2018-02-17 17:18:05 -08:00
Vidar Holen
7aa3a7ffc3 Improve message for SC2163 (export $var). Helps #1117 2018-02-17 17:12:11 -08:00
Vidar Holen
017af8333f Merge pull request #1112 from Nightfirecat/sc2154-local-test
Add test for `local` keyword in SC2154
2018-02-10 08:04:07 -08:00
Jordan Atwood
f73d6f2332 Add test for local keyword in SC2154
Follow-up to koalaman/shellcheck#988
2018-02-07 17:50:55 -08:00
koalaman
a840f4e464 Merge pull request #1100 from mkhl/SC2029
SC2029: Skip when there are options to ssh
2018-01-24 19:38:38 -08:00
Vidar Holen
0f5e40c076 Mention shellcheck-static for Arch 2018-01-24 09:02:56 -08:00
Martin Kühl
ccaacb108a SC2029: Skip when there are options to ssh
Fixes #327

SC2029 generates false positives when given an ssh command that includes
options with arguments because it assumes the first non-option must be
the host:port argument and the last argument is a command to run.
As suggested the comments on #327, this change fixes those by skipping
the check when there are any options present.
2018-01-24 13:05:22 +01:00
Martin Kühl
56751413b4 SC2029: Add false positive test
This change adds a test case for a valid command that gets falsely
flagged with SC2029.
2018-01-24 13:04:26 +01:00
Vidar Holen
ba5f20deda Fix parsing of escaped chars in regex groups. Fixes #1077 2018-01-21 16:13:16 -08:00
Vidar Holen
c86885427c Warn about comments/blanks before shebang. Fixes #844 2018-01-21 13:57:44 -08:00
Vidar Holen
7b3c4025fb Warn about redirs in the middle of 'find' commands. Fixes #405 2018-01-21 11:12:22 -08:00
Vidar Holen
3b004275cf Add unit test for issue #1091 2018-01-20 11:42:31 -08:00
koalaman
72971fa52b Merge pull request #1097 from sdknudsen/fix/recognize-ids-with-underscores
Use readVariableName combinator
2018-01-20 11:35:22 -08:00
Stefan Knudsen
dbdab5705f Use readVariableName combinator 2018-01-19 16:19:06 -05:00
Vidar Holen
46a3019ed7 Fix annotations for here documents (fixes #1071) 2018-01-17 19:20:10 -08:00
Vidar Holen
81978d15bd Remove unused here doc boundary concept. 2018-01-17 18:48:17 -08:00
Vidar Holen
1d0db9267d Mention SC2224-6 about mv/cp/ln without destination 2018-01-17 18:32:07 -08:00
Vidar Holen
a6fb9d1ef8 Warn about C-style comments 2018-01-17 18:14:36 -08:00
Vidar Holen
dc1e7c1bd4 Make docker image shellcheck-alpine behave more like alpine. 2018-01-14 18:01:18 -08:00
Vidar Holen
5b14dba489 Parse 'else if' correctly, and not like elif. Fixes #1088. 2018-01-13 22:42:19 -08:00
koalaman
ee997fdec4 Merge pull request #1093 from albertodonato/snap-package
Add snapcraft.yaml to build snap package
2018-01-13 19:36:06 -08:00
koalaman
1badeff383 Merge pull request #1092 from etam/patch-1
Update openSUSE instruction
2018-01-13 19:33:18 -08:00
Vidar Holen
2d5ed23ca1 Warn about cp/mv/ln with a single argument. Fixes #1080. 2018-01-13 16:44:58 -08:00
Alberto Donato
ec581cee90 Add snapcraft.yaml to build snap package 2018-01-13 00:31:26 +01:00
Adam Mizerski
bb32289ee3 Update openSUSE instruction
Package is main repo of all versions. No need to add repository.
2018-01-11 18:51:39 +01:00
Vidar Holen
31d6b063d9 Improve indented here doc token message. 2018-01-10 21:12:22 -08:00
Vidar Holen
3c5c74ff04 Add quote warning specific to : ${var=val}. Fixes #1084 2018-01-06 10:53:53 -08:00
koalaman
9657e8dda3 Merge pull request #1078 from CyberShadow/pull-20171221-031403
Extend SC2216/SC2217 with 'true' and 'false'
2017-12-20 20:44:06 -08:00
Vladimir Panteleev
6ed60b403f Extend SC2216/SC2217 with 'true' and 'false' 2017-12-21 03:29:17 +00:00
koalaman
8fa8823981 Merge pull request #1072 from vapier/master
convert http:// URIs to https://
2017-12-14 09:35:57 -08:00
Mike Frysinger
161801a86e convert http:// URIs to https://
Also update the ShellCheck homepage to the new dedicated domain.
2017-12-14 01:06:43 -05:00
41 changed files with 2541 additions and 1023 deletions

2
.ghci
View File

@@ -1 +1 @@
:set -idist/build/autogen
:set -idist/build/autogen -isrc

View File

@@ -1,8 +1,8 @@
#### For bugs
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] I read the issue's wiki page, e.g. https://github.com/koalaman/shellcheck/wiki/SC2086
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue
#### For new checks and feature suggestions
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this

9
.gitignore vendored
View File

@@ -1,4 +1,4 @@
# Created by http://www.gitignore.io
# Created by https://www.gitignore.io
### Haskell ###
dist
@@ -13,3 +13,10 @@ cabal-dev
cabal.sandbox.config
cabal.config
.stack-work
### Snap ###
/snap/.snapcraft/
/stage/
/parts/
/prime/
*.snap

View File

@@ -7,7 +7,7 @@ cd deploy
cp ../LICENSE LICENSE.txt
sed -e $'s/$/\r/' > README.txt << END
This is a precompiled ShellCheck binary.
http://www.shellcheck.net/
https://www.shellcheck.net/
ShellCheck is a static analysis tool for shell scripts.
It's licensed under the GNU General Public License v3.0.
@@ -27,7 +27,7 @@ do
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
done
for file in *.linux
for file in *.linux-x86_64
do
base="${file%.*}"
cp "$file" "shellcheck"
@@ -35,6 +35,14 @@ do
rm "shellcheck"
done
for file in *.linux-armv6hf
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in ./*
do
sha512sum "$file" > "$file.sha512sum"

14
.snapsquid.conf Normal file
View File

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

View File

@@ -10,45 +10,53 @@ before_install:
- DOCKER_BUILDS=""
- TAGS=""
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
- test -n "$TRAVIS_TAG" && TAGS="$TAGS $TRAVIS_TAG" || true
- test "$TRAVIS_BRANCH" = master && test -n "$TRAVIS_TAG" && TAGS="$TAGS stable" || true
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
- echo "Tags are $TAGS"
script:
- mkdir deploy
# Windows .exe
- docker pull koalaman/winghc
- docker run --user="$UID" --rm -v "$PWD:/appdata" koalaman/winghc cuib
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
- rm -rf dist || true
# Linux static executable
- docker pull koalaman/scbuilder
- docker run --user="$UID" --rm -v "$PWD:/mnt" koalaman/scbuilder
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux"; done
- ./shellcheck --version
- rm -rf dist || true
# Remove all tests to reduce binary size
- ./striptests
# Linux Docker image
- name="$DOCKER_BASE"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- docker build -t "$name:current" .
- docker run "$name:current" --version
- printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
- docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
- id=$(docker create "$name:current")
- docker cp "$id:/bin/shellcheck" "shellcheck"
- docker rm "$id"
- ls -l shellcheck
- ./shellcheck myscript
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64"; done
# Linux Alpine based Docker image
- name="$DOCKER_BASE-alpine"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- sed 's/^FROM .*/FROM alpine:latest/' Dockerfile > Dockerfile.alpine
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
- docker build -f Dockerfile.alpine -t "$name:current" .
- docker run "$name:current" --version
- docker run "$name:current" sh -c 'shellcheck --version'
# Linux armv6hf static executable
- docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf"; done
- rm -f shellcheck || true
# Windows .exe
- docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
- rm -rf dist shellcheck || true
# Misc packaging
- ./.prepare_deploy
after_success:
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- for repo in $DOCKER_BUILDS;
do
for tag in $TAGS;
do
echo "Deploying $repo:current as $repo:$tag...";
docker tag "$repo:current" "$repo:$tag";
docker push "$repo:$tag";
docker tag "$repo:current" "$repo:$tag" || exit 1;
docker push "$repo:$tag" || exit 1;
done;
done;

View File

@@ -1,3 +1,54 @@
## v0.6.0 - 2018-12-02
### Added
- Command line option --severity/-S for filtering by minimum severity
- Command line option --wiki-link-count/-W for showing wiki links
- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm
- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
- SC1133: Better diagnostics when starting a line with |/||/&&
### Changed
- Most warnings now have useful end positions
- SC1117 about unknown double-quoted escape sequences has been retired
### Fixed
- SC2021 no longer triggers for equivalence classes like `[=e=]`
- SC2221/SC2222 no longer mistriggers on fall-through case branches
- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
- SC2086 no longer warns about spaces in `$#`
- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
- `read -a` is now correctly considered an array assignment
- SC2039 no longer warns about LINENO now that it's POSIX
## v0.5.0 - 2018-05-31
### Added
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
- SC2232: Warn about invalid arguments to sudo
- SC2231: Suggest quoting expansions in for loop globs
- SC2229: Warn about 'read $var'
- SC2227: Warn about redirections in the middle of 'find' commands
- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination
- SC2223: Quote warning specific to `: ${var=value}`
- SC1131: Warn when using `elseif` or `elsif`
- SC1128: Warn about blanks/comments before shebang
- SC1127: Warn about C-style comments
### Fixed
- Annotations intended for a command's here documents now work
- Escaped characters inside groups in =~ regexes now parse
- Associative arrays are now respected in arithmetic contexts
- SC1087 about `$var[@]` now correctly triggers on any index
- Bad expansions in here documents are no longer ignored
- FD move operations like {fd}>1- now parse correctly
### Changed
- Here docs are now terminated as per spec, rather than by presumed intent
- SC1073: 'else if' is now parsed correctly and not like 'elif'
- SC2163: 'export $name' can now be silenced with 'export ${name?}'
- SC2183: Now warns when printf arg count is not a multiple of format count
## v0.4.7 - 2017-12-08
### Added
- Statically linked binaries for Linux and Windows (see README.md)!

View File

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

18
LICENSE
View File

@@ -1,7 +1,17 @@
Employer mandated disclaimer:
I am providing code in the repository to you under an open source license.
Because this is my personal repository, the license you receive to my code is
from me and other individual contributors, and not my employer (Facebook).
- Vidar "koala_man" Holen
----
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -645,7 +655,7 @@ the "copyright" line and a pointer to where the full notice is found.
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@@ -664,11 +674,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/philosophy/why-not-lgpl.html>.

102
README.md
View File

@@ -1,8 +1,10 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](https://travis-ci.org/koalaman/shellcheck)
# ShellCheck - A shell script static analysis tool
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png).
![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png)
The goals of ShellCheck are
@@ -25,7 +27,7 @@ See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for ex
- [In your editor](#in-your-editor)
- [In your build or test suites](#in-your-build-or-test-suites)
- [Installing](#installing)
- [Travis CI Setup](#travis-ci-setup)
- [Travis CI](#travis-ci)
- [Compiling from source](#compiling-from-source)
- [Installing Cabal](#installing-cabal)
- [Compiling ShellCheck](#compiling-shellcheck)
@@ -52,9 +54,9 @@ There are a number of ways to use ShellCheck!
### On the web
Paste a shell script on http://www.shellcheck.net for instant feedback.
Paste a shell script on https://www.shellcheck.net for instant feedback.
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
### From your terminal
@@ -68,7 +70,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).
@@ -99,7 +101,7 @@ On systems with Stack (installs to `~/.local/bin`):
stack update
stack install ShellCheck
On Debian based distros:
apt-get install shellcheck
@@ -108,6 +110,8 @@ On Arch Linux based distros:
pacman -S shellcheck
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
On Gentoo based distros:
emerge --ask shellcheck
@@ -121,62 +125,78 @@ On Fedora based distros:
dnf install ShellCheck
On FreeBSD:
pkg install hs-ShellCheck
On OS X with homebrew:
brew install shellcheck
On OS X with MacPorts:
On OpenBSD:
port install shellcheck
pkg_add shellcheck
On openSUSE:Tumbleweed:
On openSUSE
zypper in ShellCheck
On other openSUSE distributions:
Add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
zypper in ShellCheck
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
On Solus:
eopkg install shellcheck
On Windows (via [scoop](http://scoop.sh)):
scoop install shellcheck
From Snap Store:
snap install --channel=edge shellcheck
From Docker Hub:
```sh
docker pull koalaman/shellcheck:latest # Or :v0.4.6 for a release version
docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
```
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend.
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
Alternatively, get freshly built binaries for the latest commit here:
Alternatively, you can download pre-compiled binaries for the latest release here:
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-latest.linux.x86_64.tar.xz) (statically linked)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-latest.zip)
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums and release builds.
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
## Travis CI Setup
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
If you want to use ShellCheck in Travis CI, you can most easily install it via `apt`:
pandoc -s -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
```yml
language: bash
addons:
apt:
sources:
- debian-sid # Grab ShellCheck from the Debian repo
packages:
- shellcheck
## Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
If you still want to do so in order to upgrade at your leisure or ensure the latest release, follow the steps to install the shellcheck binary, bellow.
## Installing the shellcheck binary
*Pre-requisite*: the program 'xz' needs to be installed on the system.
To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
```bash
export scversion="stable" # or "v0.4.7", or "latest"
wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"
tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
shellcheck --version
```
## Compiling from source
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
@@ -293,6 +313,7 @@ alias archive='mv $1 /backup' # Defining aliases with arguments
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
exec foo; echo "Done!" # Misused 'exec'
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
# find . -exec foo > bar \; # Redirections in find
f() { whoami; }; sudo f # External use of internal functions
```
@@ -308,9 +329,13 @@ var$n="Hello" # Wrong indirect assignment
echo ${var$n} # Wrong indirect reference
var=(1, 2, 3) # Comma separated arrays
array=( [index] = value ) # Incorrect index initialization
echo $var[14] # Missing {} in array references
echo "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
f; f() { echo "hello world; } # Using function before definition
[ false ] # 'false' being true
if ( -f file ) # Using (..) instead of test
```
### Style
@@ -341,6 +366,8 @@ printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
var=World; echo "Hello " var # Unused lowercase variables
echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
cat foo | cp bar # Piping to commands that don't read
printf '%s: %s\n' foo # Mismatches in printf argument count
```
### Robustness
@@ -354,6 +381,7 @@ find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
printf "Hello $name" # Variables in printf format
for f in $(ls *.txt); do # Iterating over ls output
export MYVAR=$(cmd) # Masked exit codes
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
```
### Portability
@@ -388,6 +416,7 @@ var=42 echo $var # Expansion of inlined environment
echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
```
## Testimonials
@@ -422,6 +451,11 @@ The contributor retains the copyright.
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
Happy ShellChecking!
## Other Resources
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!

View File

@@ -33,4 +33,4 @@ myPreSDist _ _ = do
putStrLn $ "pandoc exited with " ++ show result
return emptyHookedBuildInfo
where
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,12 +1,12 @@
Name: ShellCheck
Version: 0.4.7
Version: 0.6.0
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
Category: Static Analysis
Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: http://www.shellcheck.net/
Homepage: https://www.shellcheck.net/
Build-Type: Custom
Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues
@@ -28,19 +28,34 @@ Extra-Source-Files:
shellcheck.1.md
-- built with a cabal sdist hook
shellcheck.1
-- convenience script for stripping tests
striptests
-- tests
test/shellcheck.hs
custom-setup
setup-depends:
base >= 4 && <5,
process >= 1.0 && <1.7,
Cabal >= 1.10 && <2.5
source-repository head
type: git
location: git://github.com/koalaman/shellcheck.git
library
hs-source-dirs: src
if impl(ghc < 8.0)
build-depends:
semigroups
build-depends:
base >= 4 && < 5,
containers,
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-- Just disable that version entirely to fail fast.
aeson,
base > 4.6.0.1 && < 5,
bytestring,
containers >= 0.5,
directory,
json,
mtl >= 2.2.1,
parsec,
regex-tdfa,
@@ -69,27 +84,34 @@ library
Paths_ShellCheck
executable shellcheck
if impl(ghc < 8.0)
build-depends:
semigroups
build-depends:
aeson,
base >= 4 && < 5,
bytestring,
ShellCheck,
containers,
directory,
json,
mtl >= 2.2.1,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4
parsec >= 3.0,
QuickCheck >= 2.7.4,
regex-tdfa
main-is: shellcheck.hs
test-suite test-shellcheck
type: exitcode-stdio-1.0
build-depends:
aeson,
base >= 4 && < 5,
bytestring,
ShellCheck,
containers,
directory,
json,
mtl >= 2.2.1,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4
QuickCheck >= 2.7.4,
regex-tdfa
main-is: test/shellcheck.hs

View File

@@ -1,121 +0,0 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
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 <http://www.gnu.org/licenses/>.
-}
module ShellCheck.Interface where
import ShellCheck.AST
import Control.Monad.Identity
import qualified Data.Map as Map
newtype SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String)
}
-- ShellCheck input and output
data CheckSpec = CheckSpec {
csFilename :: String,
csScript :: String,
csCheckSourced :: Bool,
csExcludedWarnings :: [Integer],
csShellTypeOverride :: Maybe Shell
} deriving (Show, Eq)
data CheckResult = CheckResult {
crFilename :: String,
crComments :: [PositionedComment]
} deriving (Show, Eq)
emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec {
csFilename = "",
csScript = "",
csCheckSourced = False,
csExcludedWarnings = [],
csShellTypeOverride = Nothing
}
-- Parser input and output
data ParseSpec = ParseSpec {
psFilename :: String,
psScript :: String,
psCheckSourced :: Bool
} deriving (Show, Eq)
data ParseResult = ParseResult {
prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id Position,
prRoot :: Maybe Token
} deriving (Show, Eq)
-- Analyzer input and output
data AnalysisSpec = AnalysisSpec {
asScript :: Token,
asShellType :: Maybe Shell,
asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool
}
newtype AnalysisResult = AnalysisResult {
arComments :: [TokenComment]
}
-- Formatter options
newtype FormatterOptions = FormatterOptions {
foColorOption :: ColorOption
}
-- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String
type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
data Position = Position {
posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq)
data Comment = Comment Severity Code String deriving (Show, Eq)
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
data ColorOption =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {
siReadFile = rf
}
where
rf file =
case filter ((== file) . fst) files of
[] -> return $ Left "File not included in mock."
[(_, contents)] -> return $ Right contents

View File

@@ -1,7 +1,10 @@
#!/bin/bash
#!/usr/bin/env bash
# TODO: Find a less trashy way to get the next available error code
shopt -s globstar
if ! shopt -s globstar
then
echo "Error: This script depends on Bash 4." >&2
exit 1
fi
for i in 1 2
do

View File

@@ -1,5 +1,5 @@
#!/bin/bash
#!/usr/bin/env bash
# quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling.
runghc -idist/build/autogen shellcheck.hs "$@"
runghc -isrc -idist/build/autogen shellcheck.hs "$@"

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# quicktest runs the ShellCheck unit tests in an interpreted mode.
# This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth.

View File

@@ -56,6 +56,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information.
**-S**\ *SEVERITY*,\ **--severity=***severity*
: Specify minimum severity of errors to consider. Valid values are *error*,
*warning*, *info* and *style*. The default is *style*.
**-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
@@ -66,6 +71,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Print version information and exit.
**-W** *NUM*,\ **--wiki-link-count=NUM**
: For TTY output, show *NUM* wiki links to more information about mentioned
warnings. Set to 0 to disable them entirely.
**-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input.
@@ -207,7 +217,7 @@ https://github.com/koalaman/shellcheck/issues
# COPYRIGHT
Copyright 2012-2015, Vidar Holen.
Licensed under the GNU General Public License version 3 or later,
see http://gnu.org/licenses/gpl.html
see https://gnu.org/licenses/gpl.html
# SEE ALSO

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,37 +15,38 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
import ShellCheck.Data
import ShellCheck.Checker
import ShellCheck.Interface
import ShellCheck.Regex
import ShellCheck.Checker
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Regex
import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.CheckStyle
import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.GCC
import qualified ShellCheck.Formatter.JSON
import qualified ShellCheck.Formatter.TTY
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Data.Bits
import Data.Char
import Data.Either
import Data.Functor
import Data.IORef
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Monoid
import Prelude hiding (catch)
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.IO
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Data.Bits
import Data.Char
import Data.Either
import Data.Functor
import Data.IORef
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Monoid
import Data.Semigroup (Semigroup (..))
import Prelude hiding (catch)
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.IO
data Flag = Flag String String
data Status =
@@ -56,22 +57,27 @@ data Status =
| RuntimeException
deriving (Ord, Eq, Show)
instance Semigroup Status where
(<>) = max
instance Monoid Status where
mempty = NoProblems
mappend = max
mappend = (Data.Semigroup.<>)
data Options = Options {
checkSpec :: CheckSpec,
externalSources :: Bool,
formatterOptions :: FormatterOptions
checkSpec :: CheckSpec,
externalSources :: Bool,
formatterOptions :: FormatterOptions,
minSeverity :: Severity
}
defaultOptions = Options {
checkSpec = emptyCheckSpec,
externalSources = False,
formatterOptions = FormatterOptions {
formatterOptions = newFormatterOptions {
foColorOption = ColorAuto
}
},
minSeverity = StyleC
}
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@@ -89,8 +95,14 @@ options = [
Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)",
Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)",
Option "V" ["version"]
(NoArg $ Flag "version" "true") "Print version information",
Option "W" ["wiki-link-count"]
(ReqArg (Flag "wiki-link-count") "NUM")
"The number of wiki links to show, when applicable.",
Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
]
@@ -117,9 +129,9 @@ formatList = intercalate ", " names
where
names = Map.keys $ formats (formatterOptions defaultOptions)
getOption [] _ = Nothing
getOption [] _ = Nothing
getOption (Flag var val:_) name | name == var = return val
getOption (_:rest) flag = getOption rest flag
getOption (_:rest) flag = getOption rest flag
getOptions options name =
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
@@ -133,12 +145,6 @@ split char str =
else split' rest (a:element)
split' [] element = [reverse element]
getExclusions options =
let elements = concatMap (split ',') $ getOptions options "exclude"
clean = dropWhile (not . isDigit)
in
map (Prelude.read . clean) elements :: [Int]
toStatus = fmap (either id id) . runExceptT
getEnvArgs = do
@@ -159,10 +165,10 @@ main = do
statusToCode status =
case status of
NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1
SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4
NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1
SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4
RuntimeException -> ExitFailure 2
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
@@ -203,7 +209,7 @@ runFormatter sys format options files = do
process :: FilePath -> IO Status
process filename = do
input <- (siReadFile sys) filename
input <- siReadFile sys filename
either (reportFailure filename) check input
where
check contents = do
@@ -218,12 +224,28 @@ runFormatter sys format options files = do
then NoProblems
else SomeProblems
parseColorOption colorOption =
case colorOption of
"auto" -> ColorAuto
"always" -> ColorAlways
"never" -> ColorNever
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
parseEnum name value list =
case filter ((== value) . fst) list of
[(name, value)] -> return value
[] -> do
printErr $ "Unknown value for --" ++ name ++ ". " ++
"Valid options are: " ++ (intercalate ", " $ map fst list)
throwError SupportFailure
parseColorOption value =
parseEnum "color" value [
("auto", ColorAuto),
("always", ColorAlways),
("never", ColorNever)
]
parseSeverityOption value =
parseEnum "severity" value [
("error", ErrorC),
("warning", WarningC),
("info", InfoC),
("style", StyleC)
]
parseOption flag options =
case flag of
@@ -237,7 +259,7 @@ parseOption flag options =
}
Flag "exclude" str -> do
new <- mapM parseNum $ split ',' str
new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csExcludedWarnings . checkSpec $ options
return options {
checkSpec = (checkSpec options) {
@@ -254,10 +276,11 @@ parseOption flag options =
externalSources = True
}
Flag "color" color ->
Flag "color" color -> do
option <- parseColorOption color
return options {
formatterOptions = (formatterOptions options) {
foColorOption = parseColorOption color
foColorOption = option
}
}
@@ -268,6 +291,22 @@ parseOption flag options =
}
}
Flag "severity" severity -> do
option <- parseSeverityOption severity
return options {
checkSpec = (checkSpec options) {
csMinSeverity = option
}
}
Flag "wiki-link-count" countString -> do
count <- parseNum countString
return options {
formatterOptions = (formatterOptions options) {
foWikiLinkCount = count
}
}
_ -> return options
where
die s = do
@@ -276,7 +315,7 @@ parseOption flag options =
parseNum ('S':'C':str) = parseNum str
parseNum num = do
unless (all isDigit num) $ do
printErr $ "Bad exclusion: " ++ num
printErr $ "Invalid number: " ++ num
throwError SyntaxFailure
return (Prelude.read num :: Integer)
@@ -292,7 +331,7 @@ ioInterface options files = do
get cache inputs file = do
map <- readIORef cache
case Map.lookup file map of
Just x -> return $ Right x
Just x -> return $ Right x
Nothing -> fetch cache inputs file
fetch cache inputs file = do
@@ -355,7 +394,7 @@ decodeString = decode
in
case next of
Just (n, remainder) -> chr n : decode remainder
Nothing -> c : decode rest
Nothing -> c : decode rest
construct x 0 rest = do
guard $ x <= 0x10FFFF
@@ -378,4 +417,4 @@ printVersion = do
putStrLn "ShellCheck - shell script analysis tool"
putStrLn $ "version: " ++ shellcheckVersion
putStrLn "license: GNU General Public License, version 3"
putStrLn "website: http://www.shellcheck.net"
putStrLn "website: https://www.shellcheck.net"

53
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,53 @@
name: shellcheck
summary: A shell script static analysis tool
description: |
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh
shell scripts.
The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues that cause a
shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future
circumstances.
By default ShellCheck can only check non-hidden files under /home, to make
ShellCheck be able to check files under /media and /run/media you must
connect it to the `removable-media` interface manually:
# snap connect shellcheck:removable-media
version: git
grade: devel
confinement: strict
apps:
shellcheck:
command: usr/bin/shellcheck
plugs: [home, removable-media]
parts:
shellcheck:
plugin: dump
source: ./
build-packages:
- cabal-install
- squid3
build: |
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init
cabal update || cat /var/log/squid/*
cabal install -j
install: |
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.AST where
@@ -37,8 +37,8 @@ newtype Root = Root Token
data Token =
TA_Binary Id String Token Token
| TA_Assignment Id String Token Token
| TA_Variable Id String [Token]
| TA_Expansion Id [Token]
| TA_Index Id Token
| TA_Sequence Id [Token]
| TA_Trinary Id Token Token Token
| TA_Unary Id String Token
@@ -134,7 +134,8 @@ data Token =
| T_Pipe Id String
| T_CoProc Id (Maybe String) Token
| T_CoProcBody Id Token
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
| T_Include Id Token
| T_SourceCommand Id Token Token
deriving (Show)
data Annotation =
@@ -266,11 +267,12 @@ analyze f g i =
c <- round t3
return $ TA_Trinary id a b c
delve (TA_Expansion id t) = dl t $ TA_Expansion id
delve (TA_Index id t) = d1 t $ TA_Index id
delve (TA_Variable id str t) = dl t $ TA_Variable id str
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
delve (T_Include id includer script) = d2 includer script $ T_Include id
delve (T_Include id script) = d1 script $ T_Include id
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
delve t = return t
getId :: Token -> Id
@@ -360,7 +362,6 @@ getId t = case t of
TA_Sequence id _ -> id
TA_Trinary id _ _ _ -> id
TA_Expansion id _ -> id
TA_Index id _ -> id
T_ProcSub id _ _ -> id
T_Glob id _ -> id
T_ForArithmetic id _ _ _ _ -> id
@@ -371,9 +372,11 @@ getId t = case t of
T_Pipe id _ -> id
T_CoProc id _ _ -> id
T_CoProcBody id _ -> id
T_Include id _ _ -> id
T_Include id _ -> id
T_SourceCommand id _ _ -> id
T_UnparsedIndex id _ _ -> id
TC_Empty id _ -> id
TA_Variable id _ _ -> id
blank :: Monad m => Token -> m ()
blank = const $ return ()

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.ASTLib where
@@ -23,6 +23,7 @@ import ShellCheck.AST
import Control.Monad.Writer
import Control.Monad
import Data.Char
import Data.Functor
import Data.List
import Data.Maybe
@@ -112,6 +113,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
-- Get all flags in a GNU way, up until --
getAllFlags :: Token -> [(Token, String)]
getAllFlags = getFlagsUntil (== "--")
-- Get all flags in a BSD way, up until first non-flag argument or --
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
@@ -225,8 +227,43 @@ getLiteralStringExt more = g
g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
g x = more x
-- Bash style $'..' decoding
decodeEscapes ('\\':c:cs) =
case c of
'a' -> '\a' : rest
'b' -> '\b' : rest
'e' -> '\x1B' : rest
'f' -> '\f' : rest
'n' -> '\n' : rest
'r' -> '\r' : rest
't' -> '\t' : rest
'v' -> '\v' : rest
'\'' -> '\'' : rest
'"' -> '"' : rest
'\\' -> '\\' : rest
'x' ->
case cs of
(x:y:more) ->
if isHexDigit x && isHexDigit y
then chr (16*(digitToInt x) + (digitToInt y)) : rest
else '\\':c:rest
_ | isOctDigit c ->
let digits = take 3 $ takeWhile isOctDigit (c:cs)
num = parseOct digits
in (if num < 256 then chr num else '?') : rest
_ -> '\\' : c : rest
where
rest = decodeEscapes cs
parseOct = f 0
where
f n "" = n
f n (c:rest) = f (n * 8 + digitToInt c) rest
decodeEscapes (c:cs) = c : decodeEscapes cs
decodeEscapes [] = []
-- Is this token a string literal?
isLiteral t = isJust $ getLiteralString t
@@ -256,17 +293,27 @@ getCommand t =
T_Annotation _ _ t -> getCommand t
_ -> Nothing
-- Maybe get the command name of a token representing a command
getCommandName t = do
-- Maybe get the command name string of a token representing a command
getCommandName :: Token -> Maybe String
getCommandName = fst . getCommandNameAndToken
-- Get the command name token from a command, i.e.
-- the token representing 'ls' in 'ls -la 2> foo'.
-- If it can't be determined, return the original token.
getCommandTokenOrThis = snd . getCommandNameAndToken
getCommandNameAndToken :: Token -> (Maybe String, Token)
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w
if "busybox" `isSuffixOf` s
if "busybox" `isSuffixOf` s || "builtin" == s
then
case rest of
(applet:_) -> getLiteralString applet
_ -> return s
(applet:_) -> return (getLiteralString applet, applet)
_ -> return (Just s, w)
else
return s
return (Just s, w)
-- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date"
@@ -434,3 +481,12 @@ pseudoGlobIsSuperSetof = matchable
wordsCanBeEqual x y = fromMaybe True $
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
-- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})?
isQuoteableExpansion t = case t of
T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True
T_Backticked {} -> True
T_DollarBraced {} -> True
_ -> False

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,9 +15,10 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where
import ShellCheck.AST
@@ -119,7 +120,6 @@ nodeChecks = [
,checkGlobbedRegex
,checkTestRedirects
,checkIndirectExpansion
,checkSudoRedirect
,checkPS1Assignments
,checkBackticks
,checkInexplicablyUnquoted
@@ -166,6 +166,10 @@ nodeChecks = [
,checkFlagAsCommand
,checkEmptyCondition
,checkPipeToNowhere
,checkForLoopGlobVariables
,checkSubshelledTests
,checkInvertedStringTest
,checkRedirectionToCommand
]
@@ -394,7 +398,7 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices
return . not . null $ indices
for' l f = for l (first f)
first func (x:_) = func (getId x)
first func (x:_) = func (getId $ getCommandTokenOrThis x)
first _ _ = return ()
hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
hasParameter string =
@@ -428,17 +432,22 @@ prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
prop_checkShebang8 = verifyTree checkShebang "#!bin/sh\ntrue"
prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"
prop_checkShebang10= verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"
checkShebang params (T_Annotation _ list t) =
if any isOverride list then [] else checkShebang params t
where
isOverride (ShellOverride _) = True
isOverride _ = False
checkShebang params (T_Script id sb _) = execWriter $
checkShebang params (T_Script id sb _) = execWriter $ do
unless (shellTypeSpecified params) $ do
when (sb == "") $
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
when (executableFromShebang sb == "ash") $
warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
unless (null sb || "/" `isPrefixOf` sb) $
err id 2239 "Ensure the shebang uses an absolute path to the interpreter."
prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
@@ -572,6 +581,7 @@ prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/
prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
prop_checkRedirectToSame8 = verifyNot checkRedirectToSame "while read -r line; do _=\"$fname\"; done <\"$fname\""
checkRedirectToSame params s@(T_Pipeline _ _ list) =
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
where
@@ -582,7 +592,8 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
&& x == y
&& not (isOutput t && isOutput u)
&& not (special t)
&& not (any isHarmlessCommand [t,u])) $ do
&& not (any isHarmlessCommand [t,u])
&& not (any containsAssignment [u])) $ do
addComment $ note newId
addComment $ note exceptId
checkOccurrences _ _ = return ()
@@ -609,6 +620,9 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
cmd <- getClosestCommand (parentMap params) arg
name <- getCommandBasename cmd
return $ name `elem` ["echo", "printf", "sponge"]
containsAssignment arg = fromMaybe False $ do
cmd <- getClosestCommand (parentMap params) arg
return $ isAssignment cmd
checkRedirectToSame _ _ = return ()
@@ -702,6 +716,7 @@ prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\""
checkArrayWithoutIndex params _ =
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
where
@@ -742,6 +757,7 @@ prop_checkStderrRedirect3 = verifyNot checkStderrRedirect "test 2>&1 > file | gr
prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
prop_checkStderrRedirect7 = verifyNot checkStderrRedirect "{ cmd > file; } 2>&1"
checkStderrRedirect params redir@(T_Redirecting _ [
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
T_FdRedirect _ _ (T_IoFile _ op _)
@@ -760,7 +776,7 @@ checkStderrRedirect params redir@(T_Redirecting _ [
isCaptured = any usesOutput $ getPath (parentMap params) redir
error = unless isCaptured $
err id 2069 "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last."
warn id 2069 "To redirect stdout+stderr, 2>&1 must be last (or use '{ cmd > file; } 2>&1' to clarify)."
checkStderrRedirect _ _ = return ()
@@ -786,6 +802,10 @@ prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"
prop_checkSingleQuotedVariables15= verifyNot checkSingleQuotedVariables "git filter-branch 'test $GIT_COMMIT'"
prop_checkSingleQuotedVariables16= verify checkSingleQuotedVariables "git '$a'"
prop_checkSingleQuotedVariables17= verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *"
checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
when (s `matches` re) $
if "sed" == commandName
@@ -798,7 +818,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
commandName = fromMaybe "" $ do
cmd <- getClosestCommand parents t
name <- getCommandBasename cmd
return $ if name == "find" then getFindCommand cmd else name
return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else name
isProbablyOk =
any isOkAssignment (take 3 $ getPath parents t)
@@ -813,8 +833,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
,"xprop"
,"alias"
,"sudo" -- covering "sudo sh" and such
,"docker" -- like above
,"dpkg-query"
,"jq" -- could also check that user provides --arg
,"rename"
,"unset"
,"git filter-branch"
]
|| "awk" `isSuffixOf` commandName
|| "perl" `isPrefixOf` commandName
@@ -838,6 +862,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
_ -> "find"
getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
getFindCommand _ = "find"
getGitCommand (T_SimpleCommand _ _ words) =
case map getLiteralString words of
Just "git":Just "filter-branch":_ -> "git filter-branch"
_ -> "git"
getGitCommand (T_Redirecting _ _ cmd) = getGitCommand cmd
getGitCommand _ = "git"
checkSingleQuotedVariables _ _ = return ()
@@ -845,6 +875,7 @@ prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]"
checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
checkUnquotedN _ _ = return ()
@@ -996,7 +1027,7 @@ checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
error t =
unless (isConstantNonRe t) $
err (getId t) 2076
"Don't quote rhs of =~, it'll match literally rather than as a regex."
"Don't quote right-hand side of =~, it'll match literally rather than as a regex."
re = mkRegex "[][*.+()|]"
hasMetachars s = s `matches` re
isConstantNonRe t = fromMaybe False $ do
@@ -1006,13 +1037,16 @@ checkQuotedCondRegex _ _ = return ()
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
prop_checkGlobbedRegex5 = verifyNot checkGlobbedRegex "[[ $foo =~ \\* ]]"
prop_checkGlobbedRegex6 = verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]"
prop_checkGlobbedRegex7 = verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]"
prop_checkGlobbedRegex8 = verifyNot checkGlobbedRegex "[[ $foo =~ x\\* ]]"
checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
let s = concat $ oversimplify rhs in
when (isConfusedGlobRegex s) $
warn (getId rhs) 2049 "=~ is for regex. Use == for globs."
warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."
checkGlobbedRegex _ _ = return ()
@@ -1140,12 +1174,10 @@ checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
T_Arithmetic {} -> return normalWarning
T_DollarArithmetic {} -> return normalWarning
T_ForArithmetic {} -> return normalWarning
TA_Index {} -> return indexWarning
T_SimpleCommand {} -> return noWarning
_ -> Nothing
normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
indexWarning = style id 2149 "Remove $/${} for numeric index, or escape it for string."
noWarning = return ()
checkArithmeticDeref _ _ = return ()
@@ -1165,11 +1197,12 @@ prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
| op `elem` ["=", "==", "!="] =
warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
| (op == "=" || op == "==") && isGlob word =
| op `elem` ["=", "==", "!="] && isGlob word =
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
checkComparisonAgainstGlob _ _ = return ()
@@ -1276,33 +1309,6 @@ checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
_ -> False
checkTestRedirects _ _ = return ()
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
checkSudoRedirect _ (T_Redirecting _ redirs cmd) | cmd `isCommand` "sudo" =
mapM_ warnAbout redirs
where
warnAbout (T_FdRedirect _ s (T_IoFile id op file))
| (s == "" || s == "&") && not (special file) =
case op of
T_Less _ ->
info (getId op) 2024
"sudo doesn't affect redirects. Use sudo cat file | .."
T_Greater _ ->
warn (getId op) 2024
"sudo doesn't affect redirects. Use ..| sudo tee file"
T_DGREAT _ ->
warn (getId op) 2024
"sudo doesn't affect redirects. Use .. | sudo tee -a file"
_ -> return ()
warnAbout _ = return ()
special file = concat (oversimplify file) == "/dev/null"
checkSudoRedirect _ _ = return ()
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
@@ -1330,7 +1336,7 @@ prop_checkBackticks1 = verify checkBackticks "echo `foo`"
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
checkBackticks _ (T_Backticked id list) | not (null list) =
style id 2006 "Use $(..) instead of legacy `..`."
style id 2006 "Use $(...) notation instead of legacy backticked `...`."
checkBackticks _ _ = return ()
prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
@@ -1426,6 +1432,7 @@ prop_checkSpuriousExec4 = verifyNot checkSpuriousExec "if a; then exec b; fi"
prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"
prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar"
checkSpuriousExec _ = doLists
where
doLists (T_Script _ _ cmds) = doList cmds
@@ -1603,6 +1610,7 @@ prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"
checkSpacefulness params t =
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
@@ -1617,16 +1625,23 @@ checkSpacefulness params t =
modify $ Map.insert name bool
readF _ token name = do
spaced <- hasSpaces name
return [makeComment InfoC (getId token) 2086 warning |
isExpansion token && spaced
spaces <- hasSpaces name
return [warning |
isExpansion token && spaces
&& not (isArrayExpansion token) -- There's another warning for this
&& not (isCountingReference token)
&& not (isQuoteFree parents token)
&& not (isQuotedAlternativeReference token)
&& not (usedAsCommandName parents token)]
where
warning = "Double quote to prevent globbing and word splitting."
warning =
if isDefaultAssignment (parentMap params) token
then
makeComment InfoC (getId token) 2223
"This default assignment may cause DoS due to globbing. Quote it."
else
makeComment InfoC (getId token) 2086
"Double quote to prevent globbing and word splitting."
writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
@@ -1665,6 +1680,12 @@ checkSpacefulness params t =
globspace = "*?[] \t\n"
containsAny s = any (`elem` s)
isDefaultAssignment parents token =
let modifier = getBracedModifier $ bracedString token in
isExpansion token
&& any (`isPrefixOf` modifier) ["=", ":="]
&& isParamTo parents ":" token
prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""
@@ -1811,6 +1832,9 @@ prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo ));
prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"
prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"
prop_checkUnused38= verifyTree checkUnusedAssignments "(( a=42 ))"
prop_checkUnused39= verifyNotTree checkUnusedAssignments "declare -x -f foo"
prop_checkUnused40= verifyNotTree checkUnusedAssignments "arr=(1 2); num=2; echo \"${arr[@]:num}\""
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
where
flow = variableFlow params
@@ -1828,7 +1852,7 @@ checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
warnFor (name, token) =
warn (getId token) 2034 $
name ++ " appears unused. Verify it or export it."
name ++ " appears unused. Verify use (or export if used externally)."
stripSuffix = takeWhile isVariableChar
defaultMap = Map.fromList $ zip internalVariables $ repeat ()
@@ -1865,6 +1889,10 @@ prop_checkUnassignedReferences29= verifyNotTree checkUnassignedReferences "if [[
prop_checkUnassignedReferences30= verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"
prop_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"
prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"
prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""
checkUnassignedReferences params t = warnings
where
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
@@ -1924,11 +1952,13 @@ checkUnassignedReferences params t = warnings
isArray _ = False
isGuarded (T_DollarBraced _ v) =
any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
rest `matches` guardRegex
where
name = concat $ oversimplify v
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
isGuarded _ = False
-- :? or :- with optional array index and colon
guardRegex = mkRegex "^(\\[.*\\])?:?[-?]"
match var candidate =
if var /= candidate && map toLower var == map toLower candidate
@@ -2053,6 +2083,7 @@ prop_checkCdAndBack1 = verify checkCdAndBack "for f in *; do cd $f; git pull; cd
prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
prop_checkCdAndBack5 = verifyNot checkCdAndBack "cd ..; foo; cd .."
checkCdAndBack params = doLists
where
shell = shellType params
@@ -2075,10 +2106,20 @@ checkCdAndBack params = doLists
getCmd (T_Pipeline id _ [x]) = getCommandName x
getCmd _ = Nothing
findCdPair list =
case list of
(a:b:rest) ->
if isCdRevert b && not (isCdRevert a)
then return $ getId b
else findCdPair (b:rest)
_ -> Nothing
doList list =
let cds = filter ((== Just "cd") . getCmd) list in
when (length cds >= 2 && isCdRevert (last cds)) $
info (getId $ last cds) 2103 message
potentially $ do
cd <- findCdPair cds
return $ info cd 2103 message
message = "Use a ( subshell ) to avoid having to cd back."
@@ -2456,7 +2497,7 @@ prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
unless ("r" `elem` map snd (getAllFlags t)) $
info (getId t) 2162 "read without -r will mangle backslashes."
info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes."
checkReadWithoutR _ _ = return ()
prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
@@ -2467,6 +2508,7 @@ prop_checkUncheckedCd5 = verifyTree checkUncheckedCdPushdPopd "if true; then cd
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar"
prop_checkUncheckedCd9 = verifyTree checkUncheckedCdPushdPopd "builtin cd ~/src; rm -r foo"
prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"
prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo"
@@ -2484,6 +2526,7 @@ prop_checkUncheckedPopd5 = verifyTree checkUncheckedCdPushdPopd "if true; then p
prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
prop_checkUncheckedPopd9 = verifyNotTree checkUncheckedCdPushdPopd "popd -n foo"
checkUncheckedCdPushdPopd params root =
if hasSetE params then
@@ -2493,7 +2536,7 @@ checkUncheckedCdPushdPopd params root =
checkElement t@T_SimpleCommand {} =
when(name t `elem` ["cd", "pushd", "popd"]
&& not (isSafeDir t)
&& not (name t == "pushd" && ("n" `elem` map snd (getAllFlags t)))
&& not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))
&& not (isCondition $ getPath (parentMap params) t)) $
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
checkElement _ = return ()
@@ -2525,7 +2568,7 @@ checkLoopVariableReassignment params token =
T_ForArithmetic _
(TA_Sequence _
[TA_Assignment _ "="
(TA_Expansion _ [T_Literal _ var]) _])
(TA_Variable _ var _ ) _])
_ _ _ -> return var
_ -> fail "not loop"
@@ -2668,26 +2711,31 @@ prop_checkUnmatchableCases5 = verify checkUnmatchableCases "case $f in *.txt) tr
prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"
prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac"
prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac"
prop_checkUnmatchableCases9 = verifyNot checkUnmatchableCases "case $f in x) true;;& x) false;; esac"
checkUnmatchableCases _ t =
case t of
T_CaseExpression _ word list -> do
let patterns = concatMap snd3 list
-- Check all patterns for whether they can ever match
let allpatterns = concatMap snd3 list
-- Check only the non-fallthrough branches for shadowing
let breakpatterns = concatMap snd3 $ filter (\x -> fst3 x == CaseBreak) list
if isConstant word
then warn (getId word) 2194
"This word is constant. Did you forget the $ on a variable?"
else potentially $ do
pg <- wordToPseudoGlob word
return $ mapM_ (check pg) patterns
return $ mapM_ (check pg) allpatterns
let exactGlobs = tupMap wordToExactPseudoGlob patterns
let fuzzyGlobs = tupMap wordToPseudoGlob patterns
let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns
let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
mapM_ checkDoms dominators
_ -> return ()
where
fst3 (x,_,_) = x
snd3 (_,x,_) = x
check target candidate = potentially $ do
candidateGlob <- wordToPseudoGlob candidate
@@ -2819,6 +2867,8 @@ prop_checkPipeToNowhere4 = verify checkPipeToNowhere "printf 'Lol' << eof\nlol\n
prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin"
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
checkPipeToNowhere _ t =
case t of
@@ -2831,15 +2881,25 @@ checkPipeToNowhere _ t =
name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd
-- Confusing echo for cat is so common that it's worth a special case
let suggestion =
if name == "echo"
then "Did you want 'cat' instead?"
else "Wrong command or missing xargs?"
return $ warn (getId cmd) 2216 $
"Piping to '" ++ name ++ "', a command that doesn't read stdin. Wrong command or missing xargs?"
"Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
checkRedir cmd = potentially $ do
name <- getCommandBasename cmd
guard $ name `elem` nonReadingCommands
guard . not $ hasAdditionalConsumers cmd
guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i"
let suggestion =
if name == "echo"
then "Did you want 'cat' instead?"
else "Bad quoting, wrong command or missing xargs?"
return $ warn (getId cmd) 2217 $
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. Bad quoting or missing xargs?"
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
hasAdditionalConsumers t = fromMaybe True $ do
@@ -2891,5 +2951,113 @@ checkUseBeforeDefinition _ t =
then [x]
else concatMap recursiveSequences list
prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done"
prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done"
prop_checkForLoopGlobVariables3 = verifyNot checkForLoopGlobVariables "for i in $var; do true; done"
checkForLoopGlobVariables _ t =
case t of
T_ForIn _ _ words _ -> mapM_ check words
_ -> return ()
where
check (T_NormalWord _ parts) =
when (any isGlob parts) $
mapM_ suggest $ filter isQuoteableExpansion parts
suggest t = info (getId t) 2231
"Quote expansions in this for loop glob to prevent wordsplitting, e.g. \"$dir\"/*.txt ."
prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )"
prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )"
prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )"
prop_checkSubshelledTests4 = verify checkSubshelledTests "( [ a ] && { [ b ] && [ c ]; } )"
checkSubshelledTests params t =
case t of
T_Subshell id list | all isTestStructure list ->
case () of
-- Special case for if (test) and while (test)
_ | isCompoundCondition (getPath (parentMap params) t) ->
style id 2233 "Remove superfluous (..) around condition."
-- Special case for ([ x ])
_ | isSingleTest list ->
style id 2234 "Remove superfluous (..) around test command."
-- General case for ([ x ] || [ y ] && etc)
_ -> style id 2235 "Use { ..; } instead of (..) to avoid subshell overhead."
_ -> return ()
where
isSingleTest cmds =
case cmds of
[c] | isTestCommand c -> True
_ -> False
isTestStructure t =
case t of
T_Banged _ t -> isTestStructure t
T_AndIf _ a b -> isTestStructure a && isTestStructure b
T_OrIf _ a b -> isTestStructure a && isTestStructure b
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
case cmd of
T_BraceGroup _ ts -> all isTestStructure ts
T_Subshell _ ts -> all isTestStructure ts
_ -> isTestCommand t
_ -> isTestCommand t
isTestCommand t =
case t of
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
case cmd of
T_Condition {} -> True
_ -> cmd `isCommand` "test"
_ -> False
-- Check if a T_Subshell is used as a condition, e.g. if ( test )
-- This technically also triggers for `if true; then ( test ); fi`
-- but it's still a valid suggestion.
isCompoundCondition chain =
case dropWhile skippable (drop 1 chain) of
T_IfExpression {} : _ -> True
T_WhileExpression {} : _ -> True
T_UntilExpression {} : _ -> True
_ -> False
-- Skip any parent of a T_Subshell until we reach something interesting
skippable t =
case t of
T_Redirecting _ [] _ -> True
T_Pipeline _ [] _ -> True
T_Annotation {} -> True
_ -> False
prop_checkInvertedStringTest1 = verify checkInvertedStringTest "[ ! -z $var ]"
prop_checkInvertedStringTest2 = verify checkInvertedStringTest "! [[ -n $var ]]"
prop_checkInvertedStringTest3 = verifyNot checkInvertedStringTest "! [ -x $var ]"
prop_checkInvertedStringTest4 = verifyNot checkInvertedStringTest "[[ ! -w $var ]]"
prop_checkInvertedStringTest5 = verifyNot checkInvertedStringTest "[ -z $var ]"
checkInvertedStringTest _ t =
case t of
TC_Unary _ _ "!" (TC_Unary _ _ op _) ->
case op of
"-n" -> style (getId t) 2236 "Use -z instead of ! -n."
"-z" -> style (getId t) 2236 "Use -n instead of ! -z."
_ -> return ()
T_Banged _ (T_Pipeline _ _
[T_Redirecting _ _ (T_Condition _ _ (TC_Unary _ _ op _))]) ->
case op of
"-n" -> style (getId t) 2237 "Use [ -z .. ] instead of ! [ -n .. ]."
"-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]."
_ -> return ()
_ -> return ()
prop_checkRedirectionToCommand1 = verify checkRedirectionToCommand "ls > rm"
prop_checkRedirectionToCommand2 = verifyNot checkRedirectionToCommand "ls > 'rm'"
prop_checkRedirectionToCommand3 = verifyNot checkRedirectionToCommand "ls > myfile"
checkRedirectionToCommand _ t =
case t of
T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands ->
unless (str == "file") $ -- This would be confusing
warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?"
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Analyzer (analyzeScript) where
@@ -30,7 +30,7 @@ import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript spec = AnalysisResult {
analyzeScript spec = newAnalysisResult {
arComments =
filterByAnnotation spec params . nub $
runAnalytics spec

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,30 +15,31 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Arrow (first)
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Control.Arrow (first)
import Control.Monad.Identity
import Control.Monad.RWS
import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Semigroup
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
type Analysis = AnalyzerM ()
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
@@ -47,7 +48,7 @@ nullCheck = const $ return ()
data Checker = Checker {
perScript :: Root -> Analysis,
perToken :: Token -> Analysis
perToken :: Token -> Analysis
}
runChecker :: Parameters -> Checker -> [TokenComment]
@@ -57,28 +58,30 @@ runChecker params checker = notes
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
notes = snd $ evalRWS (check $ Root root) params Cache
instance Semigroup Checker where
(<>) x y = Checker {
perScript = perScript x `composeAnalyzers` perScript y,
perToken = perToken x `composeAnalyzers` perToken y
}
instance Monoid Checker where
mempty = Checker {
perScript = nullCheck,
perToken = nullCheck
}
mappend x y = Checker {
perScript = perScript x `composeAnalyzers` perScript y,
perToken = perToken x `composeAnalyzers` perToken y
}
mappend = (Data.Semigroup.<>)
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x
data Parameters = Parameters {
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
shellType :: Shell, -- The shell type, such as Bash or Ksh
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
shellType :: Shell, -- The shell type, such as Bash or Ksh
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
rootNode :: Token -- The root node of the AST
rootNode :: Token -- The root node of the AST
}
-- TODO: Cache results of common AST ops here
@@ -106,19 +109,17 @@ data DataSource =
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec {
asScript = root,
defaultSpec root = spec {
asShellType = Nothing,
asCheckSourced = False,
asExecutionMode = Executed
}
} where spec = newAnalysisSpec root
pScript s =
let
pSpec = ParseSpec {
pSpec = newParseSpec {
psFilename = "script",
psScript = s,
psCheckSourced = False
psScript = s
}
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
@@ -132,7 +133,14 @@ producesComments c s = do
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
TokenComment id $ Comment severity code note
newTokenComment {
tcId = id,
tcComment = newComment {
cSeverity = severity,
cCode = code,
cMessage = note
}
}
addComment note = tell [note]
@@ -151,8 +159,8 @@ makeParameters spec =
case shellType params of
Bash -> containsLastpipe root
Dash -> False
Sh -> False
Ksh -> True,
Sh -> False
Ksh -> True,
shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root,
@@ -205,7 +213,7 @@ determineShell t = fromMaybe Bash $ do
forAnnotation t =
case t of
(ShellOverride s) -> return s
_ -> fail ""
_ -> fail ""
getCandidates :: Token -> [Maybe String]
getCandidates t@T_Script {} = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) =
@@ -232,9 +240,10 @@ getParentTree t =
where
pre t = modify (first ((:) t))
post t = do
(_:rest, map) <- get
case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
(x, map) <- get
case x of
_:rest -> case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
-- Given a root node, make a map from Id to Token
getTokenMap :: Token -> Map.Map Id Token
@@ -264,27 +273,27 @@ isQuoteFreeNode strict tree t =
case t of
T_Assignment {} -> return True
T_FdRedirect {} -> return True
_ -> Nothing
_ -> Nothing
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
case t of
TC_Nullary _ DoubleBracket _ -> return True
TC_Unary _ DoubleBracket _ _ -> return True
TC_Nullary _ DoubleBracket _ -> return True
TC_Unary _ DoubleBracket _ _ -> return True
TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
T_CaseExpression {} -> return True
T_HereDoc {} -> return True
T_DollarBraced {} -> return True
TA_Sequence {} -> return True
T_Arithmetic {} -> return True
T_Assignment {} -> return True
T_Redirecting {} -> return False
T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True
T_CaseExpression {} -> return True
T_HereDoc {} -> return True
T_DollarBraced {} -> return True
-- When non-strict, pragmatically assume it's desirable to split here
T_ForIn {} -> return (not strict)
T_SelectIn {} -> return (not strict)
_ -> Nothing
T_ForIn {} -> return (not strict)
T_SelectIn {} -> return (not strict)
_ -> Nothing
-- Check if a token is a parameter to a certain command by name:
-- Example: isParamTo (parentMap params) "sed" t
@@ -293,16 +302,16 @@ isParamTo tree cmd =
go
where
go x = case Map.lookup (getId x) tree of
Nothing -> False
Nothing -> False
Just parent -> check parent
check t =
case t of
T_SingleQuoted _ _ -> go t
T_DoubleQuoted _ _ -> go t
T_NormalWord _ _ -> go t
T_NormalWord _ _ -> go t
T_SimpleCommand {} -> isCommand t cmd
T_Redirecting {} -> isCommand t cmd
_ -> False
T_Redirecting {} -> isCommand t cmd
_ -> False
-- Get the parent command (T_Redirecting) of a Token, if any.
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
@@ -312,8 +321,8 @@ getClosestCommand tree t =
findCommand t =
case t of
T_Redirecting {} -> return True
T_Script {} -> return False
_ -> Nothing
T_Script {} -> return False
_ -> Nothing
-- Like above, if koala_man knew Haskell when starting this project.
getClosestCommandM t = do
@@ -334,7 +343,7 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
-- 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 -> []
Nothing -> []
Just parent -> getPath tree parent
-- Version of the above taking the map from the current context
@@ -360,9 +369,9 @@ findFirst p l =
[] -> Nothing
(x:xs) ->
case p x of
Just True -> return x
Just True -> return x
Just False -> Nothing
Nothing -> findFirst p xs
Nothing -> findFirst p xs
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
@@ -373,7 +382,7 @@ tokenIsJustCommandOutput t = case t of
_ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
check _ = False
-- TODO: Replace this with a proper Control Flow Graph
getVariableFlow params t =
@@ -393,9 +402,9 @@ getVariableFlow params t =
unless (assignFirst t) $ setWritten t
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
assignFirst T_ForIn {} = True
assignFirst T_ForIn {} = True
assignFirst T_SelectIn {} = True
assignFirst _ = False
assignFirst _ = False
setRead t =
let read = getReferencedVariables (parentMap params) t
@@ -423,7 +432,7 @@ leadType params t =
parent <- Map.lookup (getId t) (parentMap params)
case parent of
T_Pipeline {} -> return parent
_ -> Nothing
_ -> Nothing
causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline
@@ -444,15 +453,12 @@ getModifiedVariables t =
c@T_SimpleCommand {} ->
getModifiedVariableCommand c
TA_Unary _ "++|" var -> maybeToList $ do
name <- getLiteralString var
return (t, t, name, DataString $ SourceFrom [t])
TA_Unary _ "|++" var -> maybeToList $ do
name <- getLiteralString var
return (t, t, name, DataString $ SourceFrom [t])
TA_Assignment _ op lhs rhs -> maybeToList $ do
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
[(t, v, name, DataString $ SourceFrom [v])]
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
name <- getLiteralString lhs
return (t, t, name, DataString $ SourceFrom [rhs])
-- Count [[ -v foo ]] as an "assignment".
@@ -462,10 +468,10 @@ getModifiedVariables t =
flip getLiteralStringExt token $ \x ->
case x of
T_Glob _ s -> return s -- Unquoted index
_ -> Nothing
_ -> Nothing
guard . not . null $ str
return (t, token, str, DataString $ SourceChecked)
return (t, token, str, DataString SourceChecked)
T_DollarBraced _ l -> maybeToList $ do
let string = bracedString t
@@ -489,7 +495,7 @@ isClosingFileOp op =
case op of
T_IoDuplicate _ (T_GREATAND _) "-" -> True
T_IoDuplicate _ (T_LESSAND _) "-" -> True
_ -> False
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available
@@ -498,7 +504,9 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if any (`elem` flags) ["x", "p"]
"declare" -> if
any (`elem` flags) ["x", "p"] &&
(not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest
else []
"readonly" ->
@@ -518,16 +526,26 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
getReferencedVariableCommand _ = []
-- The function returns a tuple consisting of four items describing an assignment.
-- Given e.g. declare foo=bar
-- (
-- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar
-- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
-- VariableName :: String, -- The variable name, i.e. foo
-- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
-- )
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"read" ->
let params = map getLiteral rest in
catMaybes . takeWhile isJust . reverse $ params
let params = map getLiteral rest
readArrayVars = getReadArrayVariables rest
in
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
_ -> []
_ -> []
"let" -> concatMap letParamToLiteral rest
@@ -566,10 +584,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do
getLiteralOfDataType t d = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, DataString SourceExternal)
return (base, t, s, d)
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
getModifierParamString = getModifierParam DataString
@@ -591,9 +613,9 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getSetParams (t:rest) =
let s = getLiteralString t in
case s of
Just "--" -> return rest
Just "--" -> return rest
Just ('-':_) -> getSetParams rest
_ -> return (t:fromMaybe [] (getSetParams rest))
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
@@ -611,6 +633,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args = do
map (getLiteralArray . snd)
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
@@ -620,12 +647,17 @@ getIndexReferences s = fromMaybe [] $ do
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 !!! 0
offsets <- match !!! 1
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^ *:([^-=?+].*)"
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
getReferencedVariables parents t =
case t of
@@ -634,10 +666,10 @@ getReferencedVariables parents t =
map (\x -> (l, l, x)) (
getIndexReferences str
++ getOffsetReferences (getBracedModifier str))
TA_Expansion id _ ->
TA_Variable id name _ ->
if isArithmeticAssignment t
then []
else getIfReference t t
else [(t, t, name)]
T_Assignment id mode str _ word ->
[(t, t, str) | mode == Append] ++ specialReferences str t word
@@ -664,9 +696,8 @@ getReferencedVariables parents t =
else []
literalizer t = case t of
TA_Index {} -> return "" -- x[0] becomes a reference of x
T_Glob _ s -> return s -- Also when parsed as globs
_ -> Nothing
_ -> Nothing
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
@@ -678,7 +709,7 @@ getReferencedVariables parents t =
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
_ -> False
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
@@ -691,9 +722,8 @@ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `
-- Compare a command to a literal. Like above, but checks full path.
isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ do
cmd <- getCommandName token
return $ matcher cmd
isCommandMatch token matcher = fromMaybe False $
fmap matcher (getCommandName token)
-- Does this regex look like it was intended as a glob?
-- True: *foo*
@@ -701,7 +731,7 @@ isCommandMatch token matcher = fromMaybe False $ do
isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
isConfusedGlobRegex _ = False
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
@@ -711,7 +741,7 @@ prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
@@ -744,7 +774,7 @@ getBracedReference s = fromMaybe s $
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
@@ -769,12 +799,12 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
dropModifier x = [x]
-- Useful generic functions.
@@ -789,12 +819,12 @@ potentially = fromMaybe (return ())
-- Get element 0 or a default. Like `head` but safe.
headOrDefault _ (a:_) = a
headOrDefault def _ = def
headOrDefault def _ = def
--- Get element n of a list, or Nothing. Like `!!` but safe.
(!!!) list i =
case drop i list of
[] -> Nothing
[] -> Nothing
(r:_) -> Just r
-- Run a command if the shell is in the given list
@@ -807,25 +837,24 @@ filterByAnnotation asSpec params =
filter (not . shouldIgnore)
where
token = asScript asSpec
idFor (TokenComment id _) = id
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ idFor note)
getPath parents (T_Bang $ tcId note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
hasNum _ = False
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor _ _ = False
parents = parentMap params
getCode (TokenComment _ (Comment _ c _)) = c
getCode = cCode . tcComment
-- Is this a ${#anything}, to get string length or array count?
isCountingReference (T_DollarBraced id token) =
case concat $ oversimplify token of
'#':_ -> True
_ -> False
_ -> False
isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
@@ -837,7 +866,38 @@ isQuotedAlternativeReference t =
where
re = mkRegex "(^|\\]):?\\+"
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
-- read -re -d : -u 3 bar
-- into
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
-- where flags with arguments map to arguments, while others map to themselves.
-- Any unrecognized flag will result in Nothing.
getGnuOpts = getOpts getAllFlags
getBsdOpts = getOpts getLeadingFlags
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
getOpts flagTokenizer string cmd = process flags
where
flags = flagTokenizer cmd
flagList (c:':':rest) = ([c], True) : flagList rest
flagList (c:rest) = ([c], False) : flagList rest
flagList [] = []
flagMap = Map.fromList $ ("", False) : flagList string
process [] = return []
process [(token, flag)] = do
takesArg <- Map.lookup flag flagMap
guard $ not takesArg
return [(flag, token)]
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
takesArg <- Map.lookup flag1 flagMap
if takesArg
then do
guard $ flag2 == ""
more <- process rest
return $ (flag1, token2) : more
else do
more <- process rest2
return $ (flag1, token1) : more
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
@@ -37,25 +37,30 @@ import Control.Monad
import Test.QuickCheck.All
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
position <- Map.lookup id map
return $ PositionedComment position position c
tokenToPosition startMap t = fromMaybe fail $ do
span <- Map.lookup (tcId t) startMap
return $ newPositionedComment {
pcStartPos = fst span,
pcEndPos = snd span,
pcComment = tcComment t
}
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do
results <- checkScript (csScript spec)
return CheckResult {
return emptyCheckResult {
crFilename = csFilename spec,
crComments = results
}
where
checkScript contents = do
result <- parseScript sys ParseSpec {
result <- parseScript sys newParseSpec {
psFilename = csFilename spec,
psScript = contents,
psCheckSourced = csCheckSourced spec
psCheckSourced = csCheckSourced spec,
psShellTypeOverride = csShellTypeOverride spec
}
let parseMessages = prComments result
let analysisMessages =
@@ -66,27 +71,38 @@ checkScript sys spec = do
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
code `notElem` csExcludedWarnings spec
shouldInclude pc =
let code = cCode (pcComment pc)
severity = cSeverity (pcComment pc)
in
code `notElem` csExcludedWarnings spec &&
severity <= csMinSeverity spec
sortMessages = sortBy (comparing order)
order (PositionedComment pos _ (Comment severity code message)) =
(posFile pos, posLine pos, posColumn pos, severity, code, message)
getPosition (PositionedComment pos _ _) = pos
order pc =
let pos = pcStartPos pc
comment = pcComment pc in
(posFile pos,
posLine pos,
posColumn pos,
cSeverity comment,
cCode comment,
cMessage comment)
getPosition = pcStartPos
analysisSpec root =
AnalysisSpec {
as {
asScript = root,
asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed
}
} where as = newAnalysisSpec root
getErrors sys spec =
sort . map getCode . crComments $
runIdentity (checkScript sys spec)
where
getCode (PositionedComment _ _ (Comment _ code _)) = code
getCode = cCode . pcComment
check = checkWithIncludes []
@@ -136,6 +152,21 @@ prop_optionDisablesIssue2 =
csExcludedWarnings = [2148, 1037]
}
prop_wontParseBadShell =
[1071] == check "#!/usr/bin/python\ntrue $1\n"
prop_optionDisablesBadShebang =
null $ getErrors
(mockedSystemInterface [])
emptyCheckSpec {
csScript = "#!/usr/bin/python\ntrue\n",
csShellTypeOverride = Just Sh
}
prop_annotationDisablesBadShebang =
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
prop_canParseDevNull =
[] == check "source /dev/null"
@@ -180,7 +211,7 @@ prop_filewideAnnotation1 = null $
prop_filewideAnnotation2 = null $
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation3 = null $
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation4 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotation5 = null $
@@ -194,5 +225,8 @@ prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo
prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
return []
runTests = $quickCheckAll

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,15 +15,13 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker
, ShellCheck.Checks.Commands.runTests
) where
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
@@ -63,6 +61,7 @@ commandChecks = [
,checkGrepRe
,checkTrapQuotes
,checkReturn
,checkExit
,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes
,checkInjectableFindSh
@@ -88,6 +87,13 @@ commandChecks = [
,checkWhileGetoptsCase
,checkCatastrophicRm
,checkLetUsage
,checkMvArguments, checkCpArguments, checkLnArguments
,checkFindRedirections
,checkReadExpansions
,checkWhich
,checkSudoRedirect
,checkSudoArgs
,checkSourceArgs
]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -137,6 +143,7 @@ prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
@@ -148,7 +155,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
unless ("[:" `isPrefixOf` s) $
unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
Nothing -> return ()
@@ -179,7 +186,7 @@ prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)
checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t =
when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId t) 2003
style (getId $ getCommandTokenOrThis t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ]
@@ -275,15 +282,28 @@ prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments)
checkReturn = CommandCheck (Exactly "return") (returnOrExit
(\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
(\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
prop_checkExit1 = verifyNot checkExit "exit"
prop_checkExit2 = verifyNot checkExit "exit 1"
prop_checkExit3 = verifyNot checkExit "exit $var"
prop_checkExit4 = verifyNot checkExit "exit $((a|b))"
prop_checkExit5 = verify checkExit "exit -1"
prop_checkExit6 = verify checkExit "exit 1000"
prop_checkExit7 = verify checkExit "exit 'hello world'"
checkExit = CommandCheck (Exactly "exit") (returnOrExit
(\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
(\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
returnOrExit multi invalid = (f . arguments)
where
f (first:second:_) =
err (getId second) 2151
"Only one integer 0-255 can be returned. Use stdout for other data."
multi (getId first)
f [value] =
when (isInvalid $ literal value) $
err (getId value) 2152
"Can only return 0-255. Other data should be written to stdout."
invalid (getId value)
f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
@@ -320,26 +340,18 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
return ()
where allButLast = reverse . drop 1 . reverse $ args
f args = mapM_ checkEscapes args
f cmd =
whenShell [Sh, Bash, Ksh] $
unless (cmd `hasFlag` "e") $
mapM_ examine $ arguments cmd
checkEscapes (T_NormalWord _ args) =
mapM_ checkEscapes args
checkEscapes (T_DoubleQuoted id args) =
mapM_ checkEscapes args
checkEscapes (T_Literal id str) = examine id str
checkEscapes (T_SingleQuoted id str) = examine id str
checkEscapes _ = return ()
examine id str =
examine token = do
let str = onlyLiteralString token
when (str `matches` hasEscapes) $
info id 2028 "echo won't expand escape sequences. Consider printf."
info (getId token) 2028 "echo may not expand escape sequences. Use printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
@@ -480,13 +492,13 @@ checkInteractiveSu = CommandCheck (Basename "su") f
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\""
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
where
nonOptions =
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
f args =
case nonOptions args of
(hostport:r@(_:_)) -> checkArg $ last r
case partition isOption args of
([], hostport:r@(_:_)) -> checkArg $ last r
_ -> return ()
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
case filter (not . isConstant) parts of
@@ -507,6 +519,13 @@ prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@@ -517,10 +536,20 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
case string of
'%':'%':rest -> countFormats rest
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
'%':rest -> 1 + countFormats rest
'%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)
_:rest -> countFormats rest
[] -> 0
regexBasedCountFormats rest =
maybe 1 (foldl (\acc group -> acc + (if group == "*" then 1 else 0)) 1) (matchRegex re rest)
where
-- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*).?(\\d*|\\*)[diouxXfFeEgGaAcsb]"
-- \____ _____/\___ ____/ \____ ____/\________ ________/
-- V V V V
-- flags field width precision format character
-- field width and precision can be specified with a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
check format more = do
fromMaybe (return ()) $ do
string <- getLiteralString format
@@ -532,7 +561,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
"This printf format string has no variables. Other arguments are ignored."
when (vars > 0
&& length more < vars
&& ((length more) `mod` vars /= 0 || null more)
&& all (not . mayBecomeMultipleArgs) more) $
warn (getId format) 2183 $
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
@@ -585,15 +614,48 @@ checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where
check = mapM_ checkForVariables
checkForVariables f =
case getWordParts f of
[t@(T_DollarBraced {})] ->
warn (getId t) 2163 "Exporting an expansion rather than a variable."
_ -> return ()
check t = potentially $ do
var <- getSingleUnmodifiedVariable t
let name = bracedString var
return . warn (getId t) 2163 $
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
prop_checkReadExpansions1 = verify checkReadExpansions "read $var"
prop_checkReadExpansions2 = verify checkReadExpansions "read -r $var"
prop_checkReadExpansions3 = verifyNot checkReadExpansions "read -p $var"
prop_checkReadExpansions4 = verifyNot checkReadExpansions "read -rd $delim name"
prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
checkReadExpansions = CommandCheck (Exactly "read") check
where
options = getGnuOpts "sreu:n:N:i:p:a:"
getVars cmd = fromMaybe [] $ do
opts <- options cmd
return . map snd $ filter (\(x,_) -> x == "" || x == "a") opts
check cmd = mapM_ warning $ getVars cmd
warning t = potentially $ do
var <- getSingleUnmodifiedVariable t
let name = bracedString var
guard $ isVariableName name -- e.g. not $1
return . warn (getId t) 2229 $
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
-- Return the single variable expansion that makes up this word, if any.
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
getSingleUnmodifiedVariable :: Token -> Maybe Token
getSingleUnmodifiedVariable word =
case getWordParts word of
[t@(T_DollarBraced {})] ->
let contents = bracedString t
name = getBracedReference contents
in guard (contents == name) >> return t
_ -> Nothing
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
@@ -636,20 +698,24 @@ prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help"
prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print"
checkFindWithoutPath = CommandCheck (Basename "find") f
where
f (T_SimpleCommand _ _ (cmd:args)) =
unless (hasPath args) $
f t@(T_SimpleCommand _ _ (cmd:args)) =
unless (t `hasFlag` "help" || hasPath args) $
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
-- as well as multiple non-flag arguments that are not the path. We assume that all the
-- pre-path flags are single characters, which is generally the case except for -O3.
-- This is a bit of a kludge. find supports flag arguments both before and
-- after the path, as well as multiple non-flag arguments that are not the
-- path. We assume that all the pre-path flags are single characters from a
-- list of GNU and macOS flags.
hasPath (first:rest) =
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
hasPath [] = False
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag
isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
leadingFlagChars="-EHLPXdfsxO0123456789"
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
@@ -698,20 +764,20 @@ checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t
unless (any isFunction path) $
err (getId t) 2168 "'local' is only valid in functions."
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
\t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
\t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
\t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
@@ -850,5 +916,123 @@ checkLetUsage = CommandCheck (Exactly "let") f
f t = whenShell [Bash,Ksh] $ do
style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
missingDestination handler token = do
case params of
[single] -> do
unless (hasTarget || mayBecomeMultipleArgs single) $
handler token
_ -> return ()
where
args = getAllFlags token
params = map fst $ filter (\(_,x) -> x == "") args
hasTarget =
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
map snd args
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'"
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}"
prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\""
prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar"
prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar"
prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar"
prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version"
prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\""
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
where
f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
checkCpArguments = CommandCheck (Basename "cp") $ missingDestination f
where
f t = err (getId t) 2225 "This cp has no destination. Check the arguments."
checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f
where
f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly."
prop_checkFindRedirections1 = verify checkFindRedirections "find . -exec echo {} > file \\;"
prop_checkFindRedirections2 = verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
checkFindRedirections = CommandCheck (Basename "find") f
where
f t = do
redirecting <- getClosestCommandM t
case redirecting of
Just (T_Redirecting _ redirs@(_:_) (T_SimpleCommand _ _ args@(_:_:_))) -> do
-- This assumes IDs are sequential, which is mostly but not always true.
let minRedir = minimum $ map getId redirs
let maxArg = maximum $ map getId args
when (minRedir < maxArg) $
warn minRedir 2227
"Redirection applies to the find command itself. Rewrite to work per action (or move to end)."
_ -> return ()
prop_checkWhich = verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "which") $
\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_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
checkSudoRedirect = CommandCheck (Basename "sudo") f
where
f t = do
t_redir <- getClosestCommandM t
case t_redir of
Just (T_Redirecting _ redirs _) ->
mapM_ warnAbout redirs
warnAbout (T_FdRedirect _ s (T_IoFile id op file))
| (s == "" || s == "&") && not (special file) =
case op of
T_Less _ ->
info (getId op) 2024
"sudo doesn't affect redirects. Use sudo cat file | .."
T_Greater _ ->
warn (getId op) 2024
"sudo doesn't affect redirects. Use ..| sudo tee file"
T_DGREAT _ ->
warn (getId op) 2024
"sudo doesn't affect redirects. Use .. | sudo tee -a file"
_ -> return ()
warnAbout _ = return ()
special file = concat (oversimplify file) == "/dev/null"
prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root"
prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3"
prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected"
prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3"
prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls"
prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls"
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f
where
f t = potentially $ do
opts <- parseOpts t
let nonFlags = map snd $ filter (\(flag, _) -> flag == "") opts
commandArg <- nonFlags !!! 0
command <- getLiteralString commandArg
guard $ command `elem` builtins
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
builtins = [ "cd", "eval", "export", "history", "read", "source", "wait" ]
-- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg"
prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script"
prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
checkSourceArgs = CommandCheck (Exactly ".") f
where
f t = whenShell [Sh, Dash] $
case arguments t of
(file:arg1:_) -> warn (getId arg1) 2240 $
"The dot command does not support arguments in sh/dash. Set them as variables."
_ -> return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -2,7 +2,7 @@
Copyright 2012-2016 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,13 +15,11 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Checks.ShellSupport (checker
, ShellCheck.Checks.ShellSupport.runTests
) where
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
import ShellCheck.AST
import ShellCheck.ASTLib
@@ -129,13 +127,15 @@ prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask
kludge params t
@@ -191,11 +191,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism (T_Glob id str) | "[^" `isInfixOf` str =
warnMsg id "^ in place of ! in glob bracket expressions is"
bashism t@(TA_Expansion id _) | isBashism =
warnMsg id $ fromJust str ++ " is"
where
str = getLiteralString t
isBashism = isJust str && isBashVariable (fromJust str)
bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is"
bashism t@(T_DollarBraced id token) = do
mapM_ check expansion
when (isBashVariable var) $
@@ -279,14 +277,18 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset"
] ++ if not isDash then ["local", "type"] else []
] ++ if not isDash then ["local"] else []
allowedFlags = Map.fromList [
("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"]),
("exec", []),
("export", ["-p"]),
("printf", []),
("exec", [])
("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"])
]
bashism t@(T_SourceCommand id src _) =
let name = fromMaybe "" $ getCommandName src
in do
when (name == "source") $ warnMsg id "'source' in place of '.' is"
bashism _ = return ()
varChars="_0-9a-zA-Z"
@@ -295,15 +297,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
(re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
]
bashVars = [
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "LINENO" ]
dashVars = [ ]
isBashVariable var =
(var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var))

View File

@@ -39,7 +39,7 @@ internalVariables = [
]
variablesWithoutSpaces = [
"$", "-", "?", "!",
"$", "-", "?", "!", "#",
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
@@ -79,10 +79,10 @@ commonCommands = [
nonReadingCommands = [
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
"cp", "du", "echo", "export", "fg", "fuser", "getconf", "getopt",
"getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "locale", "mv",
"nice", "printf", "ps", "pwd", "renice", "rm", "rmdir", "set", "sleep",
"touch", "trap", "ulimit", "unalias", "uname"
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
]
sampleWords = [

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.CheckStyle (format) where

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.Format where
@@ -30,17 +30,17 @@ data Formatter = Formatter {
footer :: IO ()
}
sourceFile (PositionedComment pos _ _) = posFile pos
lineNo (PositionedComment pos _ _) = posLine pos
endLineNo (PositionedComment _ end _) = posLine end
colNo (PositionedComment pos _ _) = posColumn pos
endColNo (PositionedComment _ end _) = posColumn end
codeNo (PositionedComment _ _ (Comment _ code _)) = code
messageText (PositionedComment _ _ (Comment _ _ t)) = t
sourceFile = posFile . pcStartPos
lineNo = posLine . pcStartPos
endLineNo = posLine . pcEndPos
colNo = posColumn . pcStartPos
endColNo = posColumn . pcEndPos
codeNo = cCode . pcComment
messageText = cMessage . pcComment
severityText :: PositionedComment -> String
severityText (PositionedComment _ _ (Comment c _ _)) =
case c of
severityText pc =
case cSeverity (pcComment pc) of
ErrorC -> "error"
WarningC -> "warning"
InfoC -> "info"
@@ -51,11 +51,14 @@ makeNonVirtual comments contents =
map fix comments
where
ls = lines contents
fix c@(PositionedComment start end comment) = PositionedComment start {
posColumn = realignColumn lineNo colNo c
} end {
posColumn = realignColumn endLineNo endColNo c
} comment
fix c = c {
pcStartPos = (pcStartPos c) {
posColumn = realignColumn lineNo colNo c
}
, pcEndPos = (pcEndPos c) {
posColumn = realignColumn endLineNo endColNo c
}
}
realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.GCC (format) where

View File

@@ -1,8 +1,9 @@
{-# LANGUAGE OverloadedStrings #-}
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,17 +16,19 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.JSON (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.Aeson
import Data.IORef
import Data.Monoid
import GHC.Exts
import System.IO
import Text.JSON
import qualified Data.ByteString.Lazy.Char8 as BL
format = do
ref <- newIORef []
@@ -36,19 +39,36 @@ format = do
footer = finish ref
}
instance JSON (PositionedComment) where
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
("file", showJSON $ posFile start),
("line", showJSON $ posLine start),
("endLine", showJSON $ posLine end),
("column", showJSON $ posColumn start),
("endColumn", showJSON $ posColumn end),
("level", showJSON $ severityText comment),
("code", showJSON code),
("message", showJSON string)
]
instance ToJSON (PositionedComment) where
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [
"file" .= posFile start,
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end,
"level" .= severityText comment,
"code" .= cCode c,
"message" .= cMessage c
]
readJSON = undefined
toEncoding comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
pairs (
"file" .= posFile start
<> "line" .= posLine start
<> "endLine" .= posLine end
<> "column" .= posColumn start
<> "endColumn" .= posColumn end
<> "level" .= severityText comment
<> "code" .= cCode c
<> "message" .= cMessage c
)
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref result _ =
@@ -56,5 +76,5 @@ collectResult ref result _ =
finish ref = do
list <- readIORef ref
putStrLn $ encodeStrict list
BL.putStrLn $ encode list

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,25 +15,34 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.TTY (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.Monad
import Data.IORef
import Data.List
import GHC.Exts
import System.Info
import System.IO
import System.Info
wikiLink = "https://www.shellcheck.net/wiki/"
-- An arbitrary Ord thing to order warnings
type Ranking = (Char, Severity, Integer)
format :: FormatterOptions -> IO Formatter
format options = return Formatter {
header = return (),
footer = return (),
onFailure = outputError options,
onResult = outputResult options
}
format options = do
topErrorRef <- newIORef []
return Formatter {
header = return (),
footer = outputWiki topErrorRef,
onFailure = outputError options,
onResult = outputResult options topErrorRef
}
colorForLevel level =
case level of
@@ -45,13 +54,60 @@ colorForLevel level =
"source" -> 0 -- none
_ -> 0 -- none
rankError :: PositionedComment -> Ranking
rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
where
ranking =
if cCode (pcComment err) `elem` uninteresting
then 'Z'
else 'A'
-- A list of the most generic, least directly helpful
-- error codes to downrank.
uninteresting = [
1009, -- Mentioned parser error was..
1019, -- Expected this to be an argument
1036, -- ( is invalid here
1047, -- Expected 'fi'
1062, -- Expected 'done'
1070, -- Parsing stopped here (generic)
1072, -- Missing/unexpected ..
1073, -- Couldn't parse this ..
1088, -- Parsing stopped here (paren)
1089 -- Parsing stopped here (keyword)
]
appendComments errRef comments max = do
previous <- readIORef errRef
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
where
fst3 (x,_,_) = x
equal x y = fst3 x == fst3 y
outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
outputWiki errRef = do
issues <- readIORef errRef
unless (null issues) $ do
putStrLn "For more information:"
mapM_ showErr issues
where
showErr (_, code, msg) =
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
limit = 36
shorten msg =
if length msg < limit
then msg
else (take (limit-3) msg) ++ "..."
outputError options file error = do
color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult options result sys = do
outputResult options ref result sys = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
let fileGroups = groupWith sourceFile comments
mapM_ (outputForFile color sys) fileGroups
@@ -78,9 +134,16 @@ outputForFile color sys comments = do
cuteIndent :: PositionedComment -> String
cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
where
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
makeArrow =
let sameLine = lineNo comment == endLineNo comment
delta = endColNo comment - colNo comment
in
if sameLine && delta > 2 && delta < 32 then arrow delta else "^--"
code code = "SC" ++ show code
code num = "SC" ++ show num
getColorFunc colorOption = do
term <- hIsTerminalDevice stdout

232
src/ShellCheck/Interface.hs Normal file
View File

@@ -0,0 +1,232 @@
{-
Copyright 2012-2015 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/>.
-}
module ShellCheck.Interface
(
SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)
, CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced)
, AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash)
, ExecutionMode(Executed, Sourced)
, ErrorMessage
, Code
, Severity(ErrorC, WarningC, InfoC, StyleC)
, Position(posFile, posLine, posColumn)
, Comment(cSeverity, cCode, cMessage)
, PositionedComment(pcStartPos , pcEndPos , pcComment)
, ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment)
, emptyCheckResult
, newParseResult
, newAnalysisSpec
, newAnalysisResult
, newFormatterOptions
, newPosition
, newTokenComment
, mockedSystemInterface
, newParseSpec
, emptyCheckSpec
, newPositionedComment
, newComment
) where
import ShellCheck.AST
import Control.Monad.Identity
import qualified Data.Map as Map
newtype SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String)
}
-- ShellCheck input and output
data CheckSpec = CheckSpec {
csFilename :: String,
csScript :: String,
csCheckSourced :: Bool,
csExcludedWarnings :: [Integer],
csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity
} deriving (Show, Eq)
data CheckResult = CheckResult {
crFilename :: String,
crComments :: [PositionedComment]
} deriving (Show, Eq)
emptyCheckResult :: CheckResult
emptyCheckResult = CheckResult {
crFilename = "",
crComments = []
}
emptyCheckSpec :: CheckSpec
emptyCheckSpec = CheckSpec {
csFilename = "",
csScript = "",
csCheckSourced = False,
csExcludedWarnings = [],
csShellTypeOverride = Nothing,
csMinSeverity = StyleC
}
newParseSpec :: ParseSpec
newParseSpec = ParseSpec {
psFilename = "",
psScript = "",
psCheckSourced = False,
psShellTypeOverride = Nothing
}
-- Parser input and output
data ParseSpec = ParseSpec {
psFilename :: String,
psScript :: String,
psCheckSourced :: Bool,
psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq)
data ParseResult = ParseResult {
prComments :: [PositionedComment],
prTokenPositions :: Map.Map Id (Position, Position),
prRoot :: Maybe Token
} deriving (Show, Eq)
newParseResult :: ParseResult
newParseResult = ParseResult {
prComments = [],
prTokenPositions = Map.empty,
prRoot = Nothing
}
-- Analyzer input and output
data AnalysisSpec = AnalysisSpec {
asScript :: Token,
asShellType :: Maybe Shell,
asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool
}
newAnalysisSpec token = AnalysisSpec {
asScript = token,
asShellType = Nothing,
asExecutionMode = Executed,
asCheckSourced = False
}
newtype AnalysisResult = AnalysisResult {
arComments :: [TokenComment]
}
newAnalysisResult = AnalysisResult {
arComments = []
}
-- Formatter options
data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption,
foWikiLinkCount :: Integer
}
newFormatterOptions = FormatterOptions {
foColorOption = ColorAuto,
foWikiLinkCount = 3
}
-- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String
type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
data Position = Position {
posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq)
newPosition :: Position
newPosition = Position {
posFile = "",
posLine = 1,
posColumn = 1
}
data Comment = Comment {
cSeverity :: Severity,
cCode :: Code,
cMessage :: String
} deriving (Show, Eq)
newComment :: Comment
newComment = Comment {
cSeverity = StyleC,
cCode = 0,
cMessage = ""
}
data PositionedComment = PositionedComment {
pcStartPos :: Position,
pcEndPos :: Position,
pcComment :: Comment
} deriving (Show, Eq)
newPositionedComment :: PositionedComment
newPositionedComment = PositionedComment {
pcStartPos = newPosition,
pcEndPos = newPosition,
pcComment = newComment
}
data TokenComment = TokenComment {
tcId :: Id,
tcComment :: Comment
} deriving (Show, Eq)
newTokenComment = TokenComment {
tcId = Id 0,
tcComment = newComment
}
data ColorOption =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {
siReadFile = rf
}
where
rf file =
case filter ((== file) . fst) files of
[] -> return $ Left "File not included in mock."
[(_, contents)] -> return $ Right contents

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/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
@@ -15,7 +15,7 @@
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE FlexibleContexts #-}

View File

@@ -1,5 +1,5 @@
# This file was automatically generated by stack init
# For more information, see: http://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)
resolver: lts-8.5

78
striptests Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# This file strips all unit tests from ShellCheck, removing
# the dependency on QuickCheck and Template Haskell and
# reduces the binary size considerably.
set -o pipefail
sponge() {
local data
data="$(cat)"
printf '%s\n' "$data" > "$1"
}
modify() {
if ! "${@:2}" < "$1" | sponge "$1"
then
{
printf 'Failed to modify %s: ' "$1"
printf '%q ' "${@:2}"
printf '\n'
} >&2
exit 1
fi
}
detestify() {
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
awk '
BEGIN {
state = 0;
}
/LANGUAGE TemplateHaskell/ { next; }
/^import.*Test\./ { next; }
/^module/ {
sub(/,[^,)]*runTests/, "");
}
# Delete tests
/^prop_/ { state = 1; next; }
# ..and any blank lines following them.
state == 1 && /^ / { next; }
# Template Haskell marker
/^return / {
exit;
}
{ state = 0; print; }
'
}
if [[ ! -e 'ShellCheck.cabal' ]]
then
echo "Run me from the ShellCheck directory." >&2
exit 1
fi
if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
then
echo "You have local changes! These may be overwritten." >&2
exit 2
fi
modify 'ShellCheck.cabal' sed -e '
/QuickCheck/d
/^test-suite/{ s/.*//; q; }
'
find . -name '.git' -prune -o -type f -name '*.hs' -print |
while IFS= read -r file
do
modify "$file" detestify
done

35
test/buildtest Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# This script configures, builds and runs tests.
# It's meant for automatic cross-distro testing.
die() { echo "$*" >&2; exit 1; }
[ -e "ShellCheck.cabal" ] ||
die "ShellCheck.cabal not in current dir"
command -v cabal ||
die "cabal is missing"
cabal update ||
die "can't update"
cabal install --dependencies-only --enable-tests ||
die "can't install dependencies"
cabal configure --enable-tests ||
die "configure failed"
cabal build ||
die "build failed"
cabal test ||
die "test failed"
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
#!/bin/sh
echo "Hello World"
EOF
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
#!/bin/sh
echo $1
EOF
echo "Success"
exit 0

80
test/distrotest Executable file
View File

@@ -0,0 +1,80 @@
#!/bin/bash
# This script runs 'buildtest' on each of several distros
# via Docker.
set -o pipefail
exec 3>&1 4>&2
die() { echo "$*" >&4; exit 1; }
[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir"
[ "$1" = "--run" ] || {
cat << EOF
This script pulls multiple distros via Docker and compiles
ShellCheck and dependencies for each one. It takes hours,
and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue.
Also note that 'dist' will be deleted.
EOF
exit 0
}
echo "Deleting 'dist'..."
rm -rf dist
log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log"
echo "Logging to $log" >&3
exec >> "$log" 2>&1
final=0
while read -r distro setup
do
[[ "$distro" = "#"* || -z "$distro" ]] && continue
printf '%s ' "$distro" >&3
docker pull "$distro" || die "Can't pull $distro"
printf 'pulled. ' >&3
tmp=$(mktemp -d) || die "Can't make temp dir"
cp -r . "$tmp/" || die "Can't populate test dir"
printf 'Result: ' >&3
< /dev/null docker run -v "$tmp:/mnt" "$distro" sh -c "
$setup
cd /mnt || exit 1
test/buildtest
"
ret=$?
if [ "$ret" = 0 ]
then
echo "OK" >&3
else
echo "FAIL with $ret. See $log" >&3
final=1
fi
rm -rf "$tmp"
done << EOF
# Docker tag Setup command
debian:stable apt-get update && apt-get install -y cabal-install
debian:testing apt-get update && apt-get install -y cabal-install
ubuntu:latest apt-get update && apt-get install -y cabal-install
opensuse:latest zypper install -y cabal-install ghc
# Older Ubuntu versions we want to support
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:17.10 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
haskell:latest true
# Known to currently fail
centos:latest yum install -y epel-release && yum install -y cabal-install
fedora:latest dnf install -y cabal-install
base/archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
EOF
exit "$final"

27
test/stacktest Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# This script builds ShellCheck through `stack` using
# various resolvers. It's run via distrotest.
resolvers=(
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
)
die() { echo "$*" >&2; exit 1; }
[ -e "ShellCheck.cabal" ] ||
die "ShellCheck.cabal not in current dir"
[ -e "stack.yaml" ] ||
die "stack.yaml not in current dir"
command -v stack ||
die "stack is missing"
stack setup || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
for resolver in "${resolvers[@]}"
do
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
done
echo "Success"